@solutiondesk/changelog 0.0.0 → 1.0.0-2cf6d3e

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wemogy GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,45 +1,93 @@
1
1
  # @solutiondesk/changelog
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ Typed client for the public [SolutionDesk](https://solutiondesk.io) changelogs, plus React hooks and unstyled components. Your team writes the updates for each of its products in SolutionDesk; your app or website shows them with this package.
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ - The client has no dependencies, ships ESM and CommonJS with full TypeScript types, and runs wherever `fetch` does: Node.js 18+, Deno, Bun, edge runtimes and browsers.
6
+ - No key and no login: the API only ever returns published entries.
7
+ - `@solutiondesk/changelog/react`: a hook and unstyled components (`ChangelogFeed`, `WhatsNew`) that take your design system's styles. React is an optional peer dependency.
6
8
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
9
+ ```bash
10
+ npm install @solutiondesk/changelog
11
+ ```
8
12
 
9
- ## Purpose
13
+ No code at all? The [Products guide](https://help.solutiondesk.io/docs/team/changelog) shows the hosted page and the ready-made "What's new" button (two lines of HTML).
10
14
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@solutiondesk/changelog`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
15
+ ## Quick start
15
16
 
16
- ## What is OIDC Trusted Publishing?
17
+ ```ts
18
+ import { createChangelogClient } from "@solutiondesk/changelog";
17
19
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
20
+ const changelog = createChangelogClient({ workspace: "acme", product: "acme-mobile" });
19
21
 
20
- ## Setup Instructions
22
+ const { title, entries, nextCursor } = await changelog.list({ limit: 10 });
23
+ const { entry } = await changelog.getEntry("export-to-csv");
24
+ ```
21
25
 
22
- To properly configure OIDC trusted publishing for this package:
26
+ `workspace` and `product` are the two parts of the address shown under **Products → your product → Page & settings → For developers** (`https://api.solutiondesk.io/v1/changelog/acme/acme-mobile`). Leave `product` out to read the workspace's default product.
23
27
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
28
+ ## What you can read
28
29
 
29
- ## DO NOT USE THIS PACKAGE
30
+ | Method | API | Returns |
31
+ | --- | --- | --- |
32
+ | `list({ category, limit, cursor })` | `GET /v1/changelog/{workspace}/{product}` | The `product`, its title and button settings, the hosted `pageUrl`, one page of entries (newest first) and `nextCursor` (`limit` 1–50, default 20). |
33
+ | `allEntries({ category })` | the same, page by page | An async iterator over every published entry, e.g. for a sitemap or an RSS feed. |
34
+ | `getEntry(slug)` | `GET /v1/changelog/{workspace}/{product}/entries/{slug}` | One entry and its product. |
35
+ | `listProducts()` | `GET /v1/changelog/{workspace}/products` | Every product (name, slug, description, `pageUrl`), the default first. |
30
36
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
37
+ Each entry has `title`, `summary` (one or two sentences), `category` (`new`, `improved`, `fixed`, `announcement`), `publishedAt`, the details as `markdown` and as `html`, and `url` (the entry on the hosted page). `html` is safe to insert as it is: raw HTML in the team's text is escaped, links only go to web addresses and paths, images only come from https addresses.
36
38
 
37
- ## More Information
39
+ A workspace or product that doesn't exist, a changelog that is turned off and an unpublished entry all answer `404` (`SolutionDeskError` with `isNotFound`).
38
40
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
41
+ ## "New" badge
42
42
 
43
- ---
43
+ ```ts
44
+ import { createSeenTracker } from "@solutiondesk/changelog";
44
45
 
45
- **Maintained for OIDC setup purposes only**
46
+ const seen = createSeenTracker("acme", undefined, "acme-mobile"); // the browser's localStorage by default
47
+ const unseen = seen.unseen(entries); // entries newer than the visitor's last look
48
+ seen.markSeen(entries[0].publishedAt); // when they open your panel
49
+ ```
50
+
51
+ On the very first visit nothing counts as new — remember the newest entry then, so the next one is the first that counts. The tracker uses the same key as SolutionDesk's own button, and nothing about the visitor leaves their browser.
52
+
53
+ `categoryLabel(category, locale)`, `CATEGORY_LABELS` and `CATEGORY_COLORS` give you the names (German and English) and colors the hosted page uses.
54
+
55
+ ## React
56
+
57
+ ```tsx
58
+ import { ChangelogFeed, WhatsNew } from "@solutiondesk/changelog/react";
59
+
60
+ // In your app's header: a button with the unseen count that opens the latest entries.
61
+ <WhatsNew workspace="acme" product="acme-mobile" classNames={{ button: "btn", badge: "badge", panel: "popover" }} />
62
+
63
+ // A whole changelog page: entries with their details and "Older updates".
64
+ <ChangelogFeed workspace="acme" product="acme-mobile" locale="de" entryClassNames={{ title: "text-xl font-semibold" }} />
65
+ ```
66
+
67
+ The components render plain elements with `className` props and `data-sd-*` attributes (`data-sd-changelog-entry`, `data-category`, `data-sd-whats-new-badge` …) and ship no CSS. Both render nothing (or your `fallback`) when the changelog can't be loaded, so they never break your page.
68
+
69
+ For your own markup, use the hook:
70
+
71
+ ```tsx
72
+ import { useChangelog } from "@solutiondesk/changelog/react";
73
+
74
+ function Updates() {
75
+ const { status, entries, unseen, markAllSeen, hasMore, loadMore } = useChangelog({
76
+ workspace: "acme",
77
+ product: "acme-mobile",
78
+ limit: 5,
79
+ });
80
+ if (status !== "ready") return null;
81
+ // …
82
+ }
83
+ ```
84
+
85
+ Pass `initialData` (a `list()` answer fetched on your server) for a first paint without loading. `ChangelogEntryView` and `ChangelogEntryBody` render a single entry or just its details.
86
+
87
+ ## Options
88
+
89
+ `createChangelogClient` (and the hook) take `baseUrl` (default `https://api.solutiondesk.io`), `fetch`, `headers` and `etagCache` (revalidate with `If-None-Match`). Per call, `fetchOptions` is handed to `fetch` unchanged — e.g. Next.js `{ next: { revalidate: 300, tags: ["changelog"] } }`.
90
+
91
+ ## License
92
+
93
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,263 @@
1
+ 'use strict';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
5
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
6
+
7
+ // ../sdk-core/src/http.ts
8
+ var DEFAULT_BASE_URL = "https://api.solutiondesk.io";
9
+ var SolutionDeskError = class extends Error {
10
+ constructor(message, init) {
11
+ super(message);
12
+ /** The HTTP status; `0` when no answer arrived (network error, aborted). */
13
+ __publicField(this, "status");
14
+ /** The parsed JSON body of the error answer (usually `{ error: string }`), else its text. */
15
+ __publicField(this, "body");
16
+ /** The requested URL. */
17
+ __publicField(this, "url");
18
+ /** Messages per input field on a `400` (e.g. `{ email: "Enter a valid email address." }`). */
19
+ __publicField(this, "fields");
20
+ this.name = "SolutionDeskError";
21
+ this.status = init.status;
22
+ this.body = init.body ?? null;
23
+ this.url = init.url;
24
+ this.fields = fieldsOf(init.body);
25
+ if (init.cause !== void 0) {
26
+ Object.defineProperty(this, "cause", { value: init.cause, enumerable: false });
27
+ }
28
+ }
29
+ /** `404`: unknown workspace, module turned off or unknown item — on purpose indistinguishable. */
30
+ get isNotFound() {
31
+ return this.status === 404;
32
+ }
33
+ /** `429`: over the rate limit; try again later. */
34
+ get isRateLimited() {
35
+ return this.status === 429;
36
+ }
37
+ };
38
+ function fieldsOf(body) {
39
+ if (!body || typeof body !== "object") return {};
40
+ const fields = body.fields;
41
+ if (!fields || typeof fields !== "object") return {};
42
+ const out = {};
43
+ for (const [key, value] of Object.entries(fields)) {
44
+ if (typeof value === "string") out[key] = value;
45
+ }
46
+ return out;
47
+ }
48
+ function messageOf(body, status) {
49
+ if (body && typeof body === "object") {
50
+ const { error, message, statusCode } = body;
51
+ if (typeof statusCode === "number" && typeof message === "string" && message) return message;
52
+ if (typeof error === "string" && error) return error;
53
+ if (typeof message === "string" && message) return message;
54
+ if (Array.isArray(message) && typeof message[0] === "string") return message[0];
55
+ }
56
+ if (typeof body === "string" && body.trim() && body.length <= 300) return body.trim();
57
+ return `SolutionDesk answered ${status}.`;
58
+ }
59
+ function createMemoryEtagCache(maxEntries = 500) {
60
+ const entries = /* @__PURE__ */ new Map();
61
+ return {
62
+ get: (key) => entries.get(key),
63
+ set(key, value) {
64
+ entries.delete(key);
65
+ entries.set(key, value);
66
+ if (entries.size > maxEntries) {
67
+ const oldest = entries.keys().next().value;
68
+ if (oldest !== void 0) entries.delete(oldest);
69
+ }
70
+ }
71
+ };
72
+ }
73
+ function queryString(query) {
74
+ if (!query) return "";
75
+ const parts = [];
76
+ for (const [key, value] of Object.entries(query)) {
77
+ if (value === void 0 || value === null || value === "") continue;
78
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
79
+ }
80
+ return parts.length ? `?${parts.join("&")}` : "";
81
+ }
82
+ var seg = (value) => encodeURIComponent(String(value));
83
+ function parseBody(text) {
84
+ if (!text) return null;
85
+ try {
86
+ return JSON.parse(text);
87
+ } catch {
88
+ return text;
89
+ }
90
+ }
91
+ function createHttp(options = {}) {
92
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "").replace(/\/v1$/, "");
93
+ const fetchImpl = options.fetch ?? globalThis.fetch;
94
+ const cache = options.etagCache === true ? createMemoryEtagCache() : options.etagCache ? options.etagCache : void 0;
95
+ const url = (path, query) => `${baseUrl}/v1${path}${queryString(query)}`;
96
+ async function requestWithMeta(req) {
97
+ if (!fetchImpl) {
98
+ throw new Error("No global fetch available: pass `fetch` to the client.");
99
+ }
100
+ const target = url(req.path, req.query);
101
+ const headers = {
102
+ accept: req.accept ?? "application/json",
103
+ ...options.headers,
104
+ ...req.options?.headers
105
+ };
106
+ if (req.body !== void 0) headers["content-type"] = "application/json";
107
+ if (req.apiKey) headers.authorization = `Bearer ${req.apiKey}`;
108
+ const cacheKey = req.method === "GET" && !req.apiKey ? target : void 0;
109
+ const cached = cacheKey && cache ? cache.get(cacheKey) : void 0;
110
+ if (cached && !Object.keys(headers).some((h) => h.toLowerCase() === "if-none-match")) {
111
+ headers["if-none-match"] = cached.etag;
112
+ }
113
+ const init = {
114
+ ...req.options?.fetchOptions,
115
+ method: req.method,
116
+ headers
117
+ };
118
+ if (req.body !== void 0) init.body = JSON.stringify(req.body);
119
+ if (req.options?.signal !== void 0) init.signal = req.options.signal;
120
+ let response;
121
+ try {
122
+ response = await fetchImpl(target, init);
123
+ } catch (cause) {
124
+ const reason = cause instanceof Error ? cause.message : String(cause);
125
+ throw new SolutionDeskError(`The request to SolutionDesk failed: ${reason}`, {
126
+ status: 0,
127
+ url: target,
128
+ cause
129
+ });
130
+ }
131
+ const finalUrl = response.url;
132
+ const meta = {
133
+ status: response.status,
134
+ finalUrl: typeof finalUrl === "string" && finalUrl ? finalUrl : null
135
+ };
136
+ if (response.status === 304 && cached) {
137
+ return { data: cached.body, ...meta };
138
+ }
139
+ const text = await response.text();
140
+ const body = response.ok && req.responseType === "text" ? text : parseBody(text);
141
+ if (!response.ok) {
142
+ throw new SolutionDeskError(messageOf(body, response.status), {
143
+ status: response.status,
144
+ body,
145
+ url: target
146
+ });
147
+ }
148
+ const etag = response.headers.get("etag");
149
+ if (cacheKey && cache && etag) cache.set(cacheKey, { etag, body });
150
+ return { data: body, ...meta };
151
+ }
152
+ return {
153
+ baseUrl,
154
+ url,
155
+ requestWithMeta,
156
+ request: async (req) => (await requestWithMeta(req)).data
157
+ };
158
+ }
159
+ function required(value, name) {
160
+ if (typeof value !== "string" || !value.trim()) {
161
+ throw new TypeError(`SolutionDesk: \`${name}\` is required.`);
162
+ }
163
+ return value;
164
+ }
165
+
166
+ // src/client.ts
167
+ function createChangelogClient(options) {
168
+ const workspace = required(options.workspace, "workspace");
169
+ const product = options.product?.trim() || void 0;
170
+ const http = createHttp(options);
171
+ const root = `/changelog/${seg(workspace)}`;
172
+ const base = product ? `${root}/${seg(product)}` : root;
173
+ const list = (params = {}, requestOptions) => http.request({
174
+ method: "GET",
175
+ path: base,
176
+ query: { category: params.category, limit: params.limit, cursor: params.cursor },
177
+ options: requestOptions
178
+ });
179
+ async function* allEntries(params = {}, requestOptions) {
180
+ let cursor = null;
181
+ do {
182
+ const page = await list(
183
+ { ...params, limit: params.limit ?? 50, cursor },
184
+ requestOptions
185
+ );
186
+ yield* page.entries;
187
+ cursor = page.nextCursor;
188
+ } while (cursor);
189
+ }
190
+ return {
191
+ workspace,
192
+ product,
193
+ list,
194
+ allEntries,
195
+ getEntry: (slug, requestOptions) => http.request({
196
+ method: "GET",
197
+ path: `${base}/entries/${seg(required(slug, "slug"))}`,
198
+ options: requestOptions
199
+ }),
200
+ listProducts: (requestOptions) => http.request({
201
+ method: "GET",
202
+ path: `${root}/products`,
203
+ options: requestOptions
204
+ })
205
+ };
206
+ }
207
+
208
+ // src/seen.ts
209
+ function browserStorage() {
210
+ try {
211
+ const storage = globalThis.localStorage;
212
+ return storage ?? null;
213
+ } catch {
214
+ return null;
215
+ }
216
+ }
217
+ function createSeenTracker(workspace, storage = browserStorage(), product) {
218
+ const key = `sd-changelog-seen:${workspace.toLowerCase()}${product ? `:${product.toLowerCase()}` : ""}`;
219
+ const lastSeen = () => {
220
+ try {
221
+ return storage?.getItem(key) ?? null;
222
+ } catch {
223
+ return null;
224
+ }
225
+ };
226
+ return {
227
+ lastSeen,
228
+ markSeen(publishedAt) {
229
+ try {
230
+ storage?.setItem(key, publishedAt);
231
+ } catch {
232
+ }
233
+ },
234
+ unseen(entries) {
235
+ const seen = lastSeen();
236
+ if (!seen) return 0;
237
+ return entries.filter((entry) => entry.publishedAt > seen).length;
238
+ }
239
+ };
240
+ }
241
+ var CATEGORY_LABELS = {
242
+ en: { new: "New", improved: "Improved", fixed: "Fixed", announcement: "Announcement" },
243
+ de: { new: "Neu", improved: "Verbessert", fixed: "Behoben", announcement: "Ank\xFCndigung" }
244
+ };
245
+ var CATEGORY_COLORS = {
246
+ new: "#2563eb",
247
+ improved: "#7c3aed",
248
+ fixed: "#16a34a",
249
+ announcement: "#d97706"
250
+ };
251
+ function categoryLabel(category, locale = "en") {
252
+ return CATEGORY_LABELS[locale.toLowerCase().startsWith("de") ? "de" : "en"][category];
253
+ }
254
+
255
+ exports.CATEGORY_COLORS = CATEGORY_COLORS;
256
+ exports.CATEGORY_LABELS = CATEGORY_LABELS;
257
+ exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
258
+ exports.SolutionDeskError = SolutionDeskError;
259
+ exports.browserStorage = browserStorage;
260
+ exports.categoryLabel = categoryLabel;
261
+ exports.createChangelogClient = createChangelogClient;
262
+ exports.createMemoryEtagCache = createMemoryEtagCache;
263
+ exports.createSeenTracker = createSeenTracker;
@@ -0,0 +1,273 @@
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
+ * The answers of the public changelog API (`GET /v1/changelog/{workspace}/…`).
99
+ * Written by hand so the package has no dependencies; a drift test in the
100
+ * SolutionDesk repository keeps them identical to the API's own contracts.
101
+ */
102
+ type ChangelogCategory = "new" | "improved" | "fixed" | "announcement";
103
+ type ChangelogWidgetPosition = "bottom-right" | "bottom-left";
104
+ /** A published entry. */
105
+ interface PublicChangelogEntry {
106
+ slug: string;
107
+ title: string;
108
+ /** One or two sentences — what a "What's new" panel shows. May be empty. */
109
+ summary: string;
110
+ category: ChangelogCategory;
111
+ /** The release this change ships in (`2.3.0`); empty when it isn't tied to one. */
112
+ releaseVersion: string;
113
+ /** Where the release can be downloaded (an app store page, GitHub Releases …), or null. */
114
+ downloadUrl: string | null;
115
+ /** ISO 8601. The list is sorted by it, newest first. */
116
+ publishedAt: string;
117
+ /** The details as the team wrote them (Markdown). */
118
+ markdown: string;
119
+ /**
120
+ * The details as safe HTML: raw HTML in the text is escaped, links are only
121
+ * http(s)/mailto/relative, images only the ones uploaded to the entry (with
122
+ * `srcset`). Ready for `innerHTML` / `dangerouslySetInnerHTML`. Empty when
123
+ * there are no details.
124
+ */
125
+ html: string;
126
+ /** The entry on the workspace's hosted changelog page. */
127
+ url: string;
128
+ }
129
+ /** A product with its own changelog. */
130
+ interface PublicChangelogProduct {
131
+ name: string;
132
+ slug: string;
133
+ /** One sentence about the product. May be empty. */
134
+ description: string;
135
+ /** The product's hosted changelog page. */
136
+ pageUrl: string;
137
+ /** The default product: addresses without a product show this one. */
138
+ isDefault: boolean;
139
+ }
140
+ /** `GET /v1/changelog/{workspace}/products` — every product, the default first. */
141
+ interface PublicChangelogProductsPayload {
142
+ workspace: {
143
+ name: string;
144
+ slug: string;
145
+ logoUrl: string | null;
146
+ };
147
+ products: PublicChangelogProduct[];
148
+ }
149
+ /**
150
+ * `GET /v1/changelog/{workspace}/{product}` — a product's settings and one
151
+ * page of its entries (`GET /v1/changelog/{workspace}`: the default product).
152
+ */
153
+ interface PublicChangelogPayload {
154
+ /** The workspace as the hosted page shows it; `logoUrl` is relative to the API. */
155
+ workspace: {
156
+ name: string;
157
+ slug: string;
158
+ logoUrl: string | null;
159
+ };
160
+ /** The product these entries belong to. */
161
+ product: PublicChangelogProduct;
162
+ /** The heading the team chose; empty ⇒ use your own ("What's new"). */
163
+ title: string;
164
+ /** The team's settings for the "What's new" button (empty label ⇒ your own). */
165
+ widget: {
166
+ label: string;
167
+ position: ChangelogWidgetPosition;
168
+ };
169
+ /** The product's hosted changelog page. */
170
+ pageUrl: string;
171
+ entries: PublicChangelogEntry[];
172
+ /** Pass as `cursor` for the next page; null on the last one. */
173
+ nextCursor: string | null;
174
+ }
175
+ /** `GET /v1/changelog/{workspace}/{product}/entries/{slug}`. */
176
+ interface PublicChangelogEntryPayload {
177
+ /** The product whose changelog shows the entry. */
178
+ product: PublicChangelogProduct;
179
+ entry: PublicChangelogEntry;
180
+ }
181
+
182
+ interface ChangelogClientOptions extends BaseClientOptions {
183
+ /** Your workspace's slug — the `acme` in `https://api.solutiondesk.io/v1/changelog/acme`. */
184
+ workspace: string;
185
+ /**
186
+ * The product whose changelog to read (its web address, e.g. `acme-mobile`).
187
+ * Without it, the workspace's default product.
188
+ */
189
+ product?: string;
190
+ }
191
+ interface ListEntriesParams {
192
+ /** Only entries of this category. */
193
+ category?: ChangelogCategory;
194
+ /** Page size, 1–50 (default 20). */
195
+ limit?: number;
196
+ /** `nextCursor` of the previous page. */
197
+ cursor?: string | null;
198
+ }
199
+ interface ChangelogClient {
200
+ /** The workspace this client reads. */
201
+ readonly workspace: string;
202
+ /** The product this client reads, or undefined for the default product. */
203
+ readonly product: string | undefined;
204
+ /**
205
+ * The product's settings and one page of its published entries, newest first
206
+ * (`GET /v1/changelog/{workspace}/{product}`). Throws a `SolutionDeskError`
207
+ * with status 404 when the workspace or product doesn't exist or the
208
+ * changelog is turned off.
209
+ */
210
+ list(params?: ListEntriesParams, options?: RequestOptions): Promise<PublicChangelogPayload>;
211
+ /** Every published entry, page by page — e.g. for a sitemap, an RSS feed or a static build. */
212
+ allEntries(params?: Omit<ListEntriesParams, "cursor">, options?: RequestOptions): AsyncGenerator<PublicChangelogEntry, void, undefined>;
213
+ /**
214
+ * One published entry of the product (`…/{product}/entries/{slug}`; without
215
+ * a product, of any product). Throws a `SolutionDeskError` with status 404
216
+ * for an unknown or unpublished entry.
217
+ */
218
+ getEntry(slug: string, options?: RequestOptions): Promise<PublicChangelogEntryPayload>;
219
+ /** Every product of the workspace, the default first (`GET /v1/changelog/{workspace}/products`). */
220
+ listProducts(options?: RequestOptions): Promise<PublicChangelogProductsPayload>;
221
+ }
222
+ /**
223
+ * A client for the public changelog API — no key, no login; only published
224
+ * entries. Safe in the browser and on the server.
225
+ *
226
+ * @example
227
+ * const changelog = createChangelogClient({ workspace: "acme", product: "acme-mobile" });
228
+ * const { entries, nextCursor } = await changelog.list({ limit: 10 });
229
+ */
230
+ declare function createChangelogClient(options: ChangelogClientOptions): ChangelogClient;
231
+
232
+ /** Where "last seen" is kept — `window.localStorage`, `sessionStorage` or your own. */
233
+ interface SeenStorage {
234
+ getItem(key: string): string | null;
235
+ setItem(key: string, value: string): void;
236
+ }
237
+ /**
238
+ * Remembers when a visitor last looked at the changelog, so you can show a
239
+ * "new" badge. It lives where you keep it (by default the browser's
240
+ * `localStorage`); nothing about the visitor is sent to SolutionDesk.
241
+ */
242
+ interface SeenTracker {
243
+ /** ISO time of the newest entry the visitor has seen, or null before the first look. */
244
+ lastSeen(): string | null;
245
+ /** Remember that the visitor has seen everything up to `publishedAt`. */
246
+ markSeen(publishedAt: string): void;
247
+ /**
248
+ * How many of `entries` are new to the visitor. On the very first visit
249
+ * nothing counts as new — call {@link SeenTracker.markSeen} with the newest
250
+ * entry then, so the next entry is the first one that counts.
251
+ */
252
+ unseen(entries: readonly {
253
+ publishedAt: string;
254
+ }[]): number;
255
+ }
256
+ /** The browser's `localStorage`, or null (server rendering, privacy mode). */
257
+ declare function browserStorage(): SeenStorage | null;
258
+ /**
259
+ * A {@link SeenTracker} for one workspace's product. Uses the key
260
+ * `sd-changelog-seen:<workspace>` (default product) or
261
+ * `sd-changelog-seen:<workspace>:<product>` — the same ones the SolutionDesk
262
+ * widget uses, so a visitor who opened either sees no badge in the other.
263
+ */
264
+ declare function createSeenTracker(workspace: string, storage?: SeenStorage | null, product?: string): SeenTracker;
265
+ type ChangelogLocale = "de" | "en";
266
+ /** The category names the hosted page uses, for your own UI. */
267
+ declare const CATEGORY_LABELS: Record<ChangelogLocale, Record<ChangelogCategory, string>>;
268
+ /** The category colors of the hosted page and the dashboard (hex). */
269
+ declare const CATEGORY_COLORS: Record<ChangelogCategory, string>;
270
+ /** A category's name in German (`de*`) or English (everything else). */
271
+ declare function categoryLabel(category: ChangelogCategory, locale?: string): string;
272
+
273
+ export { type BaseClientOptions, CATEGORY_COLORS, CATEGORY_LABELS, type CachedResponse, type ChangelogCategory, type ChangelogClient, type ChangelogClientOptions, type ChangelogLocale, type ChangelogWidgetPosition, DEFAULT_BASE_URL, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type ListEntriesParams, type PublicChangelogEntry, type PublicChangelogEntryPayload, type PublicChangelogPayload, type PublicChangelogProduct, type PublicChangelogProductsPayload, type RequestOptions, type SeenStorage, type SeenTracker, SolutionDeskError, browserStorage, categoryLabel, createChangelogClient, createMemoryEtagCache, createSeenTracker };