@bettercms-ai/sdk 1.32.0 → 1.34.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/read.cjs ADDED
@@ -0,0 +1,457 @@
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/read.ts
21
+ var read_exports = {};
22
+ __export(read_exports, {
23
+ asStringArray: () => asStringArray,
24
+ createClient: () => createClient,
25
+ formInitialValues: () => formInitialValues,
26
+ isMultiValueField: () => isMultiValueField,
27
+ search: () => search,
28
+ shouldShowField: () => shouldShowField,
29
+ submitForm: () => submitForm,
30
+ toggleOption: () => toggleOption
31
+ });
32
+ module.exports = __toCommonJS(read_exports);
33
+
34
+ // src/errors.ts
35
+ var ErrorCodes = {
36
+ CONTENT_NOT_FOUND: "CONTENT_NOT_FOUND",
37
+ UNAUTHORIZED: "UNAUTHORIZED",
38
+ FORBIDDEN: "FORBIDDEN",
39
+ RATE_LIMITED: "RATE_LIMITED",
40
+ INTERNAL_ERROR: "INTERNAL_ERROR",
41
+ VALIDATION_ERROR: "VALIDATION_ERROR",
42
+ NETWORK_ERROR: "NETWORK_ERROR",
43
+ ADMIN_NOT_IMPLEMENTED: "ADMIN_NOT_IMPLEMENTED"
44
+ };
45
+ function statusToCode(status) {
46
+ if (status === 404) return ErrorCodes.CONTENT_NOT_FOUND;
47
+ if (status === 401) return ErrorCodes.UNAUTHORIZED;
48
+ if (status === 403) return ErrorCodes.FORBIDDEN;
49
+ if (status === 429) return ErrorCodes.RATE_LIMITED;
50
+ if (status >= 500) return ErrorCodes.INTERNAL_ERROR;
51
+ if (status === 422) return ErrorCodes.VALIDATION_ERROR;
52
+ return ErrorCodes.INTERNAL_ERROR;
53
+ }
54
+ var BetterCMSError = class _BetterCMSError extends Error {
55
+ status;
56
+ code;
57
+ /**
58
+ * Raw machine-readable `code` from the response body (e.g. "PROJECT_DELETED"),
59
+ * when the API supplies one. Distinct from `code` (which is derived from the
60
+ * HTTP status), this lets callers branch on a specific server condition that
61
+ * shares a status with others (e.g. 409 slug-conflict vs 409 project-deleted).
62
+ */
63
+ bodyCode;
64
+ /**
65
+ * Per-field validation messages from a 400 `{ errors: { field: message } }` body.
66
+ * The backend returns validation failures in this shape (not `message`/`error`), so
67
+ * without this the SDK collapsed every validation 400 to a bare "Bad Request" with no
68
+ * hint which field or rule failed (FLO-474). Mirrors the forms client's `fieldErrors`.
69
+ */
70
+ fieldErrors;
71
+ /**
72
+ * The parsed response body, when there was one.
73
+ *
74
+ * Several refusals carry STRUCTURED detail that neither `message` nor `fieldErrors` can hold,
75
+ * and which is the whole point of the refusal: a component publish 409s with the full
76
+ * `readiness` tuple (what evidence is missing, whether a human still has to approve it) and
77
+ * 422s with the `issues` naming the Layouts it would break. Collapsing those to a sentence
78
+ * leaves a caller able to report that something failed and not what to do about it.
79
+ */
80
+ body;
81
+ constructor(message, status, code, bodyCode, fieldErrors, body) {
82
+ super(message);
83
+ this.name = "BetterCMSError";
84
+ this.status = status;
85
+ this.code = code;
86
+ this.bodyCode = bodyCode;
87
+ this.fieldErrors = fieldErrors;
88
+ this.body = body;
89
+ if (Error.captureStackTrace) {
90
+ Error.captureStackTrace(this, _BetterCMSError);
91
+ }
92
+ }
93
+ toJSON() {
94
+ return {
95
+ name: this.name,
96
+ message: this.message,
97
+ status: this.status,
98
+ code: this.code,
99
+ bodyCode: this.bodyCode,
100
+ fieldErrors: this.fieldErrors
101
+ };
102
+ }
103
+ /**
104
+ * Factory — creates a BetterCMSError from a failed fetch Response. Reads the body's
105
+ * `message` (or `error`) for the human message and `code` for a machine-readable
106
+ * condition surfaced as `bodyCode`. Validation failures arrive as `{ errors: {...} }`
107
+ * (no `message`/`error`), so fall back to the first field error for the message and
108
+ * expose the full map as `fieldErrors` (FLO-474).
109
+ */
110
+ static async from(res) {
111
+ let message = res.statusText || "An error occurred";
112
+ let bodyCode;
113
+ let fieldErrors;
114
+ let code = statusToCode(res.status);
115
+ let raw;
116
+ try {
117
+ const body = await res.json();
118
+ if (body?.errors && typeof body.errors === "object") {
119
+ fieldErrors = body.errors;
120
+ code = ErrorCodes.VALIDATION_ERROR;
121
+ }
122
+ if (body?.message) message = body.message;
123
+ else if (body?.error) message = body.error;
124
+ else if (fieldErrors) {
125
+ const [field, detail] = Object.entries(fieldErrors).find(
126
+ ([, v]) => typeof v === "string" && v.length > 0
127
+ ) ?? [];
128
+ if (field) message = `${field}: ${detail}`;
129
+ }
130
+ if (body?.code) bodyCode = body.code;
131
+ raw = body;
132
+ } catch {
133
+ }
134
+ return new _BetterCMSError(message, res.status, code, bodyCode, fieldErrors, raw);
135
+ }
136
+ };
137
+
138
+ // src/retry.ts
139
+ var RETRY_STATUSES = [429, 503];
140
+ var MAX_RETRIES = 3;
141
+ var MAX_DELAY_MS = 65e3;
142
+ var BASE_DELAY_MS = 1e3;
143
+ var JITTER_MS = 250;
144
+ function isRetryableStatus(status) {
145
+ return RETRY_STATUSES.includes(status);
146
+ }
147
+ function retryAfterMs(res) {
148
+ const raw = res.headers?.get?.("retry-after");
149
+ if (!raw) return null;
150
+ const seconds = Number(raw);
151
+ if (Number.isFinite(seconds)) {
152
+ return Math.min(Math.max(seconds, 0) * 1e3, MAX_DELAY_MS);
153
+ }
154
+ const at = Date.parse(raw);
155
+ if (!Number.isNaN(at)) {
156
+ return Math.min(Math.max(0, at - Date.now()), MAX_DELAY_MS);
157
+ }
158
+ return null;
159
+ }
160
+ function backoffMs(attempt, res) {
161
+ const fromServer = res ? retryAfterMs(res) : null;
162
+ const base = Math.min(fromServer ?? BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS - JITTER_MS);
163
+ return base + Math.random() * JITTER_MS;
164
+ }
165
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
166
+
167
+ // src/read-client.ts
168
+ var DEFAULT_TIMEOUT = 1e4;
169
+ function qs(params) {
170
+ const sp = new URLSearchParams();
171
+ for (const [k, v] of Object.entries(params)) {
172
+ if (v !== void 0 && v !== "") sp.set(k, String(v));
173
+ }
174
+ const s = sp.toString();
175
+ return s ? `?${s}` : "";
176
+ }
177
+ function unwrap(json) {
178
+ if (json && typeof json === "object") {
179
+ const o = json;
180
+ const isPayload = "id" in o || "slug" in o || "items" in o;
181
+ if (!isPayload && "data" in o) return o.data;
182
+ }
183
+ return json;
184
+ }
185
+ function tokenSlug(token) {
186
+ try {
187
+ const part = token.split(".")[1];
188
+ if (!part) return null;
189
+ const json = atob(part.replace(/-/g, "+").replace(/_/g, "/"));
190
+ return JSON.parse(json).entrySlug ?? null;
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+ function createRequestCache() {
196
+ const inflight = /* @__PURE__ */ new Map();
197
+ const settled = /* @__PURE__ */ new Map();
198
+ const copy = (value) => typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
199
+ return async function collapse(url, fetcher) {
200
+ if (settled.has(url)) return copy(settled.get(url));
201
+ const pending = inflight.get(url);
202
+ if (pending) return copy(await pending);
203
+ const run = (async () => {
204
+ try {
205
+ const value = await fetcher();
206
+ settled.set(url, value);
207
+ return value;
208
+ } finally {
209
+ inflight.delete(url);
210
+ }
211
+ })();
212
+ inflight.set(url, run);
213
+ return copy(await run);
214
+ };
215
+ }
216
+ function createClient(options) {
217
+ const apiUrl = options.apiUrl.replace(/\/+$/, "");
218
+ const { workspace, apiKey, perspective = "published", previewToken } = options;
219
+ const defaultProjectId = options.projectId;
220
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT;
221
+ const base = `${apiUrl}/api/v1/delivery/${workspace}`;
222
+ const headers = () => {
223
+ const h = { "Content-Type": "application/json" };
224
+ if (apiKey) h["X-API-Key"] = apiKey;
225
+ return h;
226
+ };
227
+ const collapse = options.cache && perspective === "published" ? createRequestCache() : void 0;
228
+ const fetchJSON = (url) => collapse ? collapse(url, () => request(url)) : request(url);
229
+ async function request(url) {
230
+ let lastError;
231
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
232
+ const controller = new AbortController();
233
+ const timer = setTimeout(() => controller.abort(), timeout);
234
+ try {
235
+ const res = await globalThis.fetch(url, {
236
+ headers: headers(),
237
+ signal: controller.signal
238
+ });
239
+ clearTimeout(timer);
240
+ if (res.ok) return await res.json();
241
+ const retryable = attempt < MAX_RETRIES && isRetryableStatus(res.status);
242
+ if (!retryable) throw await BetterCMSError.from(res);
243
+ await sleep(backoffMs(attempt, res));
244
+ } catch (err) {
245
+ clearTimeout(timer);
246
+ if (err instanceof Error && err.name === "AbortError") {
247
+ throw new BetterCMSError("Request timeout", 408, "NETWORK_ERROR");
248
+ }
249
+ if (err instanceof BetterCMSError) throw err;
250
+ lastError = err;
251
+ if (attempt < MAX_RETRIES) {
252
+ await sleep(backoffMs(attempt));
253
+ continue;
254
+ }
255
+ throw new BetterCMSError(
256
+ `Network error: ${err instanceof Error ? err.message : String(err)}`,
257
+ 0,
258
+ "NETWORK_ERROR"
259
+ );
260
+ }
261
+ }
262
+ throw lastError ?? new BetterCMSError("Unreachable", 0, "NETWORK_ERROR");
263
+ }
264
+ const draft = perspective === "drafts";
265
+ const stega = options.stega ?? draft;
266
+ return {
267
+ perspective,
268
+ async getEntry(slug, opts) {
269
+ const url = draft && previewToken ? `${apiUrl}/api/v1/preview/${encodeURIComponent(slug)}${qs({
270
+ token: previewToken,
271
+ depth: opts?.depth,
272
+ select: opts?.select?.join(","),
273
+ stega: stega ? "1" : void 0
274
+ })}` : `${base}/content-entries/${encodeURIComponent(slug)}${qs({
275
+ depth: opts?.depth,
276
+ select: opts?.select?.join(","),
277
+ perspective: draft ? "draft" : void 0,
278
+ stega: draft && stega ? "1" : void 0,
279
+ projectId: defaultProjectId
280
+ })}`;
281
+ return unwrap(await fetchJSON(url));
282
+ },
283
+ async listEntries(opts) {
284
+ const url = `${base}/content-entries${qs({
285
+ model: opts?.model,
286
+ page: opts?.page,
287
+ perPage: opts?.perPage,
288
+ depth: opts?.depth,
289
+ select: opts?.select?.join(","),
290
+ perspective: draft ? "draft" : void 0,
291
+ stega: draft && stega ? "1" : void 0,
292
+ projectId: defaultProjectId
293
+ })}`;
294
+ return unwrap(await fetchJSON(url));
295
+ },
296
+ async getPage(slug) {
297
+ if (draft && previewToken && tokenSlug(previewToken) === slug) {
298
+ const url = `${apiUrl}/api/v1/preview/${encodeURIComponent(slug)}${qs({
299
+ token: previewToken,
300
+ stega: stega ? "1" : void 0
301
+ })}`;
302
+ const d = unwrap(await fetchJSON(url));
303
+ return {
304
+ slug: d.slug,
305
+ title: d.title,
306
+ metaTitle: d.metaTitle ?? null,
307
+ metaDescription: d.metaDescription ?? null,
308
+ metaJson: d.metaJson ?? null,
309
+ blocks: d.blockJson ?? [],
310
+ // C3: the DRAFT field styles, present only when the page styles a field.
311
+ ...d.fieldStyles ? { fieldStyles: d.fieldStyles } : {},
312
+ updatedAt: d.updatedAt ?? null,
313
+ publishedAt: d.publishedAt ?? null
314
+ };
315
+ }
316
+ let page = 1;
317
+ for (; ; ) {
318
+ const list = await this.listPages({ page, perPage: 100 });
319
+ const found = list.items.find((p) => p.slug === slug);
320
+ if (found) return found;
321
+ if (!list.hasNextPage) return null;
322
+ page += 1;
323
+ }
324
+ },
325
+ async listPages(opts) {
326
+ const url = `${base}/pages${qs({
327
+ page: opts?.page,
328
+ perPage: opts?.perPage,
329
+ projectId: defaultProjectId
330
+ })}`;
331
+ return unwrap(await fetchJSON(url));
332
+ },
333
+ async listComponents() {
334
+ const url = `${base}/components${qs({
335
+ perspective: draft ? "draft" : void 0,
336
+ token: draft ? previewToken : void 0,
337
+ projectId: defaultProjectId
338
+ })}`;
339
+ return unwrap(await fetchJSON(url));
340
+ },
341
+ async getLayout() {
342
+ const url = `${base}/layout${qs({ perspective: draft ? "draft" : void 0, projectId: defaultProjectId })}`;
343
+ try {
344
+ return unwrap(await fetchJSON(url));
345
+ } catch (error) {
346
+ if (error instanceof BetterCMSError && error.status === 404) return null;
347
+ throw error;
348
+ }
349
+ },
350
+ async listForms(opts) {
351
+ const url = `${base}/forms${qs({
352
+ projectId: opts?.projectId ?? defaultProjectId
353
+ })}`;
354
+ return unwrap(
355
+ await fetchJSON(url)
356
+ );
357
+ },
358
+ tag(resource, id) {
359
+ return id ? `bcms:${workspace}:${resource}:${id}` : `bcms:${workspace}:${resource}`;
360
+ }
361
+ };
362
+ }
363
+
364
+ // src/forms.ts
365
+ function isMultiValueField(type) {
366
+ return type === "checkboxes";
367
+ }
368
+ function asStringArray(value) {
369
+ if (Array.isArray(value)) return value;
370
+ return value ? [String(value)] : [];
371
+ }
372
+ function toggleOption(value, option, on) {
373
+ const current = asStringArray(value);
374
+ if (on) return current.includes(option) ? current : [...current, option];
375
+ return current.filter((v) => v !== option);
376
+ }
377
+ var DEFAULT_BASE_URL = "https://api.bettercms.ai";
378
+ function shouldShowField(field, values) {
379
+ if (!field.showIf) return true;
380
+ const trigger = values[field.showIf.field];
381
+ if (Array.isArray(trigger)) return trigger.includes(field.showIf.equals);
382
+ return String(trigger ?? "") === field.showIf.equals;
383
+ }
384
+ function formInitialValues(form, prefill = {}) {
385
+ const values = {};
386
+ for (const field of form.fields) {
387
+ if (field.type === "step") continue;
388
+ if (field.type === "checkbox" || field.type === "consent") {
389
+ values[field.key] = false;
390
+ } else if (field.type === "checkboxes") {
391
+ const seed = prefill[field.key] ?? field.defaultValue ?? "";
392
+ values[field.key] = seed ? seed.split(",").map((v) => v.trim()).filter(Boolean) : [];
393
+ } else {
394
+ values[field.key] = prefill[field.key] ?? field.defaultValue ?? "";
395
+ }
396
+ }
397
+ return values;
398
+ }
399
+ async function submitForm(opts) {
400
+ const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
401
+ const doFetch = opts.fetchImpl ?? globalThis.fetch;
402
+ const body = { data: opts.data };
403
+ if (opts.turnstileToken) body["cf-turnstile-response"] = opts.turnstileToken;
404
+ let res;
405
+ try {
406
+ res = await doFetch(`${baseUrl}/api/v1/forms/public/${opts.formId}/submissions`, {
407
+ method: "POST",
408
+ headers: { "Content-Type": "application/json" },
409
+ body: JSON.stringify(body)
410
+ });
411
+ } catch (err) {
412
+ throw new BetterCMSError(
413
+ `Network error: ${err instanceof Error ? err.message : String(err)}`,
414
+ 0,
415
+ ErrorCodes.NETWORK_ERROR
416
+ );
417
+ }
418
+ if (!res.ok) {
419
+ const error = await BetterCMSError.from(res.clone());
420
+ const body2 = await res.json().catch(() => ({}));
421
+ if (body2.errors) error.fieldErrors = body2.errors;
422
+ throw error;
423
+ }
424
+ const json = await res.json().catch(() => ({}));
425
+ return { id: String(json.id ?? "") };
426
+ }
427
+
428
+ // src/search.ts
429
+ var DEFAULT_BASE_URL2 = "https://api.bettercms.ai";
430
+ async function search(opts) {
431
+ const q = opts.q.trim();
432
+ if (!q) return [];
433
+ const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
434
+ const doFetch = opts.fetchImpl ?? globalThis.fetch;
435
+ const params = new URLSearchParams({ project: opts.project, q });
436
+ if (opts.limit) params.set("limit", String(opts.limit));
437
+ try {
438
+ const res = await doFetch(`${baseUrl}/api/v1/delivery/search?${params}`, { signal: opts.signal });
439
+ if (!res.ok) return [];
440
+ const json = await res.json().catch(() => ({}));
441
+ return json.hits ?? [];
442
+ } catch {
443
+ return [];
444
+ }
445
+ }
446
+ // Annotate the CommonJS export names for ESM import in node:
447
+ 0 && (module.exports = {
448
+ asStringArray,
449
+ createClient,
450
+ formInitialValues,
451
+ isMultiValueField,
452
+ search,
453
+ shouldShowField,
454
+ submitForm,
455
+ toggleOption
456
+ });
457
+ //# sourceMappingURL=read.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/read.ts","../src/errors.ts","../src/retry.ts","../src/read-client.ts","../src/forms.ts","../src/search.ts"],"sourcesContent":["/**\n * What a browser bundle reads with, and nothing else: the read client, public form submission and\n * site search. The root entry also carries the management client, whose media upload helpers\n * import node:fs and node:dns (webpack 5 refuses them, Vite warns about them).\n */\nexport { createClient, type CreateClientOptions, type BetterCMSReadClient } from \"./read-client.js\";\nexport { submitForm, shouldShowField, formInitialValues, isMultiValueField, asStringArray, toggleOption } from \"./forms.js\";\nexport type { DeliveryForm, DeliveryFormField, DeliveryFormFieldType, FormValue, FormValues, SubmitFormOptions, SubmitFormResult, FormSubmitError } from \"./forms.js\";\nexport { search, type SearchHit, type SearchOptions } from \"./search.js\";\n","import type { BetterCMSErrorCode } from \"@bettercms-ai/types\";\n\n/** Semantic error codes used by the SDK. */\nexport const ErrorCodes = {\n CONTENT_NOT_FOUND: \"CONTENT_NOT_FOUND\",\n UNAUTHORIZED: \"UNAUTHORIZED\",\n FORBIDDEN: \"FORBIDDEN\",\n RATE_LIMITED: \"RATE_LIMITED\",\n INTERNAL_ERROR: \"INTERNAL_ERROR\",\n VALIDATION_ERROR: \"VALIDATION_ERROR\",\n NETWORK_ERROR: \"NETWORK_ERROR\",\n ADMIN_NOT_IMPLEMENTED: \"ADMIN_NOT_IMPLEMENTED\",\n} as const;\n\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\n/** Maps HTTP status codes to semantic error codes. */\nfunction statusToCode(status: number): BetterCMSErrorCode {\n if (status === 404) return ErrorCodes.CONTENT_NOT_FOUND;\n if (status === 401) return ErrorCodes.UNAUTHORIZED;\n if (status === 403) return ErrorCodes.FORBIDDEN;\n if (status === 429) return ErrorCodes.RATE_LIMITED;\n if (status >= 500) return ErrorCodes.INTERNAL_ERROR;\n if (status === 422) return ErrorCodes.VALIDATION_ERROR;\n return ErrorCodes.INTERNAL_ERROR;\n}\n\n/**\n * BetterCMSError — normalized SDK error with a semantic code.\n */\nexport class BetterCMSError extends Error {\n readonly status: number;\n readonly code: BetterCMSErrorCode;\n /**\n * Raw machine-readable `code` from the response body (e.g. \"PROJECT_DELETED\"),\n * when the API supplies one. Distinct from `code` (which is derived from the\n * HTTP status), this lets callers branch on a specific server condition that\n * shares a status with others (e.g. 409 slug-conflict vs 409 project-deleted).\n */\n readonly bodyCode?: string;\n /**\n * Per-field validation messages from a 400 `{ errors: { field: message } }` body.\n * The backend returns validation failures in this shape (not `message`/`error`), so\n * without this the SDK collapsed every validation 400 to a bare \"Bad Request\" with no\n * hint which field or rule failed (FLO-474). Mirrors the forms client's `fieldErrors`.\n */\n readonly fieldErrors?: Record<string, string>;\n /**\n * The parsed response body, when there was one.\n *\n * Several refusals carry STRUCTURED detail that neither `message` nor `fieldErrors` can hold,\n * and which is the whole point of the refusal: a component publish 409s with the full\n * `readiness` tuple (what evidence is missing, whether a human still has to approve it) and\n * 422s with the `issues` naming the Layouts it would break. Collapsing those to a sentence\n * leaves a caller able to report that something failed and not what to do about it.\n */\n readonly body?: unknown;\n\n constructor(\n message: string,\n status: number,\n code: BetterCMSErrorCode,\n bodyCode?: string,\n fieldErrors?: Record<string, string>,\n body?: unknown,\n ) {\n super(message);\n this.name = \"BetterCMSError\";\n this.status = status;\n this.code = code;\n this.bodyCode = bodyCode;\n this.fieldErrors = fieldErrors;\n this.body = body;\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, BetterCMSError);\n }\n }\n\n toJSON() {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n code: this.code,\n bodyCode: this.bodyCode,\n fieldErrors: this.fieldErrors,\n };\n }\n\n /**\n * Factory — creates a BetterCMSError from a failed fetch Response. Reads the body's\n * `message` (or `error`) for the human message and `code` for a machine-readable\n * condition surfaced as `bodyCode`. Validation failures arrive as `{ errors: {...} }`\n * (no `message`/`error`), so fall back to the first field error for the message and\n * expose the full map as `fieldErrors` (FLO-474).\n */\n static async from(res: Response): Promise<BetterCMSError> {\n let message = res.statusText || \"An error occurred\";\n let bodyCode: string | undefined;\n let fieldErrors: Record<string, string> | undefined;\n let code = statusToCode(res.status);\n let raw: unknown;\n try {\n const body = (await res.json()) as {\n message?: string;\n error?: string;\n code?: string;\n errors?: Record<string, string>;\n };\n if (body?.errors && typeof body.errors === \"object\") {\n fieldErrors = body.errors;\n code = ErrorCodes.VALIDATION_ERROR;\n }\n if (body?.message) message = body.message;\n else if (body?.error) message = body.error;\n else if (fieldErrors) {\n const [field, detail] = Object.entries(fieldErrors).find(\n ([, v]) => typeof v === \"string\" && v.length > 0,\n ) ?? [];\n if (field) message = `${field}: ${detail}`;\n }\n if (body?.code) bodyCode = body.code;\n raw = body;\n } catch {\n // ignore parse errors\n }\n return new BetterCMSError(message, res.status, code, bodyCode, fieldErrors, raw);\n }\n}\n","/**\n * Shared 429/503 backoff for every SDK client.\n *\n * ── Why this exists ─────────────────────────────────────────────────────────\n * The backend limiter (src/lib/ratelimit.ts, windowHit) is a FIXED 60-SECOND\n * WINDOW: 100 requests per workspace per endpoint, and a denial answers\n * `Retry-After: <seconds until the window resets>` — anything up to 60.\n *\n * Every client here used to climb a private ladder that topped out at 7 seconds\n * ([250,750,1500] in read-client, [1000,2000,4000] in admin/management) and none\n * of them read Retry-After. So all three retries landed INSIDE the same exhausted\n * window and a 429 was fatal roughly whenever it was thrown. The retry code looked\n * like resilience but could only ever recover from a denial raised in the final\n * seconds of a window.\n *\n * That is what failed customer builds: `action/fetch-content.mjs` spends ~65 of the\n * 100 `delivery-content-entries-list` slots pulling a large workspace, `npm run build`\n * starts seconds later in the SAME window, and the first per-route delivery read past\n * the ceiling killed the deploy with \"Too many requests. Please retry later.\"\n *\n * ── The rule ────────────────────────────────────────────────────────────────\n * Obey Retry-After when the server sends one, and let a single request outlast a\n * whole window. A build waiting 60s is slow; a build that dies is a broken product.\n */\n\n/** Statuses worth retrying. 429 = rate limited, 503 = limiter/backend unavailable. */\nexport const RETRY_STATUSES = [429, 503] as const;\n\n/** Retries after the first attempt. 3 retries × up to ~65s spans three full windows. */\nexport const MAX_RETRIES = 3;\n\n/**\n * Hard ceiling on a single wait, jitter included. Must exceed the limiter's 60s\n * window, or a denial raised at the very start of a window still cannot be waited\n * out — and must be finite, so a hostile or mis-set Retry-After cannot hang a build.\n */\nexport const MAX_DELAY_MS = 65_000;\n\n/** Fallback ladder when the server sends no Retry-After: 1s, 2s, 4s, 8s. */\nconst BASE_DELAY_MS = 1_000;\n\n/**\n * De-sync jitter, added to every wait.\n *\n * Deliberately a small FLAT amount, not a percentage. Requests denied by one window\n * only need to stop firing in the same millisecond — the server's counter serialises\n * them after that. A proportional jitter would add up to 15 seconds to a 60-second\n * wait for no benefit, and would push the total past MAX_DELAY_MS.\n */\nconst JITTER_MS = 250;\n\nexport function isRetryableStatus(status: number): boolean {\n return (RETRY_STATUSES as readonly number[]).includes(status);\n}\n\n/**\n * Parse `Retry-After`, which RFC 9110 allows in either form: delay-seconds\n * (\"47\") or an HTTP-date (\"Wed, 21 Oct 2015 07:28:00 GMT\"). Returns ms to wait,\n * or null when the header is absent or unparseable.\n */\nexport function retryAfterMs(res: Pick<Response, \"headers\">): number | null {\n const raw = res.headers?.get?.(\"retry-after\");\n if (!raw) return null;\n\n const seconds = Number(raw);\n if (Number.isFinite(seconds)) {\n return Math.min(Math.max(seconds, 0) * 1000, MAX_DELAY_MS);\n }\n\n const at = Date.parse(raw);\n if (!Number.isNaN(at)) {\n return Math.min(Math.max(0, at - Date.now()), MAX_DELAY_MS);\n }\n return null;\n}\n\n/**\n * How long to wait before retry `attempt` (0-based).\n *\n * Server instruction wins; otherwise exponential. Jitter is added in both cases:\n * when a build's requests are all denied by the same window they would otherwise\n * wake in lockstep at the reset and re-exhaust it immediately (thundering herd).\n */\nexport function backoffMs(attempt: number, res?: Pick<Response, \"headers\">): number {\n const fromServer = res ? retryAfterMs(res) : null;\n // Base is capped with the jitter's headroom already reserved, so the total never\n // exceeds MAX_DELAY_MS AND the jitter is never clamped away at the ceiling — which\n // would put every capped caller back in lockstep, the exact thing jitter prevents.\n const base = Math.min(fromServer ?? BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS - JITTER_MS);\n // Jitter is ADDED, never subtracted: waking before the server's Retry-After elapses\n // just burns a retry on a window that has not reset.\n return base + Math.random() * JITTER_MS;\n}\n\nexport const sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n","/**\n * createClient — framework-agnostic typed read client for the Delivery API.\n *\n * Shared by @bettercms-ai/astro and @bettercms-ai/next so the fetch/typing/retry\n * logic lives in one place. Read-only; for schema authoring use the\n * Management client, for sessions use the Admin client.\n *\n * const client = createClient({ apiUrl, workspace, apiKey });\n * await client.getPage(\"home\");\n * await client.listEntries({ model: \"post\", perPage: 50 });\n */\nimport type {\n DeliveryComponent,\n DeliveryEntry,\n DeliveryList,\n DeliveryPage,\n DeliveredLayout,\n Perspective,\n} from \"@bettercms-ai/types\";\nimport { BetterCMSError } from \"./errors.js\";\nimport { MAX_RETRIES, backoffMs, isRetryableStatus, sleep } from \"./retry.js\";\nimport type { DeliveryForm } from \"./forms.js\";\n\nconst DEFAULT_TIMEOUT = 10_000;\n\nexport interface CreateClientOptions {\n /** Backend base, e.g. `https://api.bettercms.ai` (no trailing slash needed). */\n apiUrl: string;\n /** Workspace slug. */\n workspace: string;\n /** Delivery API key. Use a `content:read:draft` key for drafts. */\n apiKey?: string;\n /**\n * The project every read is addressed to. A workspace holds many projects whose slugs\n * collide (each has a `blog` model and a `home` page), so a site names its own; published\n * reads need no key for it. BetterCMS hosting supplies `PUBLIC_BCMS_PROJECT_ID`.\n */\n projectId?: string;\n /** `published` (default) or `drafts`. */\n perspective?: Perspective;\n /**\n * Preview-token JWT for token-based single-document draft reads via the\n * `/preview/:slug` endpoint. Only used when `perspective === \"drafts\"`.\n */\n previewToken?: string;\n /**\n * Embed invisible visual-editing provenance in draft reads, so a preview build can\n * deep-link each rendered value back to its editor. Requires `perspective: \"drafts\"`,\n * and then either a `previewToken` or a `content:read:draft` key. Ignored on published\n * reads — the payload must never reach a production build.\n *\n * **Defaults to `true` on `perspective: \"drafts\"`** (since 1.15.0). Provenance is the whole\n * reason the drafts perspective exists in a preview build, and an option nobody knew to pass\n * meant every such build rendered anonymous strings: the editor could bind them only by\n * guessing at their text. It stays OFF on every other perspective, unconditionally, so a\n * published build cannot acquire a payload by accident — pass `stega: false` to opt a draft\n * read out (a diff or a checksum over draft content is the case that wants it).\n */\n stega?: boolean;\n /** Per-attempt timeout in ms. Defaults to 10000. */\n timeout?: number;\n /**\n * Collapse identical GETs to one network round-trip for the life of this client.\n * Default false.\n *\n * Only correct where the content is a POINT-IN-TIME SNAPSHOT — i.e. a static build,\n * where every route renders from the same published dataset. `@bettercms-ai/astro`\n * turns this on automatically when Astro's `command === \"build\"` and never for `dev`,\n * SSR or the drafts perspective, where a repeat read must see the current row.\n *\n * A static build without it re-fetches the same nav/settings/collection once PER ROUTE.\n * That is what made a large site exhaust the delivery rate limit: hundreds of identical\n * requests, and `getPage()` re-scanning every page of the paginated list each time it is\n * called.\n */\n cache?: boolean;\n}\n\nexport interface ListEntriesOptions {\n /** Filter by content-model slug. */\n model?: string;\n page?: number;\n perPage?: number;\n /** Reference hydration depth (lists cap at 1). */\n depth?: 0 | 1;\n /** Projection — field names to return. */\n select?: string[];\n}\n\nexport interface GetEntryOptions {\n depth?: 0 | 1 | 2;\n select?: string[];\n}\n\nexport interface ListPagesOptions {\n page?: number;\n perPage?: number;\n}\n\nexport interface BetterCMSReadClient {\n readonly perspective: Perspective;\n getEntry<T = Record<string, unknown>>(\n slug: string,\n opts?: GetEntryOptions,\n ): Promise<DeliveryEntry<T>>;\n listEntries<T = Record<string, unknown>>(\n opts?: ListEntriesOptions,\n ): Promise<DeliveryList<DeliveryEntry<T>>>;\n getPage(slug: string): Promise<DeliveryPage | null>;\n listPages(opts?: ListPagesOptions): Promise<DeliveryList<DeliveryPage>>;\n /**\n * The workspace's reusable component definitions, for resolving `component` blocks.\n * Under `perspective: \"drafts\"` this is the DRAFT definition — what makes an SSR render\n * of a draft page look like the page the editor is showing.\n */\n listComponents(): Promise<DeliveryComponent[]>;\n /** Fetch the project-scoped Global Layout for the active perspective. */\n getLayout(): Promise<DeliveredLayout | null>;\n listForms(opts?: {\n projectId?: string;\n }): Promise<{ items: DeliveryForm[]; turnstileSiteKey: string | null }>;\n /** Build a deterministic Next/cache tag for a resource. */\n tag(resource: \"entry\" | \"page\" | \"forms\", id?: string): string;\n}\n\n/** Drop undefined values, stringify the rest into a query string. */\nfunction qs(params: Record<string, string | number | undefined>): string {\n const sp = new URLSearchParams();\n for (const [k, v] of Object.entries(params)) {\n if (v !== undefined && v !== \"\") sp.set(k, String(v));\n }\n const s = sp.toString();\n return s ? `?${s}` : \"\";\n}\n\n/**\n * Unwrap a `{ data: ... }` list/legacy envelope when present. A bare entry is\n * already the payload (it has `id`/`slug` and its own `data` field), so we only\n * unwrap when no payload markers are at the top level.\n */\nfunction unwrap<T>(json: unknown): T {\n if (json && typeof json === \"object\") {\n const o = json as Record<string, unknown>;\n const isPayload = \"id\" in o || \"slug\" in o || \"items\" in o;\n if (!isPayload && \"data\" in o) return o.data as T;\n }\n return json as T;\n}\n\n/**\n * The `entrySlug` claim of a preview-token JWT, UNVERIFIED.\n *\n * The backend is the signature authority — this read only decides WHICH url to call, and a\n * forged claim buys nothing: `/preview/:slug` re-verifies and 401s. Kept private to this file\n * (`@bettercms-ai/astro` has its own copy for the same reason: neither package may depend on\n * the other).\n */\nfunction tokenSlug(token: string): string | null {\n try {\n const part = token.split(\".\")[1];\n if (!part) return null;\n const json = atob(part.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n return (JSON.parse(json) as { entrySlug?: string }).entrySlug ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * Per-URL request collapsing.\n *\n * Two jobs, both needed: coalesce requests that are IN FLIGHT (Astro renders routes\n * concurrently, so identical GETs overlap) and retain the ones that have COMPLETED\n * (routes render in batches, so the same GET recurs long after the first settled).\n *\n * Values are cloned on the way out. Callers get a fresh object graph, so a template that\n * sorts `items` in place — an easy thing to write and a brutal thing to debug — cannot\n * corrupt what every other route sees. Cloning costs memory bandwidth; the request it\n * replaces costs a network round-trip and a slot in the rate-limit window.\n *\n * A rejected request is never retained, so a transient failure does not become permanent.\n */\nfunction createRequestCache() {\n const inflight = new Map<string, Promise<unknown>>();\n const settled = new Map<string, unknown>();\n\n const copy = <T>(value: T): T =>\n typeof structuredClone === \"function\" ? structuredClone(value) : (JSON.parse(JSON.stringify(value)) as T);\n\n return async function collapse<T>(url: string, fetcher: () => Promise<T>): Promise<T> {\n if (settled.has(url)) return copy(settled.get(url) as T);\n\n const pending = inflight.get(url) as Promise<T> | undefined;\n if (pending) return copy(await pending);\n\n const run = (async () => {\n try {\n const value = await fetcher();\n settled.set(url, value);\n return value;\n } finally {\n inflight.delete(url);\n }\n })();\n inflight.set(url, run);\n return copy(await run);\n };\n}\n\nexport function createClient(options: CreateClientOptions): BetterCMSReadClient {\n const apiUrl = options.apiUrl.replace(/\\/+$/, \"\");\n const { workspace, apiKey, perspective = \"published\", previewToken } = options;\n const defaultProjectId = options.projectId;\n const timeout = options.timeout ?? DEFAULT_TIMEOUT;\n const base = `${apiUrl}/api/v1/delivery/${workspace}`;\n\n const headers = (): Record<string, string> => {\n const h: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (apiKey) h[\"X-API-Key\"] = apiKey;\n return h;\n };\n\n // Drafts must never be collapsed even if a caller asks: a preview surface re-reads to\n // see the edit that just happened, and serving it a retained copy would show stale content.\n const collapse =\n options.cache && perspective === \"published\" ? createRequestCache() : undefined;\n\n const fetchJSON = <T>(url: string): Promise<T> =>\n collapse ? collapse(url, () => request<T>(url)) : request<T>(url);\n\n async function request<T>(url: string): Promise<T> {\n let lastError: unknown;\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeout);\n try {\n const res = await globalThis.fetch(url, {\n headers: headers(),\n signal: controller.signal,\n });\n clearTimeout(timer);\n if (res.ok) return (await res.json()) as T;\n\n const retryable = attempt < MAX_RETRIES && isRetryableStatus(res.status);\n if (!retryable) throw await BetterCMSError.from(res);\n // Honour Retry-After: the limiter's window is up to 60s, far longer than\n // any fixed ladder, so the server's own number is the only one that works.\n await sleep(backoffMs(attempt, res));\n } catch (err) {\n clearTimeout(timer);\n if (err instanceof Error && err.name === \"AbortError\") {\n throw new BetterCMSError(\"Request timeout\", 408, \"NETWORK_ERROR\");\n }\n if (err instanceof BetterCMSError) throw err;\n lastError = err;\n if (attempt < MAX_RETRIES) {\n await sleep(backoffMs(attempt));\n continue;\n }\n throw new BetterCMSError(\n `Network error: ${err instanceof Error ? err.message : String(err)}`,\n 0,\n \"NETWORK_ERROR\",\n );\n }\n }\n throw lastError ?? new BetterCMSError(\"Unreachable\", 0, \"NETWORK_ERROR\");\n }\n\n const draft = perspective === \"drafts\";\n /**\n * Resolved ONCE, here, so the three call sites below cannot drift apart — they did exactly\n * that before (`options.stega` alone on the preview branch, `draft && options.stega` on the\n * other two), and a default expressed three times is a default that will eventually be two.\n * `??` and not `||`: an explicit `stega: false` on a draft read must win.\n */\n const stega = options.stega ?? draft;\n\n return {\n perspective,\n\n async getEntry<T = Record<string, unknown>>(slug: string, opts?: GetEntryOptions) {\n // Token-based draft reads go through the dedicated preview endpoint.\n // Without a token the drafts perspective is served by delivery itself, which needs\n // to be TOLD — `?perspective=draft` (honoured only for a `content:read:draft` key).\n // Until this branch sent it, `perspective: \"drafts\"` was silently inert here, and\n // with it `stega`, so a static build could never obtain provenance without minting\n // a preview token per entry.\n const url =\n draft && previewToken\n ? `${apiUrl}/api/v1/preview/${encodeURIComponent(slug)}${qs({\n token: previewToken,\n depth: opts?.depth,\n select: opts?.select?.join(\",\"),\n stega: stega ? \"1\" : undefined,\n })}`\n : `${base}/content-entries/${encodeURIComponent(slug)}${qs({\n depth: opts?.depth,\n select: opts?.select?.join(\",\"),\n perspective: draft ? \"draft\" : undefined,\n stega: draft && stega ? \"1\" : undefined,\n projectId: defaultProjectId,\n })}`;\n return unwrap<DeliveryEntry<T>>(await fetchJSON(url));\n },\n\n async listEntries<T = Record<string, unknown>>(opts?: ListEntriesOptions) {\n const url = `${base}/content-entries${qs({\n model: opts?.model,\n page: opts?.page,\n perPage: opts?.perPage,\n depth: opts?.depth,\n select: opts?.select?.join(\",\"),\n perspective: draft ? \"draft\" : undefined,\n stega: draft && stega ? \"1\" : undefined,\n projectId: defaultProjectId,\n })}`;\n return unwrap<DeliveryList<DeliveryEntry<T>>>(await fetchJSON(url));\n },\n\n async getPage(slug: string) {\n // The token names ONE page, and for that page the preview endpoint is the only read\n // that returns the DRAFT block tree — the paginated delivery list is published-only, so\n // an SSR draft render would otherwise be the live structure with draft field values.\n // Any other slug falls through to the published scan, which is what a token for /about\n // rendering the site's nav or footer pages must keep doing.\n if (draft && previewToken && tokenSlug(previewToken) === slug) {\n const url = `${apiUrl}/api/v1/preview/${encodeURIComponent(slug)}${qs({\n token: previewToken,\n stega: stega ? \"1\" : undefined,\n })}`;\n const d = unwrap<Record<string, unknown>>(await fetchJSON(url));\n return {\n slug: d.slug as string,\n title: d.title as string,\n metaTitle: (d.metaTitle as string | null) ?? null,\n metaDescription: (d.metaDescription as string | null) ?? null,\n metaJson: (d.metaJson as DeliveryPage[\"metaJson\"]) ?? null,\n blocks: (d.blockJson as DeliveryPage[\"blocks\"]) ?? [],\n // C3: the DRAFT field styles, present only when the page styles a field.\n ...(d.fieldStyles ? { fieldStyles: d.fieldStyles as NonNullable<DeliveryPage[\"fieldStyles\"]> } : {}),\n updatedAt: (d.updatedAt as string | null) ?? null,\n publishedAt: (d.publishedAt as string | null) ?? null,\n } satisfies DeliveryPage;\n }\n // No dedicated single-page delivery endpoint — scan the paginated list.\n let page = 1;\n for (;;) {\n const list = await this.listPages({ page, perPage: 100 });\n const found = list.items.find((p) => p.slug === slug);\n if (found) return found;\n if (!list.hasNextPage) return null;\n page += 1;\n }\n },\n\n async listPages(opts?: ListPagesOptions) {\n const url = `${base}/pages${qs({\n page: opts?.page,\n perPage: opts?.perPage,\n projectId: defaultProjectId,\n })}`;\n return unwrap<DeliveryList<DeliveryPage>>(await fetchJSON(url));\n },\n\n async listComponents() {\n // Same shape as getLayout: `?perspective=draft` alone is the key-scoped path (the server\n // ignores it for a published key), and a preview token is the page-scoped one.\n const url = `${base}/components${qs({\n perspective: draft ? \"draft\" : undefined,\n token: draft ? previewToken : undefined,\n projectId: defaultProjectId,\n })}`;\n return unwrap<DeliveryComponent[]>(await fetchJSON(url));\n },\n\n async getLayout() {\n const url = `${base}/layout${qs({ perspective: draft ? \"draft\" : undefined, projectId: defaultProjectId })}`;\n try {\n return unwrap<DeliveredLayout>(await fetchJSON(url));\n } catch (error) {\n if (error instanceof BetterCMSError && error.status === 404) return null;\n throw error;\n }\n },\n\n async listForms(opts?: { projectId?: string }) {\n const url = `${base}/forms${qs({\n projectId: opts?.projectId ?? defaultProjectId,\n })}`;\n return unwrap<{ items: DeliveryForm[]; turnstileSiteKey: string | null }>(\n await fetchJSON(url),\n );\n },\n\n tag(resource, id) {\n return id ? `bcms:${workspace}:${resource}:${id}` : `bcms:${workspace}:${resource}`;\n },\n };\n}\n","/**\n * Public forms — the headless integration surface for sites built on or imported\n * into BetterCMS (the \"render it yourself\" model, like Sanity's delivery API).\n *\n * Form definitions ship in the build-time content snapshot (`bcms-content.json` →\n * `forms` / `turnstileSiteKey`) and from `GET /api/v1/delivery/:workspace/forms`.\n * A site renders them with its own components (see `@bettercms-ai/next`'s <BcmsForm>)\n * and posts here. No API key — the endpoint is Turnstile-gated (Webflow model).\n *\n * This module is the single source of truth for the delivered form shape + the\n * pure logic (conditional visibility, initial values) the renderers share, so the\n * vanilla embed and the React component never drift.\n */\n\nimport { BetterCMSError, ErrorCodes } from \"./errors.js\";\n\n/**\n * Field types the delivery API serializes. Must stay equal to the authoring\n * vocabulary in `packages/db/src/schema/forms.ts` — this list is what every site\n * renderer switches on, so a type missing HERE renders as a plain text input on\n * every published site while the builder happily offers it. `radio`/`checkboxes`\n * were exactly that gap.\n */\nexport type DeliveryFormFieldType =\n | \"text\"\n | \"email\"\n | \"phone\"\n | \"url\"\n | \"number\"\n | \"date\"\n | \"textarea\"\n | \"select\"\n | \"radio\"\n | \"checkboxes\"\n | \"checkbox\"\n | \"consent\"\n | \"hidden\"\n | \"step\"; // a divider, not an input: renderers split the form into pages at it\n\nexport interface DeliveryFormField {\n key: string;\n label: string;\n type: DeliveryFormFieldType;\n placeholder?: string;\n /** Hint rendered under the control. */\n helpText?: string;\n required?: boolean;\n options?: string[];\n defaultValue?: string;\n /** Conditional visibility — render this field only when another field equals `equals`. */\n showIf?: { field: string; equals: string };\n /**\n * Per-field rules the API enforces on submit. Renderers should mirror them onto the\n * native control (`min`/`max`/`pattern`) so the browser reports them before the round\n * trip — the server check is the one that counts.\n */\n validation?: DeliveryFormFieldValidation;\n /**\n * Presentation options the renderer honours. Unlike `validation`, the API does NOT enforce\n * these on submit — they change what the visitor is shown, not what is accepted.\n */\n ui?: DeliveryFormFieldUi;\n}\n\n/**\n * @see FormFieldUi in @bettercms-ai/types — same shape, delivered to the site.\n *\n * ── The phone composition contract every renderer implements ──\n *\n * A picker-enabled phone field is rendered as a country control PLUS a `tel` input, but it\n * submits ONE value under the field's own key, in E.164. Whatever a renderer uses to hold the\n * country (a React state, a `{key}__country` select) is scaffolding: it must never reach the\n * POST body.\n *\n * Composition, from the visible input's raw text:\n * - empty → `\"\"`. NEVER `\"+CC\"`, and the reason is worse than \"the server rejects it\". The\n * server's loose phone shape is `{3,32}` chars, so the outcome depends on the calling\n * code's length: `\"+1\"` is REFUSED (an optional untouched field starts 400ing), while\n * `\"+91\"` is ACCEPTED and stored as a real-looking submission that is only a country code.\n * The silent case is the dangerous one. Pinned in `__tests__/lib/forms-validate.test.ts`.\n * - starts with `\"+\"` → `\"+\" + digits(raw)`. The visitor typed an international number; the\n * country control is ignored rather than fighting them.\n * - otherwise → `\"+\" + callingCode + digits(raw).replace(/^0/, \"\")`, dropping ONE trunk zero.\n * Known exception: Italian landlines keep their trunk zero, so this loses it. The value is\n * still accepted (shape-only check) — it is a formatting loss, not a refusal.\n *\n * `ui` changes what the visitor SEES. It never changes what the API accepts: enforcing a format\n * is `validation.phoneFormat`'s job and the two are independent.\n *\n * Renderer parity is behavioural, not implementational. FIVE lanes render this field, and they\n * agree on the VALUE, not on the implementation:\n *\n * | Lane | Control | As-you-type formatting |\n * |-------------------------------|---------------------------------|------------------------|\n * | dashboard form preview | searchable picker + formatter | yes |\n * | @bettercms-ai/next <BcmsForm> | native select + formatter | yes (lazy chunk) |\n * | hosted sites (render-page) | native select + tel input | NO |\n * | embed snippet | native select + tel input | NO |\n * | @bettercms-ai/astro BcmsForm | native select + tel input | NO |\n *\n * The missing as-you-type formatting on the three vanilla lanes is a NAMED degradation, not an\n * oversight: it is what keeps libphonenumber off a visitor's page entirely. `packages/next`\n * pays for it only on pages that actually render a picker, via a separate chunk.\n *\n * When a lane builds that option list, never pass a libphonenumber helper as a bare callback:\n * `getCountries().filter(isSupportedCountry)` hands it the array INDEX as its `metadata`\n * argument and throws. Always wrap — `.filter((iso) => isSupportedCountry(iso))`.\n *\n * And when a lane ships the composition rule inside a JS TEMPLATE LITERAL (the embed snippet\n * does), `\\D` collapses to `D` and the regex silently becomes `/D/g` — stripping the letter D\n * and leaving every other separator in place. Write `\\\\D` there. Raw HTML `<script>` text (the\n * hosted renderer, the Astro inline script) takes a single backslash. Nothing throws either\n * way; numbers just arrive malformed, so each lane pins it with an executed-script test.\n */\nexport interface DeliveryFormFieldUi {\n /** `phone` — render a country picker beside the number. The submitted value is E.164. */\n countryPicker?: boolean;\n /** `phone` — uppercase ISO 3166-1 alpha-2 the picker starts on. */\n defaultCountry?: string;\n /**\n * `phone` — show the country's flag beside the dialling code.\n *\n * Honoured only by a renderer with a custom picker. The native `<select>` the site lanes use\n * cannot hold an SVG, and an emoji flag renders as two grey letters on Windows, so those\n * lanes show the country NAME instead — the same information, legibly, everywhere.\n */\n showFlags?: boolean;\n}\n\n/** @see FormFieldValidation in @bettercms-ai/types — same shape, delivered to the site. */\nexport interface DeliveryFormFieldValidation {\n emailPolicy?: \"any\" | \"business\";\n min?: number;\n max?: number;\n phoneFormat?: \"any\" | \"e164\";\n pattern?: string;\n}\n\n/** The pick-many type, whose value is an array rather than a scalar. */\nexport function isMultiValueField(type: DeliveryFormFieldType): boolean {\n return type === \"checkboxes\";\n}\n\n/** A form as delivered to a published/imported site. Mirrors the delivery `/forms` payload. */\nexport interface DeliveryForm {\n id: string;\n name: string;\n /** The project the form belongs to. */\n projectId?: string | null;\n /**\n * A `draft` form refuses every submission: render it as unavailable rather than as a form that\n * silently drops what a visitor sends. Delivered for exactly that reason, and missing from this\n * type until the contract test held the two together.\n */\n status?: \"draft\" | \"published\" | (string & {});\n /** Set on a form the site's owner told BetterCMS to leave alone (ISO time); such a form is not wired. */\n ignoredAt?: string | null;\n fields: DeliveryFormField[];\n submitLabel?: string;\n successMessage?: string;\n redirectUrl?: string;\n turnstileEnabled?: boolean;\n honeypotField?: string | null;\n}\n\n/** `string[]` is the `checkboxes` (pick-many) value; every other type is scalar. */\nexport type FormValue = string | boolean | string[];\nexport type FormValues = Record<string, FormValue>;\n\n/** Read a `checkboxes` value defensively — stored data may predate the array shape. */\nexport function asStringArray(value: FormValue | undefined): string[] {\n if (Array.isArray(value)) return value;\n return value ? [String(value)] : [];\n}\n\n/** Add/remove `option` from a `checkboxes` value, preserving the field's option order. */\nexport function toggleOption(value: FormValue | undefined, option: string, on: boolean): string[] {\n const current = asStringArray(value);\n if (on) return current.includes(option) ? current : [...current, option];\n return current.filter((v) => v !== option);\n}\n\nconst DEFAULT_BASE_URL = \"https://api.bettercms.ai\";\n\n/**\n * Conditional visibility. A `showIf` field is shown only when its trigger field's\n * current value equals the target; non-conditional fields are always shown. The\n * renderer must skip hidden fields when collecting/validating so a hidden required\n * field never blocks submit.\n */\nexport function shouldShowField(field: DeliveryFormField, values: FormValues): boolean {\n if (!field.showIf) return true;\n const trigger = values[field.showIf.field];\n // A pick-many trigger satisfies `equals` when the option is among those checked;\n // `String([\"a\",\"b\"])` would compare against \"a,b\" and never match.\n if (Array.isArray(trigger)) return trigger.includes(field.showIf.equals);\n return String(trigger ?? \"\") === field.showIf.equals;\n}\n\n/**\n * Build the initial value map: checkbox/consent default to false, checkboxes to an\n * empty array, every other field to its `defaultValue`, with `prefill` (e.g. URL\n * query params) taking precedence.\n */\nexport function formInitialValues(\n form: DeliveryForm,\n prefill: Record<string, string> = {},\n): FormValues {\n const values: FormValues = {};\n for (const field of form.fields) {\n if (field.type === \"step\") continue; // a divider holds no value\n if (field.type === \"checkbox\" || field.type === \"consent\") {\n values[field.key] = false;\n } else if (field.type === \"checkboxes\") {\n const seed = prefill[field.key] ?? field.defaultValue ?? \"\";\n // A prefill/default arrives as a scalar (query string, stored column), so split\n // it back into the option list the control renders against.\n values[field.key] = seed ? seed.split(\",\").map((v) => v.trim()).filter(Boolean) : [];\n } else {\n values[field.key] = prefill[field.key] ?? field.defaultValue ?? \"\";\n }\n }\n return values;\n}\n\nexport interface SubmitFormOptions {\n formId: string;\n data: FormValues;\n /** Cloudflare Turnstile token; required when the form has `turnstileEnabled`. */\n turnstileToken?: string;\n /** API origin. Defaults to https://api.bettercms.ai. */\n baseUrl?: string;\n /** Injectable fetch for tests / non-browser runtimes. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface SubmitFormResult {\n id: string;\n}\n\n/** A submit failure that carries per-field validation messages from the API (HTTP 400). */\nexport type FormSubmitError = BetterCMSError & { fieldErrors?: Record<string, string> };\n\n/**\n * Submit a public form. Resolves with the new submission id, or throws a\n * BetterCMSError — field-level validation errors are attached as `.fieldErrors`.\n */\nexport async function submitForm(opts: SubmitFormOptions): Promise<SubmitFormResult> {\n const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const doFetch = opts.fetchImpl ?? globalThis.fetch;\n const body: Record<string, unknown> = { data: opts.data };\n if (opts.turnstileToken) body[\"cf-turnstile-response\"] = opts.turnstileToken;\n\n let res: Response;\n try {\n res = await doFetch(`${baseUrl}/api/v1/forms/public/${opts.formId}/submissions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n } catch (err) {\n throw new BetterCMSError(\n `Network error: ${err instanceof Error ? err.message : String(err)}`,\n 0,\n ErrorCodes.NETWORK_ERROR,\n );\n }\n\n if (!res.ok) {\n // `.from` reads the body for message/code; clone so we can still read `errors`.\n const error = (await BetterCMSError.from(res.clone())) as FormSubmitError;\n const body = (await res.json().catch(() => ({}))) as { errors?: Record<string, string> };\n if (body.errors) error.fieldErrors = body.errors;\n throw error;\n }\n\n const json = (await res.json().catch(() => ({}))) as { id?: string | number };\n return { id: String(json.id ?? \"\") };\n}\n","/**\n * Public site search — the headless surface for the visitor-facing search box.\n *\n * Queries `GET /api/v1/delivery/search?project=…&q=…`, the same key-less public\n * endpoint the embeddable widget uses. Powers <BcmsSearch> in @bettercms-ai/next\n * and @bettercms-ai/astro, so the component and any custom UI never drift.\n *\n * `snippet` is HTML-safe: the server escapes the field content and keeps only the\n * <mark> highlight wrappers, so it can be rendered with dangerouslySetInnerHTML.\n */\n\nconst DEFAULT_BASE_URL = \"https://api.bettercms.ai\";\n\nexport interface SearchHit {\n title: string;\n slug: string;\n type: \"page\" | \"entry\";\n /** HTML-safe snippet with <mark> highlights around matched terms. */\n snippet: string;\n /** Site-relative URL, e.g. \"/about\". */\n url: string;\n}\n\nexport interface SearchOptions {\n /** The project id (from the dashboard's Site Search settings). */\n project: string;\n /** The query string. Calls with fewer than 1 char resolve to []. */\n q: string;\n /** Max hits, 1–20 (default 8). */\n limit?: number;\n /** API origin. Defaults to https://api.bettercms.ai. */\n baseUrl?: string;\n /** Injectable fetch for tests / non-browser runtimes. */\n fetchImpl?: typeof fetch;\n /** AbortSignal so callers can cancel a stale in-flight search. */\n signal?: AbortSignal;\n}\n\n/** Search a published site. Resolves with hits, or [] on an empty query / network error. */\nexport async function search(opts: SearchOptions): Promise<SearchHit[]> {\n const q = opts.q.trim();\n if (!q) return [];\n const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const doFetch = opts.fetchImpl ?? globalThis.fetch;\n const params = new URLSearchParams({ project: opts.project, q });\n if (opts.limit) params.set(\"limit\", String(opts.limit));\n\n try {\n const res = await doFetch(`${baseUrl}/api/v1/delivery/search?${params}`, { signal: opts.signal });\n if (!res.ok) return [];\n const json = (await res.json().catch(() => ({}))) as { hits?: SearchHit[] };\n return json.hits ?? [];\n } catch {\n return []; // network/abort → no results, never throw into a render path\n }\n}\n"],"mappings":"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","names":["body","DEFAULT_BASE_URL"]}
@@ -0,0 +1,2 @@
1
+ export { a 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 SearchHit, g as SearchOptions, h as SubmitFormOptions, i as SubmitFormResult, j as asStringArray, k as createClient, l as formInitialValues, m as isMultiValueField, s as search, n as shouldShowField, o as submitForm, t as toggleOption } from './read-ndf2I3D4.cjs';
2
+ import '@bettercms-ai/types';
package/dist/read.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { a 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 SearchHit, g as SearchOptions, h as SubmitFormOptions, i as SubmitFormResult, j as asStringArray, k as createClient, l as formInitialValues, m as isMultiValueField, s as search, n as shouldShowField, o as submitForm, t as toggleOption } from './read-ndf2I3D4.js';
2
+ import '@bettercms-ai/types';