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