@charleslemaux/cms 1.0.0-rc.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.
@@ -0,0 +1,192 @@
1
+ /** The payloads of the ICRC - CMS public API v1 (spec 7.3). JSON keys are camelCase. */
2
+ interface Language {
3
+ /** API language code, e.g. "fr". */
4
+ code: string;
5
+ /** The name a visitor reads, e.g. "Français". */
6
+ name: string;
7
+ }
8
+ interface CategoryCount {
9
+ slug: string;
10
+ name: string;
11
+ /** Live articles of the zone in this category. */
12
+ count: number;
13
+ }
14
+ interface CaptchaSettings {
15
+ provider: 'turnstile';
16
+ siteKey: string;
17
+ }
18
+ interface NewsletterSettings {
19
+ enabled: boolean;
20
+ /** Null when the zone has no captcha: signups are then protected by rate limits only. */
21
+ captcha: CaptchaSettings | null;
22
+ }
23
+ interface Zone {
24
+ code: string;
25
+ name: string;
26
+ siteUrl: string;
27
+ /** The default language first. */
28
+ languages: Language[];
29
+ defaultLanguage: string;
30
+ newsletter: NewsletterSettings;
31
+ categories: CategoryCount[];
32
+ }
33
+ interface ImageSource {
34
+ width: number;
35
+ src: string;
36
+ }
37
+ interface Cover {
38
+ /** The largest version (1920 pixels wide at most). */
39
+ src: string;
40
+ width: number;
41
+ height: number;
42
+ alt: string;
43
+ caption: string;
44
+ /** Every version, smallest first: ready for srcset. */
45
+ sources: ImageSource[];
46
+ }
47
+ interface Category {
48
+ slug: string;
49
+ name: string;
50
+ }
51
+ interface Tag {
52
+ slug: string;
53
+ name: string;
54
+ }
55
+ interface Author {
56
+ name: string;
57
+ role: string;
58
+ }
59
+ interface ArticleSummary {
60
+ slug: string;
61
+ /** The article on this zone's website, in the served language. */
62
+ url: string;
63
+ /** The article on its primary website, in the served language. */
64
+ canonicalUrl: string;
65
+ /** The language served: the source language when `fallback` is true. */
66
+ lang: string;
67
+ sourceLang: string;
68
+ /** True when the article has no version in the asked language. */
69
+ fallback: boolean;
70
+ availableLangs: string[];
71
+ title: string;
72
+ subtitle: string;
73
+ excerpt: string;
74
+ cover: Cover | null;
75
+ category: Category | null;
76
+ tags: Tag[];
77
+ author: Author;
78
+ /** When the article went live on this zone (ISO 8601, UTC). */
79
+ publishedAt: string;
80
+ /** The latest change of the article or of the served translation (ISO 8601, UTC). */
81
+ updatedAt: string;
82
+ readingTimeMinutes: number;
83
+ }
84
+ interface Alternate {
85
+ lang: string;
86
+ url: string;
87
+ }
88
+ interface Article extends ArticleSummary {
89
+ /** Cleaned by Odoo with a strict allowlist (spec 7.6). */
90
+ bodyHtml: string;
91
+ /** The real translations, when the zone's article URL pattern has {lang}; else empty. */
92
+ alternates: Alternate[];
93
+ /** Up to 3 articles of the same zone. */
94
+ related: ArticleSummary[];
95
+ }
96
+ interface Page<T> {
97
+ items: T[];
98
+ page: number;
99
+ limit: number;
100
+ total: number;
101
+ }
102
+ interface SubscribeResult {
103
+ status: 'pending';
104
+ }
105
+ interface ConfirmResult {
106
+ status: 'confirmed';
107
+ zone: string;
108
+ lang: string;
109
+ }
110
+ interface UnsubscribeResult {
111
+ status: 'unsubscribed';
112
+ zone: string;
113
+ }
114
+
115
+ interface CmsClientOptions {
116
+ /** Address of the Odoo that serves the CMS, e.g. "https://b.projekts.com". */
117
+ baseUrl: string;
118
+ /** The zone (website) code, e.g. "projekts". */
119
+ zone: string;
120
+ /** A fetch implementation. Default: the global `fetch`, looked up at each call. */
121
+ fetch?: typeof fetch;
122
+ /** Added to the read calls, e.g. `{ next: { revalidate: 60 } }` in Next.js. Never to the newsletter calls. */
123
+ fetchOptions?: RequestInit & Record<string, unknown>;
124
+ }
125
+ interface ReadOptions {
126
+ /** "fr", "fr-FR" or "fr_FR". Default: the zone's default language. */
127
+ lang?: string;
128
+ /** Cancels the request. */
129
+ signal?: AbortSignal;
130
+ }
131
+ interface ListArticlesParams extends ReadOptions {
132
+ /** A category slug. */
133
+ category?: string;
134
+ /** A tag slug. */
135
+ tag?: string;
136
+ /** From 1. */
137
+ page?: number;
138
+ /** From 1 to 50. Default: 12. */
139
+ limit?: number;
140
+ }
141
+ interface SubscribeParams {
142
+ email: string;
143
+ /** Language of the newsletter the visitor joins. Default: the zone's default language. */
144
+ lang?: string;
145
+ /** The visitor ticked the consent box. */
146
+ consent: true;
147
+ /** The Cloudflare Turnstile token, when the zone has a captcha. */
148
+ captchaToken?: string;
149
+ /** The honeypot: people leave it empty. */
150
+ website?: string;
151
+ }
152
+ interface CmsClient {
153
+ readonly baseUrl: string;
154
+ readonly zone: string;
155
+ getZone(options?: ReadOptions): Promise<Zone>;
156
+ listArticles(params?: ListArticlesParams): Promise<Page<ArticleSummary>>;
157
+ /** Null when the article is not live on this zone. */
158
+ getArticle(slug: string, options?: ReadOptions): Promise<Article | null>;
159
+ /** Call it from the visitor's browser: Odoo checks the page's origin and limits signups per visitor. */
160
+ subscribe(params: SubscribeParams): Promise<SubscribeResult>;
161
+ confirm(token: string): Promise<ConfirmResult>;
162
+ unsubscribe(token: string): Promise<UnsubscribeResult>;
163
+ }
164
+ declare function createCmsClient(options: CmsClientOptions): CmsClient;
165
+
166
+ /** What went wrong: the API's codes (spec 12), and three the client adds. */
167
+ type CmsErrorCode = 'not_found' | 'invalid_request' | 'invalid_email' | 'consent_required' | 'origin_not_allowed' | 'captcha_failed' | 'captcha_unavailable' | 'invalid_token' | 'payload_too_large' | 'unsupported_media_type' | 'rate_limited' | 'network_error' | 'invalid_response' | 'http_error';
168
+ declare class CmsError extends Error {
169
+ /** The HTTP status; 0 when the CMS could not be reached. */
170
+ readonly status: number;
171
+ readonly code: CmsErrorCode | (string & {});
172
+ /** Seconds to wait before trying again, on a 429. */
173
+ readonly retryAfter?: number;
174
+ constructor(status: number, code: CmsErrorCode | (string & {}), message: string, retryAfter?: number);
175
+ }
176
+ /** True for a CmsError, including one thrown by another copy of this package. */
177
+ declare function isCmsError(value: unknown): value is CmsError;
178
+
179
+ /** Where a newsletter page reads its link: a URL, a path, a query string, URLSearchParams or Next.js searchParams. */
180
+ type NewsletterSource = string | URL | URLSearchParams | Record<string, string | string[] | undefined>;
181
+ interface NewsletterAction {
182
+ action: 'confirm' | 'unsubscribe';
183
+ token: string;
184
+ }
185
+ /**
186
+ * The action a newsletter link asks for: `?confirm=<token>` or `?unsubscribe=<token>`.
187
+ * Null when there is none, when the token is malformed, or when the link is ambiguous
188
+ * (both parameters, or one given twice).
189
+ */
190
+ declare function readNewsletterAction(source: NewsletterSource | null | undefined): NewsletterAction | null;
191
+
192
+ export { type Alternate, type Article, type ArticleSummary, type Author, type CaptchaSettings, type Category, type CategoryCount, type CmsClient, type CmsClientOptions, CmsError, type CmsErrorCode, type ConfirmResult, type Cover, type ImageSource, type Language, type ListArticlesParams, type NewsletterAction, type NewsletterSettings, type NewsletterSource, type Page, type ReadOptions, type SubscribeParams, type SubscribeResult, type Tag, type UnsubscribeResult, type Zone, createCmsClient, isCmsError, readNewsletterAction };
@@ -0,0 +1,192 @@
1
+ /** The payloads of the ICRC - CMS public API v1 (spec 7.3). JSON keys are camelCase. */
2
+ interface Language {
3
+ /** API language code, e.g. "fr". */
4
+ code: string;
5
+ /** The name a visitor reads, e.g. "Français". */
6
+ name: string;
7
+ }
8
+ interface CategoryCount {
9
+ slug: string;
10
+ name: string;
11
+ /** Live articles of the zone in this category. */
12
+ count: number;
13
+ }
14
+ interface CaptchaSettings {
15
+ provider: 'turnstile';
16
+ siteKey: string;
17
+ }
18
+ interface NewsletterSettings {
19
+ enabled: boolean;
20
+ /** Null when the zone has no captcha: signups are then protected by rate limits only. */
21
+ captcha: CaptchaSettings | null;
22
+ }
23
+ interface Zone {
24
+ code: string;
25
+ name: string;
26
+ siteUrl: string;
27
+ /** The default language first. */
28
+ languages: Language[];
29
+ defaultLanguage: string;
30
+ newsletter: NewsletterSettings;
31
+ categories: CategoryCount[];
32
+ }
33
+ interface ImageSource {
34
+ width: number;
35
+ src: string;
36
+ }
37
+ interface Cover {
38
+ /** The largest version (1920 pixels wide at most). */
39
+ src: string;
40
+ width: number;
41
+ height: number;
42
+ alt: string;
43
+ caption: string;
44
+ /** Every version, smallest first: ready for srcset. */
45
+ sources: ImageSource[];
46
+ }
47
+ interface Category {
48
+ slug: string;
49
+ name: string;
50
+ }
51
+ interface Tag {
52
+ slug: string;
53
+ name: string;
54
+ }
55
+ interface Author {
56
+ name: string;
57
+ role: string;
58
+ }
59
+ interface ArticleSummary {
60
+ slug: string;
61
+ /** The article on this zone's website, in the served language. */
62
+ url: string;
63
+ /** The article on its primary website, in the served language. */
64
+ canonicalUrl: string;
65
+ /** The language served: the source language when `fallback` is true. */
66
+ lang: string;
67
+ sourceLang: string;
68
+ /** True when the article has no version in the asked language. */
69
+ fallback: boolean;
70
+ availableLangs: string[];
71
+ title: string;
72
+ subtitle: string;
73
+ excerpt: string;
74
+ cover: Cover | null;
75
+ category: Category | null;
76
+ tags: Tag[];
77
+ author: Author;
78
+ /** When the article went live on this zone (ISO 8601, UTC). */
79
+ publishedAt: string;
80
+ /** The latest change of the article or of the served translation (ISO 8601, UTC). */
81
+ updatedAt: string;
82
+ readingTimeMinutes: number;
83
+ }
84
+ interface Alternate {
85
+ lang: string;
86
+ url: string;
87
+ }
88
+ interface Article extends ArticleSummary {
89
+ /** Cleaned by Odoo with a strict allowlist (spec 7.6). */
90
+ bodyHtml: string;
91
+ /** The real translations, when the zone's article URL pattern has {lang}; else empty. */
92
+ alternates: Alternate[];
93
+ /** Up to 3 articles of the same zone. */
94
+ related: ArticleSummary[];
95
+ }
96
+ interface Page<T> {
97
+ items: T[];
98
+ page: number;
99
+ limit: number;
100
+ total: number;
101
+ }
102
+ interface SubscribeResult {
103
+ status: 'pending';
104
+ }
105
+ interface ConfirmResult {
106
+ status: 'confirmed';
107
+ zone: string;
108
+ lang: string;
109
+ }
110
+ interface UnsubscribeResult {
111
+ status: 'unsubscribed';
112
+ zone: string;
113
+ }
114
+
115
+ interface CmsClientOptions {
116
+ /** Address of the Odoo that serves the CMS, e.g. "https://b.projekts.com". */
117
+ baseUrl: string;
118
+ /** The zone (website) code, e.g. "projekts". */
119
+ zone: string;
120
+ /** A fetch implementation. Default: the global `fetch`, looked up at each call. */
121
+ fetch?: typeof fetch;
122
+ /** Added to the read calls, e.g. `{ next: { revalidate: 60 } }` in Next.js. Never to the newsletter calls. */
123
+ fetchOptions?: RequestInit & Record<string, unknown>;
124
+ }
125
+ interface ReadOptions {
126
+ /** "fr", "fr-FR" or "fr_FR". Default: the zone's default language. */
127
+ lang?: string;
128
+ /** Cancels the request. */
129
+ signal?: AbortSignal;
130
+ }
131
+ interface ListArticlesParams extends ReadOptions {
132
+ /** A category slug. */
133
+ category?: string;
134
+ /** A tag slug. */
135
+ tag?: string;
136
+ /** From 1. */
137
+ page?: number;
138
+ /** From 1 to 50. Default: 12. */
139
+ limit?: number;
140
+ }
141
+ interface SubscribeParams {
142
+ email: string;
143
+ /** Language of the newsletter the visitor joins. Default: the zone's default language. */
144
+ lang?: string;
145
+ /** The visitor ticked the consent box. */
146
+ consent: true;
147
+ /** The Cloudflare Turnstile token, when the zone has a captcha. */
148
+ captchaToken?: string;
149
+ /** The honeypot: people leave it empty. */
150
+ website?: string;
151
+ }
152
+ interface CmsClient {
153
+ readonly baseUrl: string;
154
+ readonly zone: string;
155
+ getZone(options?: ReadOptions): Promise<Zone>;
156
+ listArticles(params?: ListArticlesParams): Promise<Page<ArticleSummary>>;
157
+ /** Null when the article is not live on this zone. */
158
+ getArticle(slug: string, options?: ReadOptions): Promise<Article | null>;
159
+ /** Call it from the visitor's browser: Odoo checks the page's origin and limits signups per visitor. */
160
+ subscribe(params: SubscribeParams): Promise<SubscribeResult>;
161
+ confirm(token: string): Promise<ConfirmResult>;
162
+ unsubscribe(token: string): Promise<UnsubscribeResult>;
163
+ }
164
+ declare function createCmsClient(options: CmsClientOptions): CmsClient;
165
+
166
+ /** What went wrong: the API's codes (spec 12), and three the client adds. */
167
+ type CmsErrorCode = 'not_found' | 'invalid_request' | 'invalid_email' | 'consent_required' | 'origin_not_allowed' | 'captcha_failed' | 'captcha_unavailable' | 'invalid_token' | 'payload_too_large' | 'unsupported_media_type' | 'rate_limited' | 'network_error' | 'invalid_response' | 'http_error';
168
+ declare class CmsError extends Error {
169
+ /** The HTTP status; 0 when the CMS could not be reached. */
170
+ readonly status: number;
171
+ readonly code: CmsErrorCode | (string & {});
172
+ /** Seconds to wait before trying again, on a 429. */
173
+ readonly retryAfter?: number;
174
+ constructor(status: number, code: CmsErrorCode | (string & {}), message: string, retryAfter?: number);
175
+ }
176
+ /** True for a CmsError, including one thrown by another copy of this package. */
177
+ declare function isCmsError(value: unknown): value is CmsError;
178
+
179
+ /** Where a newsletter page reads its link: a URL, a path, a query string, URLSearchParams or Next.js searchParams. */
180
+ type NewsletterSource = string | URL | URLSearchParams | Record<string, string | string[] | undefined>;
181
+ interface NewsletterAction {
182
+ action: 'confirm' | 'unsubscribe';
183
+ token: string;
184
+ }
185
+ /**
186
+ * The action a newsletter link asks for: `?confirm=<token>` or `?unsubscribe=<token>`.
187
+ * Null when there is none, when the token is malformed, or when the link is ambiguous
188
+ * (both parameters, or one given twice).
189
+ */
190
+ declare function readNewsletterAction(source: NewsletterSource | null | undefined): NewsletterAction | null;
191
+
192
+ export { type Alternate, type Article, type ArticleSummary, type Author, type CaptchaSettings, type Category, type CategoryCount, type CmsClient, type CmsClientOptions, CmsError, type CmsErrorCode, type ConfirmResult, type Cover, type ImageSource, type Language, type ListArticlesParams, type NewsletterAction, type NewsletterSettings, type NewsletterSource, type Page, type ReadOptions, type SubscribeParams, type SubscribeResult, type Tag, type UnsubscribeResult, type Zone, createCmsClient, isCmsError, readNewsletterAction };
package/dist/index.js ADDED
@@ -0,0 +1,192 @@
1
+ // src/errors.ts
2
+ var CmsError = class extends Error {
3
+ constructor(status, code, message, retryAfter) {
4
+ super(message);
5
+ this.name = "CmsError";
6
+ this.status = status;
7
+ this.code = code;
8
+ if (retryAfter !== void 0) this.retryAfter = retryAfter;
9
+ }
10
+ };
11
+ function isCmsError(value) {
12
+ return value instanceof Error && value.name === "CmsError" && typeof value.status === "number" && typeof value.code === "string";
13
+ }
14
+
15
+ // src/client.ts
16
+ var API_PATH = "/cms/api/v1";
17
+ function createCmsClient(options) {
18
+ const baseUrl = normalizeBaseUrl(options.baseUrl);
19
+ const zone = typeof options.zone === "string" ? options.zone.trim() : "";
20
+ if (!zone) {
21
+ throw new TypeError('createCmsClient: `zone` is required, e.g. "projekts".');
22
+ }
23
+ const zonePath = `${API_PATH}/zones/${encodeURIComponent(zone)}`;
24
+ async function send(path, init, query) {
25
+ const fetchImpl = options.fetch ?? globalThis.fetch;
26
+ if (typeof fetchImpl !== "function") {
27
+ throw new TypeError("createCmsClient: no fetch available here. Pass `fetch` in the options.");
28
+ }
29
+ let response;
30
+ try {
31
+ response = await fetchImpl(buildUrl(baseUrl, path, query), init);
32
+ } catch (error) {
33
+ if (isAbortError(error) || init.signal?.aborted) throw error;
34
+ throw new CmsError(0, "network_error", `Could not reach ${baseUrl}: ${describeError(error)}`);
35
+ }
36
+ if (!response.ok) {
37
+ throw await errorFromResponse(response, init.signal);
38
+ }
39
+ try {
40
+ return await response.json();
41
+ } catch (error) {
42
+ if (isAbortError(error) || init.signal?.aborted) throw error;
43
+ throw new CmsError(response.status, "invalid_response", "The CMS did not answer with JSON.");
44
+ }
45
+ }
46
+ function read(path, query, signal) {
47
+ const extra = options.fetchOptions ?? {};
48
+ const headers = new Headers(extra.headers);
49
+ headers.set("Accept", "application/json");
50
+ return send(path, { ...extra, method: "GET", headers, signal: signal ?? extra.signal }, query);
51
+ }
52
+ function post(path, body) {
53
+ return send(path, {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
56
+ body: JSON.stringify(body),
57
+ referrerPolicy: "no-referrer"
58
+ });
59
+ }
60
+ return {
61
+ baseUrl,
62
+ zone,
63
+ getZone: (readOptions = {}) => read(zonePath, { lang: readOptions.lang }, readOptions.signal),
64
+ listArticles: (params = {}) => read(
65
+ `${zonePath}/articles`,
66
+ { lang: params.lang, category: params.category, tag: params.tag, page: params.page, limit: params.limit },
67
+ params.signal
68
+ ),
69
+ async getArticle(slug, readOptions = {}) {
70
+ if (typeof slug !== "string" || !slug) {
71
+ throw new TypeError("getArticle: `slug` is required.");
72
+ }
73
+ try {
74
+ return await read(
75
+ `${zonePath}/articles/${encodeURIComponent(slug)}`,
76
+ { lang: readOptions.lang },
77
+ readOptions.signal
78
+ );
79
+ } catch (error) {
80
+ if (isCmsError(error) && error.status === 404) return null;
81
+ throw error;
82
+ }
83
+ },
84
+ subscribe: (params) => post(`${zonePath}/newsletter/subscribe`, {
85
+ email: params.email,
86
+ lang: params.lang,
87
+ consent: params.consent,
88
+ captchaToken: params.captchaToken,
89
+ website: params.website ?? ""
90
+ }),
91
+ confirm: (token) => post(`${API_PATH}/newsletter/confirm`, { token }),
92
+ unsubscribe: (token) => post(`${API_PATH}/newsletter/unsubscribe`, { token })
93
+ };
94
+ }
95
+ function normalizeBaseUrl(value) {
96
+ const text = typeof value === "string" ? value.trim() : "";
97
+ let url;
98
+ try {
99
+ url = new URL(text);
100
+ } catch {
101
+ url = void 0;
102
+ }
103
+ if (!url || url.protocol !== "https:" && url.protocol !== "http:") {
104
+ throw new TypeError(
105
+ `createCmsClient: \`baseUrl\` must be an absolute http(s) address such as "https://b.projekts.com", not "${text}".`
106
+ );
107
+ }
108
+ return `${url.origin}${url.pathname}`.replace(/\/+$/, "");
109
+ }
110
+ function buildUrl(baseUrl, path, query = {}) {
111
+ const search = new URLSearchParams();
112
+ for (const [key, value] of Object.entries(query)) {
113
+ if (value !== void 0 && value !== "") search.set(key, String(value));
114
+ }
115
+ const text = search.toString();
116
+ return `${baseUrl}${path}${text ? `?${text}` : ""}`;
117
+ }
118
+ async function errorFromResponse(response, signal) {
119
+ const retryAfter = parseRetryAfter(response.headers.get("Retry-After"));
120
+ let code = "http_error";
121
+ let message = `The CMS answered with the status ${response.status}.`;
122
+ try {
123
+ const data = await response.json();
124
+ const error = isRecord(data) && isRecord(data.error) ? data.error : void 0;
125
+ if (error && typeof error.code === "string") {
126
+ code = error.code;
127
+ if (typeof error.message === "string") message = error.message;
128
+ }
129
+ } catch (error) {
130
+ if (isAbortError(error) || signal?.aborted) throw error;
131
+ }
132
+ return new CmsError(response.status, code, message, retryAfter);
133
+ }
134
+ function parseRetryAfter(value) {
135
+ if (!value) return void 0;
136
+ const seconds = Number(value);
137
+ if (Number.isFinite(seconds)) return Math.max(0, Math.ceil(seconds));
138
+ const date = Date.parse(value);
139
+ return Number.isNaN(date) ? void 0 : Math.max(0, Math.ceil((date - Date.now()) / 1e3));
140
+ }
141
+ function isRecord(value) {
142
+ return typeof value === "object" && value !== null;
143
+ }
144
+ function isAbortError(error) {
145
+ return isRecord(error) && error.name === "AbortError";
146
+ }
147
+ function describeError(error) {
148
+ return error instanceof Error ? error.message : String(error);
149
+ }
150
+
151
+ // src/newsletter-action.ts
152
+ var ACTIONS = ["confirm", "unsubscribe"];
153
+ var TOKEN_RE = /^[A-Za-z0-9_-]{16,512}$/;
154
+ function readNewsletterAction(source) {
155
+ if (source == null) return null;
156
+ const params = toSearchParams(source);
157
+ let found = null;
158
+ for (const action of ACTIONS) {
159
+ const values = params.getAll(action);
160
+ if (values.length === 0) continue;
161
+ if (values.length > 1 || found) return null;
162
+ found = { action, token: values[0] ?? "" };
163
+ }
164
+ return found && TOKEN_RE.test(found.token) ? found : null;
165
+ }
166
+ function toSearchParams(source) {
167
+ if (typeof source === "string") {
168
+ const query = source.includes("?") ? source.slice(source.indexOf("?") + 1) : source;
169
+ return new URLSearchParams(query.split("#")[0]);
170
+ }
171
+ if (isSearchParams(source)) return source;
172
+ if (isUrl(source)) return source.searchParams;
173
+ const params = new URLSearchParams();
174
+ for (const [key, value] of Object.entries(source)) {
175
+ const values = Array.isArray(value) ? value : value === void 0 ? [] : [value];
176
+ for (const item of values) params.append(key, item);
177
+ }
178
+ return params;
179
+ }
180
+ function isSearchParams(value) {
181
+ return typeof value.getAll === "function" && typeof value.append === "function";
182
+ }
183
+ function isUrl(value) {
184
+ const url = value;
185
+ return typeof url.href === "string" && typeof url.searchParams === "object" && isSearchParams(url.searchParams);
186
+ }
187
+ export {
188
+ CmsError,
189
+ createCmsClient,
190
+ isCmsError,
191
+ readNewsletterAction
192
+ };