@bettercms-ai/sdk 1.34.0 → 1.36.0
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/draft.cjs +32 -0
- package/dist/draft.cjs.map +1 -1
- package/dist/draft.d.cts +15 -1
- package/dist/draft.d.ts +15 -1
- package/dist/draft.js +29 -0
- package/dist/draft.js.map +1 -1
- package/dist/index.cjs +7 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -5
- package/dist/index.d.ts +24 -5
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/{read-ndf2I3D4.d.cts → read-client-tNsCHy0p.d.cts} +1 -37
- package/dist/{read-ndf2I3D4.d.ts → read-client-tNsCHy0p.d.ts} +1 -37
- package/dist/read.d.cts +39 -1
- package/dist/read.d.ts +39 -1
- package/dist/sections.cjs +516 -0
- package/dist/sections.cjs.map +1 -0
- package/dist/sections.d.cts +49 -0
- package/dist/sections.d.ts +49 -0
- package/dist/sections.js +486 -0
- package/dist/sections.js.map +1 -0
- package/package.json +7 -2
|
@@ -354,40 +354,4 @@ interface BetterCMSReadClient {
|
|
|
354
354
|
}
|
|
355
355
|
declare function createClient(options: CreateClientOptions): BetterCMSReadClient;
|
|
356
356
|
|
|
357
|
-
|
|
358
|
-
* Public site search — the headless surface for the visitor-facing search box.
|
|
359
|
-
*
|
|
360
|
-
* Queries `GET /api/v1/delivery/search?project=…&q=…`, the same key-less public
|
|
361
|
-
* endpoint the embeddable widget uses. Powers <BcmsSearch> in @bettercms-ai/next
|
|
362
|
-
* and @bettercms-ai/astro, so the component and any custom UI never drift.
|
|
363
|
-
*
|
|
364
|
-
* `snippet` is HTML-safe: the server escapes the field content and keeps only the
|
|
365
|
-
* <mark> highlight wrappers, so it can be rendered with dangerouslySetInnerHTML.
|
|
366
|
-
*/
|
|
367
|
-
interface SearchHit {
|
|
368
|
-
title: string;
|
|
369
|
-
slug: string;
|
|
370
|
-
type: "page" | "entry";
|
|
371
|
-
/** HTML-safe snippet with <mark> highlights around matched terms. */
|
|
372
|
-
snippet: string;
|
|
373
|
-
/** Site-relative URL, e.g. "/about". */
|
|
374
|
-
url: string;
|
|
375
|
-
}
|
|
376
|
-
interface SearchOptions {
|
|
377
|
-
/** The project id (from the dashboard's Site Search settings). */
|
|
378
|
-
project: string;
|
|
379
|
-
/** The query string. Calls with fewer than 1 char resolve to []. */
|
|
380
|
-
q: string;
|
|
381
|
-
/** Max hits, 1–20 (default 8). */
|
|
382
|
-
limit?: number;
|
|
383
|
-
/** API origin. Defaults to https://api.bettercms.ai. */
|
|
384
|
-
baseUrl?: string;
|
|
385
|
-
/** Injectable fetch for tests / non-browser runtimes. */
|
|
386
|
-
fetchImpl?: typeof fetch;
|
|
387
|
-
/** AbortSignal so callers can cancel a stale in-flight search. */
|
|
388
|
-
signal?: AbortSignal;
|
|
389
|
-
}
|
|
390
|
-
/** Search a published site. Resolves with hits, or [] on an empty query / network error. */
|
|
391
|
-
declare function search(opts: SearchOptions): Promise<SearchHit[]>;
|
|
392
|
-
|
|
393
|
-
export { BetterCMSError as B, type CreateClientOptions as C, type DeliveryFormField as D, ErrorCodes as E, type FormSubmitError as F, type GetEntryOptions as G, type ListEntriesOptions as L, type SearchHit as S, type BetterCMSReadClient as a, type DeliveryForm as b, type DeliveryFormFieldType as c, type FormValue as d, type FormValues as e, type ListPagesOptions as f, type SearchOptions as g, type SubmitFormOptions as h, type SubmitFormResult as i, asStringArray as j, createClient as k, formInitialValues as l, isMultiValueField as m, shouldShowField as n, submitForm as o, search as s, toggleOption as t };
|
|
357
|
+
export { type BetterCMSReadClient as B, type CreateClientOptions as C, type DeliveryFormField as D, ErrorCodes as E, type FormSubmitError as F, type GetEntryOptions as G, type ListEntriesOptions as L, type SubmitFormOptions as S, BetterCMSError as a, type DeliveryForm as b, type DeliveryFormFieldType as c, type FormValue as d, type FormValues as e, type ListPagesOptions as f, type SubmitFormResult as g, asStringArray as h, createClient as i, formInitialValues as j, isMultiValueField as k, submitForm as l, shouldShowField as s, toggleOption as t };
|
|
@@ -354,40 +354,4 @@ interface BetterCMSReadClient {
|
|
|
354
354
|
}
|
|
355
355
|
declare function createClient(options: CreateClientOptions): BetterCMSReadClient;
|
|
356
356
|
|
|
357
|
-
|
|
358
|
-
* Public site search — the headless surface for the visitor-facing search box.
|
|
359
|
-
*
|
|
360
|
-
* Queries `GET /api/v1/delivery/search?project=…&q=…`, the same key-less public
|
|
361
|
-
* endpoint the embeddable widget uses. Powers <BcmsSearch> in @bettercms-ai/next
|
|
362
|
-
* and @bettercms-ai/astro, so the component and any custom UI never drift.
|
|
363
|
-
*
|
|
364
|
-
* `snippet` is HTML-safe: the server escapes the field content and keeps only the
|
|
365
|
-
* <mark> highlight wrappers, so it can be rendered with dangerouslySetInnerHTML.
|
|
366
|
-
*/
|
|
367
|
-
interface SearchHit {
|
|
368
|
-
title: string;
|
|
369
|
-
slug: string;
|
|
370
|
-
type: "page" | "entry";
|
|
371
|
-
/** HTML-safe snippet with <mark> highlights around matched terms. */
|
|
372
|
-
snippet: string;
|
|
373
|
-
/** Site-relative URL, e.g. "/about". */
|
|
374
|
-
url: string;
|
|
375
|
-
}
|
|
376
|
-
interface SearchOptions {
|
|
377
|
-
/** The project id (from the dashboard's Site Search settings). */
|
|
378
|
-
project: string;
|
|
379
|
-
/** The query string. Calls with fewer than 1 char resolve to []. */
|
|
380
|
-
q: string;
|
|
381
|
-
/** Max hits, 1–20 (default 8). */
|
|
382
|
-
limit?: number;
|
|
383
|
-
/** API origin. Defaults to https://api.bettercms.ai. */
|
|
384
|
-
baseUrl?: string;
|
|
385
|
-
/** Injectable fetch for tests / non-browser runtimes. */
|
|
386
|
-
fetchImpl?: typeof fetch;
|
|
387
|
-
/** AbortSignal so callers can cancel a stale in-flight search. */
|
|
388
|
-
signal?: AbortSignal;
|
|
389
|
-
}
|
|
390
|
-
/** Search a published site. Resolves with hits, or [] on an empty query / network error. */
|
|
391
|
-
declare function search(opts: SearchOptions): Promise<SearchHit[]>;
|
|
392
|
-
|
|
393
|
-
export { BetterCMSError as B, type CreateClientOptions as C, type DeliveryFormField as D, ErrorCodes as E, type FormSubmitError as F, type GetEntryOptions as G, type ListEntriesOptions as L, type SearchHit as S, type BetterCMSReadClient as a, type DeliveryForm as b, type DeliveryFormFieldType as c, type FormValue as d, type FormValues as e, type ListPagesOptions as f, type SearchOptions as g, type SubmitFormOptions as h, type SubmitFormResult as i, asStringArray as j, createClient as k, formInitialValues as l, isMultiValueField as m, shouldShowField as n, submitForm as o, search as s, toggleOption as t };
|
|
357
|
+
export { type BetterCMSReadClient as B, type CreateClientOptions as C, type DeliveryFormField as D, ErrorCodes as E, type FormSubmitError as F, type GetEntryOptions as G, type ListEntriesOptions as L, type SubmitFormOptions as S, BetterCMSError as a, type DeliveryForm as b, type DeliveryFormFieldType as c, type FormValue as d, type FormValues as e, type ListPagesOptions as f, type SubmitFormResult as g, asStringArray as h, createClient as i, formInitialValues as j, isMultiValueField as k, submitForm as l, shouldShowField as s, toggleOption as t };
|
package/dist/read.d.cts
CHANGED
|
@@ -1,2 +1,40 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { B as BetterCMSReadClient, C as CreateClientOptions, b as DeliveryForm, D as DeliveryFormField, c as DeliveryFormFieldType, F as FormSubmitError, d as FormValue, e as FormValues, S as SubmitFormOptions, g as SubmitFormResult, h as asStringArray, i as createClient, j as formInitialValues, k as isMultiValueField, s as shouldShowField, l as submitForm, t as toggleOption } from './read-client-tNsCHy0p.cjs';
|
|
2
2
|
import '@bettercms-ai/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Public site search — the headless surface for the visitor-facing search box.
|
|
6
|
+
*
|
|
7
|
+
* Queries `GET /api/v1/delivery/search?project=…&q=…`, the same key-less public
|
|
8
|
+
* endpoint the embeddable widget uses. Powers <BcmsSearch> in @bettercms-ai/next
|
|
9
|
+
* and @bettercms-ai/astro, so the component and any custom UI never drift.
|
|
10
|
+
*
|
|
11
|
+
* `snippet` is HTML-safe: the server escapes the field content and keeps only the
|
|
12
|
+
* <mark> highlight wrappers, so it can be rendered with dangerouslySetInnerHTML.
|
|
13
|
+
*/
|
|
14
|
+
interface SearchHit {
|
|
15
|
+
title: string;
|
|
16
|
+
slug: string;
|
|
17
|
+
type: "page" | "entry";
|
|
18
|
+
/** HTML-safe snippet with <mark> highlights around matched terms. */
|
|
19
|
+
snippet: string;
|
|
20
|
+
/** Site-relative URL, e.g. "/about". */
|
|
21
|
+
url: string;
|
|
22
|
+
}
|
|
23
|
+
interface SearchOptions {
|
|
24
|
+
/** The project id (from the dashboard's Site Search settings). */
|
|
25
|
+
project: string;
|
|
26
|
+
/** The query string. Calls with fewer than 1 char resolve to []. */
|
|
27
|
+
q: string;
|
|
28
|
+
/** Max hits, 1–20 (default 8). */
|
|
29
|
+
limit?: number;
|
|
30
|
+
/** API origin. Defaults to https://api.bettercms.ai. */
|
|
31
|
+
baseUrl?: string;
|
|
32
|
+
/** Injectable fetch for tests / non-browser runtimes. */
|
|
33
|
+
fetchImpl?: typeof fetch;
|
|
34
|
+
/** AbortSignal so callers can cancel a stale in-flight search. */
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
/** Search a published site. Resolves with hits, or [] on an empty query / network error. */
|
|
38
|
+
declare function search(opts: SearchOptions): Promise<SearchHit[]>;
|
|
39
|
+
|
|
40
|
+
export { type SearchHit, type SearchOptions, search };
|
package/dist/read.d.ts
CHANGED
|
@@ -1,2 +1,40 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { B as BetterCMSReadClient, C as CreateClientOptions, b as DeliveryForm, D as DeliveryFormField, c as DeliveryFormFieldType, F as FormSubmitError, d as FormValue, e as FormValues, S as SubmitFormOptions, g as SubmitFormResult, h as asStringArray, i as createClient, j as formInitialValues, k as isMultiValueField, s as shouldShowField, l as submitForm, t as toggleOption } from './read-client-tNsCHy0p.js';
|
|
2
2
|
import '@bettercms-ai/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Public site search — the headless surface for the visitor-facing search box.
|
|
6
|
+
*
|
|
7
|
+
* Queries `GET /api/v1/delivery/search?project=…&q=…`, the same key-less public
|
|
8
|
+
* endpoint the embeddable widget uses. Powers <BcmsSearch> in @bettercms-ai/next
|
|
9
|
+
* and @bettercms-ai/astro, so the component and any custom UI never drift.
|
|
10
|
+
*
|
|
11
|
+
* `snippet` is HTML-safe: the server escapes the field content and keeps only the
|
|
12
|
+
* <mark> highlight wrappers, so it can be rendered with dangerouslySetInnerHTML.
|
|
13
|
+
*/
|
|
14
|
+
interface SearchHit {
|
|
15
|
+
title: string;
|
|
16
|
+
slug: string;
|
|
17
|
+
type: "page" | "entry";
|
|
18
|
+
/** HTML-safe snippet with <mark> highlights around matched terms. */
|
|
19
|
+
snippet: string;
|
|
20
|
+
/** Site-relative URL, e.g. "/about". */
|
|
21
|
+
url: string;
|
|
22
|
+
}
|
|
23
|
+
interface SearchOptions {
|
|
24
|
+
/** The project id (from the dashboard's Site Search settings). */
|
|
25
|
+
project: string;
|
|
26
|
+
/** The query string. Calls with fewer than 1 char resolve to []. */
|
|
27
|
+
q: string;
|
|
28
|
+
/** Max hits, 1–20 (default 8). */
|
|
29
|
+
limit?: number;
|
|
30
|
+
/** API origin. Defaults to https://api.bettercms.ai. */
|
|
31
|
+
baseUrl?: string;
|
|
32
|
+
/** Injectable fetch for tests / non-browser runtimes. */
|
|
33
|
+
fetchImpl?: typeof fetch;
|
|
34
|
+
/** AbortSignal so callers can cancel a stale in-flight search. */
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
/** Search a published site. Resolves with hits, or [] on an empty query / network error. */
|
|
38
|
+
declare function search(opts: SearchOptions): Promise<SearchHit[]>;
|
|
39
|
+
|
|
40
|
+
export { type SearchHit, type SearchOptions, search };
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/sections.ts
|
|
21
|
+
var sections_exports = {};
|
|
22
|
+
__export(sections_exports, {
|
|
23
|
+
createBcmsSite: () => createBcmsSite,
|
|
24
|
+
fetchSections: () => fetchSections,
|
|
25
|
+
runSectionScripts: () => runSectionScripts,
|
|
26
|
+
sectionsMarkup: () => sectionsMarkup
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(sections_exports);
|
|
29
|
+
|
|
30
|
+
// src/errors.ts
|
|
31
|
+
var ErrorCodes = {
|
|
32
|
+
CONTENT_NOT_FOUND: "CONTENT_NOT_FOUND",
|
|
33
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
34
|
+
FORBIDDEN: "FORBIDDEN",
|
|
35
|
+
RATE_LIMITED: "RATE_LIMITED",
|
|
36
|
+
INTERNAL_ERROR: "INTERNAL_ERROR",
|
|
37
|
+
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
38
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
39
|
+
ADMIN_NOT_IMPLEMENTED: "ADMIN_NOT_IMPLEMENTED"
|
|
40
|
+
};
|
|
41
|
+
function statusToCode(status) {
|
|
42
|
+
if (status === 404) return ErrorCodes.CONTENT_NOT_FOUND;
|
|
43
|
+
if (status === 401) return ErrorCodes.UNAUTHORIZED;
|
|
44
|
+
if (status === 403) return ErrorCodes.FORBIDDEN;
|
|
45
|
+
if (status === 429) return ErrorCodes.RATE_LIMITED;
|
|
46
|
+
if (status >= 500) return ErrorCodes.INTERNAL_ERROR;
|
|
47
|
+
if (status === 422) return ErrorCodes.VALIDATION_ERROR;
|
|
48
|
+
return ErrorCodes.INTERNAL_ERROR;
|
|
49
|
+
}
|
|
50
|
+
var BetterCMSError = class _BetterCMSError extends Error {
|
|
51
|
+
status;
|
|
52
|
+
code;
|
|
53
|
+
/**
|
|
54
|
+
* Raw machine-readable `code` from the response body (e.g. "PROJECT_DELETED"),
|
|
55
|
+
* when the API supplies one. Distinct from `code` (which is derived from the
|
|
56
|
+
* HTTP status), this lets callers branch on a specific server condition that
|
|
57
|
+
* shares a status with others (e.g. 409 slug-conflict vs 409 project-deleted).
|
|
58
|
+
*/
|
|
59
|
+
bodyCode;
|
|
60
|
+
/**
|
|
61
|
+
* Per-field validation messages from a 400 `{ errors: { field: message } }` body.
|
|
62
|
+
* The backend returns validation failures in this shape (not `message`/`error`), so
|
|
63
|
+
* without this the SDK collapsed every validation 400 to a bare "Bad Request" with no
|
|
64
|
+
* hint which field or rule failed (FLO-474). Mirrors the forms client's `fieldErrors`.
|
|
65
|
+
*/
|
|
66
|
+
fieldErrors;
|
|
67
|
+
/**
|
|
68
|
+
* The parsed response body, when there was one.
|
|
69
|
+
*
|
|
70
|
+
* Several refusals carry STRUCTURED detail that neither `message` nor `fieldErrors` can hold,
|
|
71
|
+
* and which is the whole point of the refusal: a component publish 409s with the full
|
|
72
|
+
* `readiness` tuple (what evidence is missing, whether a human still has to approve it) and
|
|
73
|
+
* 422s with the `issues` naming the Layouts it would break. Collapsing those to a sentence
|
|
74
|
+
* leaves a caller able to report that something failed and not what to do about it.
|
|
75
|
+
*/
|
|
76
|
+
body;
|
|
77
|
+
constructor(message, status, code, bodyCode, fieldErrors, body) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = "BetterCMSError";
|
|
80
|
+
this.status = status;
|
|
81
|
+
this.code = code;
|
|
82
|
+
this.bodyCode = bodyCode;
|
|
83
|
+
this.fieldErrors = fieldErrors;
|
|
84
|
+
this.body = body;
|
|
85
|
+
if (Error.captureStackTrace) {
|
|
86
|
+
Error.captureStackTrace(this, _BetterCMSError);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
toJSON() {
|
|
90
|
+
return {
|
|
91
|
+
name: this.name,
|
|
92
|
+
message: this.message,
|
|
93
|
+
status: this.status,
|
|
94
|
+
code: this.code,
|
|
95
|
+
bodyCode: this.bodyCode,
|
|
96
|
+
fieldErrors: this.fieldErrors
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Factory — creates a BetterCMSError from a failed fetch Response. Reads the body's
|
|
101
|
+
* `message` (or `error`) for the human message and `code` for a machine-readable
|
|
102
|
+
* condition surfaced as `bodyCode`. Validation failures arrive as `{ errors: {...} }`
|
|
103
|
+
* (no `message`/`error`), so fall back to the first field error for the message and
|
|
104
|
+
* expose the full map as `fieldErrors` (FLO-474).
|
|
105
|
+
*/
|
|
106
|
+
static async from(res) {
|
|
107
|
+
let message = res.statusText || "An error occurred";
|
|
108
|
+
let bodyCode;
|
|
109
|
+
let fieldErrors;
|
|
110
|
+
let code = statusToCode(res.status);
|
|
111
|
+
let raw;
|
|
112
|
+
try {
|
|
113
|
+
const body = await res.json();
|
|
114
|
+
if (body?.errors && typeof body.errors === "object") {
|
|
115
|
+
fieldErrors = body.errors;
|
|
116
|
+
code = ErrorCodes.VALIDATION_ERROR;
|
|
117
|
+
}
|
|
118
|
+
if (body?.message) message = body.message;
|
|
119
|
+
else if (body?.error) message = body.error;
|
|
120
|
+
else if (fieldErrors) {
|
|
121
|
+
const [field, detail] = Object.entries(fieldErrors).find(
|
|
122
|
+
([, v]) => typeof v === "string" && v.length > 0
|
|
123
|
+
) ?? [];
|
|
124
|
+
if (field) message = `${field}: ${detail}`;
|
|
125
|
+
}
|
|
126
|
+
if (body?.code) bodyCode = body.code;
|
|
127
|
+
raw = body;
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
return new _BetterCMSError(message, res.status, code, bodyCode, fieldErrors, raw);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// src/retry.ts
|
|
135
|
+
var RETRY_STATUSES = [429, 503];
|
|
136
|
+
var MAX_RETRIES = 3;
|
|
137
|
+
var MAX_DELAY_MS = 65e3;
|
|
138
|
+
var BASE_DELAY_MS = 1e3;
|
|
139
|
+
var JITTER_MS = 250;
|
|
140
|
+
function isRetryableStatus(status) {
|
|
141
|
+
return RETRY_STATUSES.includes(status);
|
|
142
|
+
}
|
|
143
|
+
function retryAfterMs(res) {
|
|
144
|
+
const raw = res.headers?.get?.("retry-after");
|
|
145
|
+
if (!raw) return null;
|
|
146
|
+
const seconds = Number(raw);
|
|
147
|
+
if (Number.isFinite(seconds)) {
|
|
148
|
+
return Math.min(Math.max(seconds, 0) * 1e3, MAX_DELAY_MS);
|
|
149
|
+
}
|
|
150
|
+
const at = Date.parse(raw);
|
|
151
|
+
if (!Number.isNaN(at)) {
|
|
152
|
+
return Math.min(Math.max(0, at - Date.now()), MAX_DELAY_MS);
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function backoffMs(attempt, res) {
|
|
157
|
+
const fromServer = res ? retryAfterMs(res) : null;
|
|
158
|
+
const base = Math.min(fromServer ?? BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS - JITTER_MS);
|
|
159
|
+
return base + Math.random() * JITTER_MS;
|
|
160
|
+
}
|
|
161
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
162
|
+
|
|
163
|
+
// src/read-client.ts
|
|
164
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
165
|
+
function qs(params) {
|
|
166
|
+
const sp = new URLSearchParams();
|
|
167
|
+
for (const [k, v] of Object.entries(params)) {
|
|
168
|
+
if (v !== void 0 && v !== "") sp.set(k, String(v));
|
|
169
|
+
}
|
|
170
|
+
const s = sp.toString();
|
|
171
|
+
return s ? `?${s}` : "";
|
|
172
|
+
}
|
|
173
|
+
function unwrap(json) {
|
|
174
|
+
if (json && typeof json === "object") {
|
|
175
|
+
const o = json;
|
|
176
|
+
const isPayload = "id" in o || "slug" in o || "items" in o;
|
|
177
|
+
if (!isPayload && "data" in o) return o.data;
|
|
178
|
+
}
|
|
179
|
+
return json;
|
|
180
|
+
}
|
|
181
|
+
function tokenSlug(token) {
|
|
182
|
+
try {
|
|
183
|
+
const part = token.split(".")[1];
|
|
184
|
+
if (!part) return null;
|
|
185
|
+
const json = atob(part.replace(/-/g, "+").replace(/_/g, "/"));
|
|
186
|
+
return JSON.parse(json).entrySlug ?? null;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function createRequestCache() {
|
|
192
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
193
|
+
const settled = /* @__PURE__ */ new Map();
|
|
194
|
+
const copy = (value) => typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
|
|
195
|
+
return async function collapse(url, fetcher) {
|
|
196
|
+
if (settled.has(url)) return copy(settled.get(url));
|
|
197
|
+
const pending = inflight.get(url);
|
|
198
|
+
if (pending) return copy(await pending);
|
|
199
|
+
const run = (async () => {
|
|
200
|
+
try {
|
|
201
|
+
const value = await fetcher();
|
|
202
|
+
settled.set(url, value);
|
|
203
|
+
return value;
|
|
204
|
+
} finally {
|
|
205
|
+
inflight.delete(url);
|
|
206
|
+
}
|
|
207
|
+
})();
|
|
208
|
+
inflight.set(url, run);
|
|
209
|
+
return copy(await run);
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function createClient(options) {
|
|
213
|
+
const apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
214
|
+
const { workspace, apiKey, perspective = "published", previewToken } = options;
|
|
215
|
+
const defaultProjectId = options.projectId;
|
|
216
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
217
|
+
const base = `${apiUrl}/api/v1/delivery/${workspace}`;
|
|
218
|
+
const headers = () => {
|
|
219
|
+
const h = { "Content-Type": "application/json" };
|
|
220
|
+
if (apiKey) h["X-API-Key"] = apiKey;
|
|
221
|
+
return h;
|
|
222
|
+
};
|
|
223
|
+
const collapse = options.cache && perspective === "published" ? createRequestCache() : void 0;
|
|
224
|
+
const fetchJSON = (url) => collapse ? collapse(url, () => request(url)) : request(url);
|
|
225
|
+
async function request(url) {
|
|
226
|
+
let lastError;
|
|
227
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
228
|
+
const controller = new AbortController();
|
|
229
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
230
|
+
try {
|
|
231
|
+
const res = await globalThis.fetch(url, {
|
|
232
|
+
headers: headers(),
|
|
233
|
+
signal: controller.signal
|
|
234
|
+
});
|
|
235
|
+
clearTimeout(timer);
|
|
236
|
+
if (res.ok) return await res.json();
|
|
237
|
+
const retryable = attempt < MAX_RETRIES && isRetryableStatus(res.status);
|
|
238
|
+
if (!retryable) throw await BetterCMSError.from(res);
|
|
239
|
+
await sleep(backoffMs(attempt, res));
|
|
240
|
+
} catch (err) {
|
|
241
|
+
clearTimeout(timer);
|
|
242
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
243
|
+
throw new BetterCMSError("Request timeout", 408, "NETWORK_ERROR");
|
|
244
|
+
}
|
|
245
|
+
if (err instanceof BetterCMSError) throw err;
|
|
246
|
+
lastError = err;
|
|
247
|
+
if (attempt < MAX_RETRIES) {
|
|
248
|
+
await sleep(backoffMs(attempt));
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
throw new BetterCMSError(
|
|
252
|
+
`Network error: ${err instanceof Error ? err.message : String(err)}`,
|
|
253
|
+
0,
|
|
254
|
+
"NETWORK_ERROR"
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
throw lastError ?? new BetterCMSError("Unreachable", 0, "NETWORK_ERROR");
|
|
259
|
+
}
|
|
260
|
+
const draft = perspective === "drafts";
|
|
261
|
+
const stega = options.stega ?? draft;
|
|
262
|
+
return {
|
|
263
|
+
perspective,
|
|
264
|
+
async getEntry(slug, opts) {
|
|
265
|
+
const url = draft && previewToken ? `${apiUrl}/api/v1/preview/${encodeURIComponent(slug)}${qs({
|
|
266
|
+
token: previewToken,
|
|
267
|
+
depth: opts?.depth,
|
|
268
|
+
select: opts?.select?.join(","),
|
|
269
|
+
stega: stega ? "1" : void 0
|
|
270
|
+
})}` : `${base}/content-entries/${encodeURIComponent(slug)}${qs({
|
|
271
|
+
depth: opts?.depth,
|
|
272
|
+
select: opts?.select?.join(","),
|
|
273
|
+
perspective: draft ? "draft" : void 0,
|
|
274
|
+
stega: draft && stega ? "1" : void 0,
|
|
275
|
+
projectId: defaultProjectId
|
|
276
|
+
})}`;
|
|
277
|
+
return unwrap(await fetchJSON(url));
|
|
278
|
+
},
|
|
279
|
+
async listEntries(opts) {
|
|
280
|
+
const url = `${base}/content-entries${qs({
|
|
281
|
+
model: opts?.model,
|
|
282
|
+
page: opts?.page,
|
|
283
|
+
perPage: opts?.perPage,
|
|
284
|
+
depth: opts?.depth,
|
|
285
|
+
select: opts?.select?.join(","),
|
|
286
|
+
perspective: draft ? "draft" : void 0,
|
|
287
|
+
stega: draft && stega ? "1" : void 0,
|
|
288
|
+
projectId: defaultProjectId
|
|
289
|
+
})}`;
|
|
290
|
+
return unwrap(await fetchJSON(url));
|
|
291
|
+
},
|
|
292
|
+
async getPage(slug) {
|
|
293
|
+
if (draft && previewToken && tokenSlug(previewToken) === slug) {
|
|
294
|
+
const url = `${apiUrl}/api/v1/preview/${encodeURIComponent(slug)}${qs({
|
|
295
|
+
token: previewToken,
|
|
296
|
+
stega: stega ? "1" : void 0
|
|
297
|
+
})}`;
|
|
298
|
+
const d = unwrap(await fetchJSON(url));
|
|
299
|
+
return {
|
|
300
|
+
slug: d.slug,
|
|
301
|
+
title: d.title,
|
|
302
|
+
metaTitle: d.metaTitle ?? null,
|
|
303
|
+
metaDescription: d.metaDescription ?? null,
|
|
304
|
+
metaJson: d.metaJson ?? null,
|
|
305
|
+
blocks: d.blockJson ?? [],
|
|
306
|
+
// C3: the DRAFT field styles, present only when the page styles a field.
|
|
307
|
+
...d.fieldStyles ? { fieldStyles: d.fieldStyles } : {},
|
|
308
|
+
updatedAt: d.updatedAt ?? null,
|
|
309
|
+
publishedAt: d.publishedAt ?? null
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
let page = 1;
|
|
313
|
+
for (; ; ) {
|
|
314
|
+
const list = await this.listPages({ page, perPage: 100 });
|
|
315
|
+
const found = list.items.find((p) => p.slug === slug);
|
|
316
|
+
if (found) return found;
|
|
317
|
+
if (!list.hasNextPage) return null;
|
|
318
|
+
page += 1;
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
async listPages(opts) {
|
|
322
|
+
const url = `${base}/pages${qs({
|
|
323
|
+
page: opts?.page,
|
|
324
|
+
perPage: opts?.perPage,
|
|
325
|
+
projectId: defaultProjectId
|
|
326
|
+
})}`;
|
|
327
|
+
return unwrap(await fetchJSON(url));
|
|
328
|
+
},
|
|
329
|
+
async listComponents() {
|
|
330
|
+
const url = `${base}/components${qs({
|
|
331
|
+
perspective: draft ? "draft" : void 0,
|
|
332
|
+
token: draft ? previewToken : void 0,
|
|
333
|
+
projectId: defaultProjectId
|
|
334
|
+
})}`;
|
|
335
|
+
return unwrap(await fetchJSON(url));
|
|
336
|
+
},
|
|
337
|
+
async getLayout() {
|
|
338
|
+
const url = `${base}/layout${qs({ perspective: draft ? "draft" : void 0, projectId: defaultProjectId })}`;
|
|
339
|
+
try {
|
|
340
|
+
return unwrap(await fetchJSON(url));
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (error instanceof BetterCMSError && error.status === 404) return null;
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
async listForms(opts) {
|
|
347
|
+
const url = `${base}/forms${qs({
|
|
348
|
+
projectId: opts?.projectId ?? defaultProjectId
|
|
349
|
+
})}`;
|
|
350
|
+
return unwrap(
|
|
351
|
+
await fetchJSON(url)
|
|
352
|
+
);
|
|
353
|
+
},
|
|
354
|
+
tag(resource, id) {
|
|
355
|
+
return id ? `bcms:${workspace}:${resource}:${id}` : `bcms:${workspace}:${resource}`;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/config.ts
|
|
361
|
+
var DEFAULT_MEDIA_URL = "https://cdn.bettercms.ai";
|
|
362
|
+
function resolveBcmsConfig(input) {
|
|
363
|
+
const env = input.env ?? {};
|
|
364
|
+
const fromEnv = (name) => {
|
|
365
|
+
for (const p of input.prefixes) {
|
|
366
|
+
const v = env[`${p}BCMS_${name}`];
|
|
367
|
+
if (v) return v;
|
|
368
|
+
}
|
|
369
|
+
return void 0;
|
|
370
|
+
};
|
|
371
|
+
let snap;
|
|
372
|
+
const snapshot = () => snap ??= input.snapshot?.() ?? {};
|
|
373
|
+
const o = input.options ?? {};
|
|
374
|
+
const config = {
|
|
375
|
+
apiUrl: (o.apiUrl ?? fromEnv("API_URL") ?? snapshot().apiUrl ?? "").replace(/\/+$/, ""),
|
|
376
|
+
workspace: o.workspace ?? fromEnv("WORKSPACE") ?? snapshot().workspace ?? "",
|
|
377
|
+
projectId: o.projectId ?? fromEnv("PROJECT_ID") ?? snapshot().projectId,
|
|
378
|
+
mediaUrl: (o.mediaUrl ?? fromEnv("MEDIA_URL") ?? DEFAULT_MEDIA_URL).replace(/\/+$/, ""),
|
|
379
|
+
studioUrl: o.studioUrl ?? fromEnv("STUDIO_URL")
|
|
380
|
+
};
|
|
381
|
+
const missing = ["apiUrl", "workspace"].filter((k) => !config[k]);
|
|
382
|
+
return { config, missing };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/draft.ts
|
|
386
|
+
var DRAFT_COOKIE = "bcms-draft";
|
|
387
|
+
var enc = new TextEncoder();
|
|
388
|
+
function base64url(bytes) {
|
|
389
|
+
const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));
|
|
390
|
+
return b.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
391
|
+
}
|
|
392
|
+
async function hmac(value, secret) {
|
|
393
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
394
|
+
return base64url(await crypto.subtle.sign("HMAC", key, enc.encode(value)));
|
|
395
|
+
}
|
|
396
|
+
async function signValue(value, secret) {
|
|
397
|
+
return `${value}.${await hmac(value, secret)}`;
|
|
398
|
+
}
|
|
399
|
+
async function verifyValue(signed, secret) {
|
|
400
|
+
const idx = signed.lastIndexOf(".");
|
|
401
|
+
if (idx < 0) return null;
|
|
402
|
+
const value = signed.slice(0, idx);
|
|
403
|
+
const sig = signed.slice(idx + 1);
|
|
404
|
+
const expected = await hmac(value, secret);
|
|
405
|
+
if (sig.length !== expected.length) return null;
|
|
406
|
+
let diff = 0;
|
|
407
|
+
for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);
|
|
408
|
+
return diff === 0 ? value : null;
|
|
409
|
+
}
|
|
410
|
+
function safeRedirectPath(to) {
|
|
411
|
+
return to.startsWith("/") && !to.startsWith("//") && !to.startsWith("/\\") ? to : "/";
|
|
412
|
+
}
|
|
413
|
+
function decodeTokenSlug(token) {
|
|
414
|
+
try {
|
|
415
|
+
const part = token.split(".")[1];
|
|
416
|
+
if (!part) return null;
|
|
417
|
+
const bytes = Uint8Array.from(atob(part.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
|
418
|
+
const json = JSON.parse(new TextDecoder().decode(bytes));
|
|
419
|
+
return json.entrySlug ?? null;
|
|
420
|
+
} catch {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async function previewTokenValid(apiUrl, token, fetchImpl = fetch) {
|
|
425
|
+
const slug = decodeTokenSlug(token);
|
|
426
|
+
if (!slug) return false;
|
|
427
|
+
const res = await fetchImpl(`${apiUrl.replace(/\/+$/, "")}/api/v1/preview/${encodeURIComponent(slug)}?token=${encodeURIComponent(token)}`);
|
|
428
|
+
await res.body?.cancel().catch(() => {
|
|
429
|
+
});
|
|
430
|
+
return res.ok;
|
|
431
|
+
}
|
|
432
|
+
async function draftTokenFrom(cookieHeader, secret) {
|
|
433
|
+
if (!cookieHeader || !secret) return null;
|
|
434
|
+
const m = new RegExp(`(?:^|;\\s*)${DRAFT_COOKIE}=([^;]+)`).exec(cookieHeader);
|
|
435
|
+
if (!m) return null;
|
|
436
|
+
try {
|
|
437
|
+
return await verifyValue(decodeURIComponent(m[1]), secret);
|
|
438
|
+
} catch {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function draftEnableResponse(request, opts) {
|
|
443
|
+
const url = new URL(request.url);
|
|
444
|
+
const token = url.searchParams.get("token");
|
|
445
|
+
if (!opts.secret) return new Response("BCMS_DRAFT_SECRET is not configured", { status: 500 });
|
|
446
|
+
if (!token) return new Response("Missing token", { status: 400 });
|
|
447
|
+
if (!await previewTokenValid(opts.apiUrl, token, opts.fetchImpl)) return new Response("Invalid or expired token", { status: 401 });
|
|
448
|
+
const cookie = `${DRAFT_COOKIE}=${encodeURIComponent(await signValue(token, opts.secret))}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400${url.protocol === "https:" ? "; Secure" : ""}`;
|
|
449
|
+
return new Response(null, { status: 302, headers: { Location: safeRedirectPath(url.searchParams.get("redirect") ?? "/"), "Set-Cookie": cookie } });
|
|
450
|
+
}
|
|
451
|
+
function draftDisableResponse(request) {
|
|
452
|
+
const url = new URL(request.url);
|
|
453
|
+
return new Response(null, {
|
|
454
|
+
status: 302,
|
|
455
|
+
headers: { Location: safeRedirectPath(url.searchParams.get("redirect") ?? "/"), "Set-Cookie": `${DRAFT_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0` }
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/sections.ts
|
|
460
|
+
async function fetchSections(opts) {
|
|
461
|
+
const doFetch = opts.fetch ?? fetch;
|
|
462
|
+
const url = `${opts.apiUrl.replace(/\/+$/, "")}/api/v1/sites/fragment/${encodeURIComponent(opts.projectId)}/${encodeURIComponent(opts.slug || "home")}`;
|
|
463
|
+
const res = await doFetch(url, { headers: { accept: "application/json" } });
|
|
464
|
+
if (res.status === 404) return null;
|
|
465
|
+
if (!res.ok) throw new BetterCMSError(`Sections for "${opts.slug}" could not be read (${res.status})`, res.status, res.status === 403 ? ErrorCodes.FORBIDDEN : ErrorCodes.INTERNAL_ERROR);
|
|
466
|
+
return (await res.json()).data;
|
|
467
|
+
}
|
|
468
|
+
function sectionsMarkup(fragment) {
|
|
469
|
+
if (!fragment) return "";
|
|
470
|
+
return `<style>${fragment.css}</style>${fragment.html}${fragment.scripts}`;
|
|
471
|
+
}
|
|
472
|
+
function runSectionScripts(root) {
|
|
473
|
+
for (const old of Array.from(root.querySelectorAll("script"))) {
|
|
474
|
+
if (old.hasAttribute("data-bcms-ran")) continue;
|
|
475
|
+
const fresh = document.createElement("script");
|
|
476
|
+
for (const attr of Array.from(old.attributes)) fresh.setAttribute(attr.name, attr.value);
|
|
477
|
+
fresh.setAttribute("data-bcms-ran", "");
|
|
478
|
+
fresh.textContent = old.textContent;
|
|
479
|
+
old.replaceWith(fresh);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function createBcmsSite(options) {
|
|
483
|
+
const env = options.env ?? {};
|
|
484
|
+
const { config, missing } = resolveBcmsConfig({ options, env, prefixes: [...options.prefixes, ""] });
|
|
485
|
+
if (missing.length > 0) {
|
|
486
|
+
const names = missing.map((m) => `${options.prefixes[0]}BCMS_${m === "apiUrl" ? "API_URL" : "WORKSPACE"}`).join(" and ");
|
|
487
|
+
throw new Error(`[${options.adapter}] Missing ${names}: set them, or pass them to createBcms().`);
|
|
488
|
+
}
|
|
489
|
+
const secret = options.draftSecret ?? env.BCMS_DRAFT_SECRET;
|
|
490
|
+
return {
|
|
491
|
+
config,
|
|
492
|
+
async client(request) {
|
|
493
|
+
const token = request ? await draftTokenFrom(request.headers.get("cookie"), secret) : null;
|
|
494
|
+
return createClient({
|
|
495
|
+
apiUrl: config.apiUrl,
|
|
496
|
+
workspace: config.workspace,
|
|
497
|
+
projectId: config.projectId,
|
|
498
|
+
...token ? { perspective: "drafts", previewToken: token } : {}
|
|
499
|
+
});
|
|
500
|
+
},
|
|
501
|
+
sections(slug, fetchImpl) {
|
|
502
|
+
if (!config.projectId) throw new Error(`[${options.adapter}] Sections need the project: set ${options.prefixes[0]}BCMS_PROJECT_ID.`);
|
|
503
|
+
return fetchSections({ apiUrl: config.apiUrl, projectId: config.projectId, slug, fetch: fetchImpl });
|
|
504
|
+
},
|
|
505
|
+
draftEnable: (request) => draftEnableResponse(request, { apiUrl: config.apiUrl, secret }),
|
|
506
|
+
draftDisable: draftDisableResponse
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
510
|
+
0 && (module.exports = {
|
|
511
|
+
createBcmsSite,
|
|
512
|
+
fetchSections,
|
|
513
|
+
runSectionScripts,
|
|
514
|
+
sectionsMarkup
|
|
515
|
+
});
|
|
516
|
+
//# sourceMappingURL=sections.cjs.map
|