@bettercms-ai/astro 0.28.1 → 0.28.2

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.
@@ -1,27 +1,3 @@
1
- /**
2
- * The platform's canonical media host, matching `@bettercms-ai/image-url`'s own
3
- * `DEFAULT_BASE_URL` and the `CDN_URL` the backend mints asset URLs on.
4
- *
5
- * This used to be `media.bettercms.ai`. Both hostnames route to the backend — they are both in
6
- * `$bcms_api_host` in infra/nginx/backend-lb.conf — so nothing was broken, but a site built by
7
- * this integration disagreed with the URL the server itself hands out for the same asset. That
8
- * splits the CDN cache for identical bytes and makes an asset URL's origin depend on which code
9
- * path produced it, which is the exact drift image-url was moved off this host to end.
10
- *
11
- * A project that serves media from the API origin instead (`useCdn: false`) overrides this with
12
- * `PUBLIC_BCMS_MEDIA_URL` or the `mediaUrl` option.
13
- */
14
- declare const DEFAULT_MEDIA_URL = "https://cdn.bettercms.ai";
15
- declare const DRAFT_COOKIE = "bcms-draft";
16
- /** The specifier the components import, and the integration's plugin answers. */
17
- declare const PUBLIC_CONFIG_ID = "@bettercms-ai/astro/public-config";
18
- /**
19
- * The config with no integration: `PUBLIC_BCMS_*`, then the snapshot. Never throws: with neither,
20
- * `apiUrl` and `workspace` are empty, which a build that never talks to the API renders with.
21
- */
22
- declare function fallbackConfig(): PublicConfig;
23
- declare const config: PublicConfig;
24
-
25
1
  /**
26
2
  * Resolved configuration shared by the integration, runtime, middleware and
27
3
  * draft routes. Public values are safe to ship to the browser; the API key and
@@ -65,4 +41,4 @@ interface ResolvedConfig extends PublicConfig {
65
41
  */
66
42
  declare function resolveConfig(options: BetterCMSAstroOptions, env?: Record<string, string | undefined>, cwd?: string): ResolvedConfig;
67
43
 
68
- export { type BetterCMSAstroOptions as B, DRAFT_COOKIE as D, type PublicConfig as P, type ResolvedConfig as R, DEFAULT_MEDIA_URL as a, PUBLIC_CONFIG_ID as b, config as c, fallbackConfig as f, resolveConfig as r };
44
+ export { type BetterCMSAstroOptions as B, type PublicConfig as P, type ResolvedConfig as R, resolveConfig as r };
package/dist/index.d.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import { AstroIntegration } from 'astro';
2
- import { B as BetterCMSAstroOptions } from './config-CU7bp5dd.js';
3
- export { D as DRAFT_COOKIE, P as PublicConfig, R as ResolvedConfig, r as resolveConfig } from './config-CU7bp5dd.js';
2
+ import { B as BetterCMSAstroOptions } from './config-DUVkCo5R.js';
3
+ export { P as PublicConfig, R as ResolvedConfig, r as resolveConfig } from './config-DUVkCo5R.js';
4
4
  export { BcmsLocals, BetterCMSRuntime, RuntimeConfig, liveSrc } from './runtime.js';
5
5
  import { FieldStyles, FieldStyle } from '@bettercms-ai/types';
6
6
  export { ContentBlock, DeliveredLayout, DeliveryEntry, DeliveryList, DeliveryPage, LinkValue, PageMetaJson, Perspective, RESPONSIVE_CSS, SiteSeoDefaults, linkHref } from '@bettercms-ai/types';
7
7
  export { S as SectionColumnAttrs, a as SectionRootAttrs, b as SectionRootBlock, s as sectionColumnAttrs, c as sectionRootAttrs } from './section-root-JrXFxiBm.js';
8
8
  export { ImageCrop, ImageFit, ImageFocal, ImageFormat, ImageSource, ImageUrlBuilder, ImageValueUrlOptions, imageUrl, default as imageUrlBuilder, imageValueObjectPosition, imageValueUrl } from '@bettercms-ai/image-url';
9
9
  export { DeliveryForm, DeliveryFormField, ResolvedSeo, SeoInput, resolveSeo } from '@bettercms-ai/sdk';
10
+ export { DRAFT_COOKIE } from '@bettercms-ai/sdk/draft';
10
11
 
11
12
  /**
12
13
  * `bettercms()` — the Astro integration.
package/dist/index.js CHANGED
@@ -1,11 +1,14 @@
1
1
  // src/config.ts
2
2
  import { readFileSync } from "fs";
3
3
  import { resolve } from "path";
4
+ import { resolveBcmsConfig } from "@bettercms-ai/sdk/config";
4
5
 
5
6
  // src/public-config.ts
6
7
  import { readPublishedContent } from "@bettercms-ai/sdk";
7
- var DEFAULT_MEDIA_URL = "https://cdn.bettercms.ai";
8
- var DRAFT_COOKIE = "bcms-draft";
8
+ import { DEFAULT_MEDIA_URL } from "@bettercms-ai/sdk/config";
9
+ import { DRAFT_COOKIE } from "@bettercms-ai/sdk/draft";
10
+ import { DEFAULT_MEDIA_URL as DEFAULT_MEDIA_URL2 } from "@bettercms-ai/sdk/config";
11
+ import { DRAFT_COOKIE as DRAFT_COOKIE2 } from "@bettercms-ai/sdk/draft";
9
12
  var PUBLIC_CONFIG_ID = "@bettercms-ai/astro/public-config";
10
13
  function env(name) {
11
14
  const fromProcess = typeof process !== "undefined" ? process.env?.[name] : void 0;
@@ -24,9 +27,9 @@ function fallbackConfig() {
24
27
  apiUrl: trim(env("PUBLIC_BCMS_API_URL") ?? text(snapshot2.apiUrl) ?? ""),
25
28
  workspace: env("PUBLIC_BCMS_WORKSPACE") ?? text(snapshot2.workspace) ?? "",
26
29
  projectId: env("PUBLIC_BCMS_PROJECT_ID") ?? text(snapshot2.projectId),
27
- mediaUrl: trim(env("PUBLIC_BCMS_MEDIA_URL") ?? DEFAULT_MEDIA_URL),
30
+ mediaUrl: trim(env("PUBLIC_BCMS_MEDIA_URL") ?? DEFAULT_MEDIA_URL2),
28
31
  studioUrl: env("PUBLIC_BCMS_STUDIO_URL"),
29
- draftCookie: DRAFT_COOKIE
32
+ draftCookie: DRAFT_COOKIE2
30
33
  };
31
34
  }
32
35
  var KEYS = ["apiUrl", "workspace", "projectId", "mediaUrl", "studioUrl", "draftCookie"];
@@ -53,22 +56,12 @@ var required = (name, envVar, extra) => new Error(
53
56
  On BetterCMS hosting these are injected automatically \u2014 if this failed in a hosted build, the repository is running an outdated deploy workflow: publish once, or save Hosting settings, to refresh it.`
54
57
  );
55
58
  function resolveConfig(options, env2 = process.env, cwd = process.cwd()) {
56
- const snap = () => snapshot(cwd);
57
- const apiUrl = (options.apiUrl ?? env2.PUBLIC_BCMS_API_URL ?? snap().apiUrl ?? "").replace(/\/+$/, "");
58
- const workspace = options.workspace ?? env2.PUBLIC_BCMS_WORKSPACE ?? snap().workspace ?? "";
59
- if (!apiUrl) throw required("apiUrl", "PUBLIC_BCMS_API_URL", `run the content fetch so ${SNAPSHOT_FILE} is present.`);
60
- if (!workspace) {
59
+ const { config: config2, missing } = resolveBcmsConfig({ options, env: env2, prefixes: ["PUBLIC_"], snapshot: () => snapshot(cwd) });
60
+ if (missing.includes("apiUrl")) throw required("apiUrl", "PUBLIC_BCMS_API_URL", `run the content fetch so ${SNAPSHOT_FILE} is present.`);
61
+ if (missing.includes("workspace")) {
61
62
  throw required("workspace", "PUBLIC_BCMS_WORKSPACE", `run the content fetch so ${SNAPSHOT_FILE} is present.`);
62
63
  }
63
- return {
64
- apiUrl,
65
- workspace,
66
- projectId: options.projectId ?? env2.PUBLIC_BCMS_PROJECT_ID ?? snap().projectId,
67
- mediaUrl: (options.mediaUrl ?? env2.PUBLIC_BCMS_MEDIA_URL ?? DEFAULT_MEDIA_URL).replace(/\/+$/, ""),
68
- studioUrl: options.studioUrl ?? env2.PUBLIC_BCMS_STUDIO_URL,
69
- draftCookie: DRAFT_COOKIE,
70
- draft: options.draft ?? true
71
- };
64
+ return { ...config2, draftCookie: DRAFT_COOKIE, draft: options.draft ?? true };
72
65
  }
73
66
  function toPublicConfig(c) {
74
67
  const { draft: _draft, ...pub } = c;
@@ -209,6 +202,7 @@ var integration_default = bettercms;
209
202
  // src/runtime.ts
210
203
  import { createClient } from "@bettercms-ai/sdk";
211
204
  import { readPublishedContent as readPublishedContent2, publishedEntry } from "@bettercms-ai/sdk";
205
+ import { signValue, verifyValue, safeRedirectPath, decodeTokenSlug } from "@bettercms-ai/sdk/draft";
212
206
  function liveSrc(opts) {
213
207
  const base = opts.apiUrl.replace(/\/+$/, "");
214
208
  const q = new URLSearchParams({ key: opts.apiKey });
@@ -216,7 +210,6 @@ function liveSrc(opts) {
216
210
  if (opts.drafts) q.set("drafts", "1");
217
211
  return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;
218
212
  }
219
- var enc = new TextEncoder();
220
213
 
221
214
  // src/index.ts
222
215
  import { RESPONSIVE_CSS } from "@bettercms-ai/types";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config.ts","../src/public-config.ts","../src/integration.ts","../src/runtime.ts","../src/index.ts","../src/section-root.ts","../src/field-attrs.ts"],"sourcesContent":["/**\n * Resolved configuration shared by the integration, runtime, middleware and\n * draft routes. Public values are safe to ship to the browser; the API key and\n * draft secret are read from server env at runtime and never live here.\n *\n * Resolution order is options → `PUBLIC_BCMS_*` env → the build snapshot. Config-time\n * only: this module is reached exclusively through `dist/index.js`, which only an\n * `astro.config.*` imports, so the `node:fs` read below never enters a client bundle.\n * Browser code gets the `bettercms:config` virtual module instead.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nimport { DEFAULT_MEDIA_URL, DRAFT_COOKIE } from \"./public-config.js\";\n\nexport { DRAFT_COOKIE };\nconst SNAPSHOT_FILE = \"bcms-content.json\";\n\nexport interface BetterCMSAstroOptions {\n /** Backend base, e.g. `https://api.bettercms.ai`. Falls back to `PUBLIC_BCMS_API_URL`. */\n apiUrl?: string;\n /** Workspace slug. Falls back to `PUBLIC_BCMS_WORKSPACE`. */\n workspace?: string;\n /** Optional project scope for forms. Falls back to `PUBLIC_BCMS_PROJECT_ID`. */\n projectId?: string;\n /** Media host for the image-url builder. Defaults to the prod media host. */\n mediaUrl?: string;\n /** Dashboard base for visual-editing deep links. */\n studioUrl?: string;\n /** Enable the draft-mode middleware + injected draft routes. Default true. */\n draft?: boolean;\n}\n\n/** Browser-safe config — exposed via the `bettercms:config` virtual module. */\nexport interface PublicConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n mediaUrl: string;\n studioUrl?: string;\n draftCookie: string;\n}\n\nexport interface ResolvedConfig extends PublicConfig {\n draft: boolean;\n}\n\n/** The fields of the deploy Action's content snapshot that identify the site. */\ninterface Snapshot {\n apiUrl?: string;\n workspace?: string;\n projectId?: string;\n}\n\n/**\n * Read `bcms-content.json` — the snapshot the deploy Action writes next to the build,\n * BEFORE `astro build` runs — which already carries the workspace this site belongs to.\n *\n * This is what lets `bettercms()` work with no arguments and no env at all. Not reading it\n * was the whole bug: a hosted build had the answer sitting in its own working directory and\n * threw anyway, and every starter template had to reimplement this by hand to survive.\n *\n * Lazy: only consulted when options and env both miss, so the configured path does no I/O.\n * Absent/corrupt file ⇒ `{}`, and the caller raises the actionable error below.\n */\nlet snapshotCache: Snapshot | undefined;\nfunction snapshot(cwd: string): Snapshot {\n if (snapshotCache === undefined) {\n try {\n snapshotCache = JSON.parse(readFileSync(resolve(cwd, SNAPSHOT_FILE), \"utf8\")) as Snapshot;\n } catch {\n snapshotCache = {};\n }\n }\n return snapshotCache;\n}\n\n/** Test seam — the snapshot is memoised for the life of the process. */\nexport function resetSnapshotCache(): void {\n snapshotCache = undefined;\n}\n\nconst required = (name: string, envVar: string, extra: string) =>\n new Error(\n `[@bettercms-ai/astro] \\`${name}\\` is required. Pass \\`bettercms({ ${name} })\\`, set ` +\n `${envVar}, or ${extra}\\n` +\n `On BetterCMS hosting these are injected automatically — if this failed in a hosted ` +\n `build, the repository is running an outdated deploy workflow: publish once, or save ` +\n `Hosting settings, to refresh it.`,\n );\n\n/**\n * Merge integration options with `PUBLIC_BCMS_*` env and the build snapshot, validating the\n * essentials. Precedence: explicit options → env → snapshot.\n */\nexport function resolveConfig(\n options: BetterCMSAstroOptions,\n env: Record<string, string | undefined> = process.env,\n cwd: string = process.cwd(),\n): ResolvedConfig {\n const snap = (): Snapshot => snapshot(cwd);\n\n const apiUrl = (options.apiUrl ?? env.PUBLIC_BCMS_API_URL ?? snap().apiUrl ?? \"\").replace(/\\/+$/, \"\");\n const workspace = options.workspace ?? env.PUBLIC_BCMS_WORKSPACE ?? snap().workspace ?? \"\";\n // No default origin on purpose: guessing one produces a site that builds cleanly against\n // the wrong backend, which is far harder to notice than this error.\n if (!apiUrl) throw required(\"apiUrl\", \"PUBLIC_BCMS_API_URL\", `run the content fetch so ${SNAPSHOT_FILE} is present.`);\n if (!workspace) {\n throw required(\"workspace\", \"PUBLIC_BCMS_WORKSPACE\", `run the content fetch so ${SNAPSHOT_FILE} is present.`);\n }\n\n return {\n apiUrl,\n workspace,\n projectId: options.projectId ?? env.PUBLIC_BCMS_PROJECT_ID ?? snap().projectId,\n mediaUrl: (options.mediaUrl ?? env.PUBLIC_BCMS_MEDIA_URL ?? DEFAULT_MEDIA_URL).replace(/\\/+$/, \"\"),\n studioUrl: options.studioUrl ?? env.PUBLIC_BCMS_STUDIO_URL,\n draftCookie: DRAFT_COOKIE,\n draft: options.draft ?? true,\n };\n}\n\nexport function toPublicConfig(c: ResolvedConfig): PublicConfig {\n const { draft: _draft, ...pub } = c;\n return pub;\n}\n","/**\n * The public config the shipped components read (`BcmsImage`, `BcmsForm`).\n *\n * Where `bettercms()` is registered, its Vite plugin answers this very specifier with the config it\n * resolved, options included (@see integration.ts). Where it is not, this file is what resolves:\n * the same values from the build's environment and its content snapshot.\n *\n * WHY A REAL MODULE. The components imported the virtual `bettercms:config`, which only the\n * integration can resolve. A site that registers it conditionally (ScrewFast:\n * `...(bcmsReady ? [bettercms({})] : [])`) could not compile a page that imported `<BcmsBlocks>`,\n * so `<BcmsSections>` was given no block renderer there and a section added in the editor had\n * nothing to draw it.\n *\n * No `node:fs` import: this is bundled into a server render, an edge runtime's included. Read on\n * use, never once at module scope: a server's environment and snapshot are the run's.\n */\nimport { readPublishedContent } from \"@bettercms-ai/sdk\";\nimport type { PublicConfig } from \"./config.js\";\n\n/**\n * The platform's canonical media host, matching `@bettercms-ai/image-url`'s own\n * `DEFAULT_BASE_URL` and the `CDN_URL` the backend mints asset URLs on.\n *\n * This used to be `media.bettercms.ai`. Both hostnames route to the backend — they are both in\n * `$bcms_api_host` in infra/nginx/backend-lb.conf — so nothing was broken, but a site built by\n * this integration disagreed with the URL the server itself hands out for the same asset. That\n * splits the CDN cache for identical bytes and makes an asset URL's origin depend on which code\n * path produced it, which is the exact drift image-url was moved off this host to end.\n *\n * A project that serves media from the API origin instead (`useCdn: false`) overrides this with\n * `PUBLIC_BCMS_MEDIA_URL` or the `mediaUrl` option.\n */\nexport const DEFAULT_MEDIA_URL = \"https://cdn.bettercms.ai\";\nexport const DRAFT_COOKIE = \"bcms-draft\";\n/** The specifier the components import, and the integration's plugin answers. */\nexport const PUBLIC_CONFIG_ID = \"@bettercms-ai/astro/public-config\";\n\n/** The server's runtime env first (hosting injects it), then the build's. */\nfunction env(name: string): string | undefined {\n const fromProcess = typeof process !== \"undefined\" ? process.env?.[name] : undefined;\n if (fromProcess) return fromProcess;\n try {\n return ((import.meta.env as Record<string, string | undefined> | undefined)?.[name]) || undefined;\n } catch {\n return undefined;\n }\n}\n\nconst trim = (url: string): string => url.replace(/\\/+$/, \"\");\nconst text = (value: unknown): string | undefined => (typeof value === \"string\" && value ? value : undefined);\n\n/**\n * The config with no integration: `PUBLIC_BCMS_*`, then the snapshot. Never throws: with neither,\n * `apiUrl` and `workspace` are empty, which a build that never talks to the API renders with.\n */\nexport function fallbackConfig(): PublicConfig {\n const snapshot = (readPublishedContent() ?? {}) as { apiUrl?: unknown; workspace?: unknown; projectId?: unknown };\n return {\n apiUrl: trim(env(\"PUBLIC_BCMS_API_URL\") ?? text(snapshot.apiUrl) ?? \"\"),\n workspace: env(\"PUBLIC_BCMS_WORKSPACE\") ?? text(snapshot.workspace) ?? \"\",\n projectId: env(\"PUBLIC_BCMS_PROJECT_ID\") ?? text(snapshot.projectId),\n mediaUrl: trim(env(\"PUBLIC_BCMS_MEDIA_URL\") ?? DEFAULT_MEDIA_URL),\n studioUrl: env(\"PUBLIC_BCMS_STUDIO_URL\"),\n draftCookie: DRAFT_COOKIE,\n };\n}\n\nconst KEYS = [\"apiUrl\", \"workspace\", \"projectId\", \"mediaUrl\", \"studioUrl\", \"draftCookie\"] as const;\n\nexport const config = Object.defineProperties(\n {},\n Object.fromEntries(KEYS.map((key) => [key, { enumerable: true, get: () => fallbackConfig()[key] }])),\n) as PublicConfig;\nexport default config;\n","/**\n * `bettercms()` — the Astro integration.\n *\n * - Registers two virtual modules:\n * `bettercms:client` → server runtime (client + typed loaders) — has the key.\n * `bettercms:config` → browser-safe public config — no secrets.\n * - Adds the draft-mode middleware and injects the draft enable/disable routes.\n */\nimport type { AstroIntegration } from \"astro\";\nimport {\n resolveConfig,\n toPublicConfig,\n type BetterCMSAstroOptions,\n type ResolvedConfig,\n} from \"./config.js\";\n\nimport { PUBLIC_CONFIG_ID } from \"./public-config.js\";\n\nconst CLIENT_ID = \"bettercms:client\";\nconst CONFIG_ID = \"bettercms:config\";\nconst RESOLVED = (id: string) => `\\0${id}`;\n\nfunction configModule(cfg: ResolvedConfig): string {\n return `export const config = ${JSON.stringify(toPublicConfig(cfg))};\\nexport default config;\\n`;\n}\n\nfunction clientModule(cfg: ResolvedConfig, buildCache: boolean): string {\n // The key is optional: published reads are addressed by the inlined projectId. When one is\n // set it is read from the server's env at run time (a server-output site gets it from its\n // host, not the build), falling back to the build's env; it never lands in a client bundle.\n return [\n `import { createRuntime } from \"@bettercms-ai/astro/runtime\";`,\n `const runtime = createRuntime({`,\n ` apiUrl: ${JSON.stringify(cfg.apiUrl)},`,\n ` workspace: ${JSON.stringify(cfg.workspace)},`,\n ` projectId: ${JSON.stringify(cfg.projectId)},`,\n ` apiKey: globalThis.process?.env?.BCMS_API_KEY ?? import.meta.env.BCMS_API_KEY,`,\n ` buildCache: ${JSON.stringify(buildCache)},`,\n `});`,\n `export const client = runtime.client;`,\n `export const getClient = runtime.getClient;`,\n `export const loadEntry = runtime.loadEntry;`,\n `export const loadEntries = runtime.loadEntries;`,\n `export const loadPage = runtime.loadPage;`,\n `export const loadComponents = runtime.loadComponents;`,\n `export const loadForms = runtime.loadForms;`,\n `export const loadLayout = runtime.loadLayout;`,\n `export default runtime;`,\n ].join(\"\\n\");\n}\n\nfunction virtualPlugin(cfg: ResolvedConfig, buildCache: () => boolean) {\n return {\n name: \"@bettercms-ai/astro:virtual\",\n // Before Vite's own resolver: `PUBLIC_CONFIG_ID` names a real file of this package, and a\n // plugin that runs after the resolver is never asked about a specifier it already resolved.\n enforce: \"pre\" as const,\n resolveId(id: string) {\n if (id === CLIENT_ID || id === CONFIG_ID) return RESOLVED(id);\n // The components' own import: answered with the config resolved here, options included.\n // Without this plugin it resolves to the package's file, which reads the environment.\n if (id === PUBLIC_CONFIG_ID) return RESOLVED(CONFIG_ID);\n return null;\n },\n load(id: string) {\n if (id === RESOLVED(CONFIG_ID)) return configModule(cfg);\n if (id === RESOLVED(CLIENT_ID)) return clientModule(cfg, buildCache());\n return null;\n },\n };\n}\n\nexport function bettercms(options: BetterCMSAstroOptions = {}): AstroIntegration {\n /**\n * Whether identical delivery GETs may be collapsed for the life of the process.\n *\n * Read lazily by the virtual module (see `astro:config:done` below) because only the\n * FINAL config settles `output` and `adapter` — another integration can add an adapter\n * after `astro:config:setup` runs.\n */\n const buildCache = { on: false, command: \"\", resolved: false };\n /** Set in `astro:config:done`, re-run in `astro:build:start` — see both hooks below. */\n let decide: (() => void) | undefined;\n\n return {\n name: \"@bettercms-ai/astro\",\n hooks: {\n \"astro:config:setup\": ({ command, updateConfig, addMiddleware, injectRoute, logger }) => {\n const cfg = resolveConfig(options);\n buildCache.command = command;\n\n updateConfig({ vite: { plugins: [virtualPlugin(cfg, () => buildCache.on)] } });\n\n if (cfg.draft) {\n addMiddleware({ entrypoint: \"@bettercms-ai/astro/middleware\", order: \"pre\" });\n injectRoute({\n pattern: \"/api/bcms/draft/enable\",\n entrypoint: \"@bettercms-ai/astro/draft-enable\",\n prerender: false,\n });\n injectRoute({\n pattern: \"/api/bcms/draft/disable\",\n entrypoint: \"@bettercms-ai/astro/draft-disable\",\n prerender: false,\n });\n logger.info(\"draft mode enabled — /api/bcms/draft/{enable,disable} injected\");\n }\n },\n\n /**\n * Decide whether the build-time request cache is safe, on the FINAL config.\n *\n * A fully static build renders every route from one published dataset, so identical\n * delivery GETs can collapse to a single round-trip. Without that a large site\n * re-fetched its nav/settings/collections once PER ROUTE and exhausted the delivery\n * rate limit mid-build — a 429 the build could not avoid.\n *\n * `command === \"build\"` is NOT sufficient to detect that, and getting this wrong is\n * far worse than the problem it solves: Astro also reports `command === \"build\"` when\n * it compiles `output: \"server\"`, whose bundle then runs as a LONG-LIVED process. A\n * module-lifetime cache there would pin the first response for every URL and serve it\n * to unrelated visitors until the process restarted — permanent stale content on a\n * production site.\n *\n * `buildOutput` (Astro 5) is Astro's OWN answer to \"will this emit a server?\", so it\n * is what we trust when present. Inferring it from `config.adapter` is not equivalent:\n * `setAdapter()` is documented as being called from inside `astro:config:done`, so an\n * adapter integration whose hook runs after this one would still read as absent here.\n *\n * On Astro 4 there is no `buildOutput`, and there `output: \"static\"` genuinely\n * prerenders everything — on-demand rendering required `\"server\"` or `\"hybrid\"`, both\n * of which fail this check. The adapter test stays as a second line of defence.\n */\n \"astro:config:done\": ({ config, logger, ...rest }) => {\n // Astro >= 5 only; undefined on 4, hence the fallback rather than a bare read.\n const buildOutput = (rest as { buildOutput?: \"static\" | \"server\" }).buildOutput;\n const staticOutput =\n buildOutput !== undefined\n ? buildOutput === \"static\"\n : config.output === \"static\" && !config.adapter;\n\n buildCache.on = buildCache.command === \"build\" && staticOutput;\n // Re-checked in astro:build:start below; only announce once it is final.\n decide = () => {\n const late = (rest as { buildOutput?: \"static\" | \"server\" }).buildOutput;\n const stillStatic =\n late !== undefined ? late === \"static\" : config.output === \"static\" && !config.adapter;\n buildCache.on = buildCache.command === \"build\" && stillStatic;\n buildCache.resolved = true;\n if (buildCache.on) {\n logger.info(\"static build — identical content reads will be fetched once\");\n }\n };\n },\n\n /**\n * Last word on the cache, and the reason this is not decided in `astro:config:done`\n * alone: Astro runs every integration's `config:done` in order, and `setAdapter()` is\n * called FROM that hook. An adapter registered by an integration ordered after this one\n * would flip the project to on-demand rendering *after* we had already recorded\n * \"static\" — and the stale `true` would ship a module-lifetime cache into a\n * long-lived server. `astro:build:start` runs after all of them and before Vite loads\n * the virtual module, so re-reading here is the value that is actually true.\n *\n * It never fires for `dev` or `preview`, which is exactly right: both must stay\n * uncached, and `buildCache.on` is only ever set to true under `command === \"build\"`.\n */\n \"astro:build:start\": () => {\n decide?.();\n },\n },\n };\n}\n\nexport default bettercms;\n","/**\n * Runtime for @bettercms-ai/astro — the code the `bettercms:client` virtual\n * module binds to. Provides a published client, per-request draft client\n * selection, typed loaders, and signed-cookie helpers for draft mode.\n *\n * Read-only; no I/O at import time. Safe in Node and edge runtimes (uses Web\n * Crypto + global fetch only).\n */\nimport { createClient, type BetterCMSReadClient } from \"@bettercms-ai/sdk\";\nimport type {\n DeliveredLayout,\n DeliveryComponent,\n DeliveryEntry,\n DeliveryList,\n DeliveryPage,\n} from \"@bettercms-ai/types\";\n\nexport interface RuntimeConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n /** Server-only delivery key (a `content:read:draft` key for drafts). */\n apiKey?: string;\n /**\n * Collapse identical delivery GETs for the life of the process. Set by the integration\n * when Astro's `command === \"build\"` — a static build renders every route from one\n * published dataset, so a repeat read cannot legitimately differ.\n *\n * Never set for `dev` or SSR, where each request must see current content.\n */\n buildCache?: boolean;\n}\n\n/** Per-request draft state, set on `Astro.locals.bcms` by the middleware. */\nexport interface BcmsLocals {\n draft: boolean;\n token?: string;\n}\n\ninterface HasLocals {\n locals: { bcms?: BcmsLocals };\n}\n\nexport interface BetterCMSRuntime {\n /** Published-perspective client (use for SSG / non-draft reads). */\n client: BetterCMSReadClient;\n /** Pick the right client for a request (draft client when draft mode is on). */\n getClient(astro?: HasLocals): BetterCMSReadClient;\n loadEntry<T = Record<string, unknown>>(\n astro: HasLocals,\n slug: string,\n opts?: Parameters<BetterCMSReadClient[\"getEntry\"]>[1],\n ): Promise<DeliveryEntry<T>>;\n loadEntries<T = Record<string, unknown>>(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listEntries\"]>[0],\n ): Promise<DeliveryList<DeliveryEntry<T>>>;\n loadPage(astro: HasLocals, slug: string): Promise<DeliveryPage | null>;\n /** Component definitions for `<BcmsBlocks components={…}>` — draft-side in draft mode. */\n loadComponents(astro: HasLocals): Promise<DeliveryComponent[]>;\n loadLayout(astro: HasLocals): Promise<DeliveredLayout | null>;\n loadForms(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listForms\"]>[0],\n ): Promise<Awaited<ReturnType<BetterCMSReadClient[\"listForms\"]>>>;\n}\n\nexport function createRuntime(config: RuntimeConfig): BetterCMSRuntime {\n const { buildCache, ...clientConfig } = config;\n // One client for the whole build, so the cache below is shared across every route\n // rather than rebuilt per render.\n const published = createClient({ ...clientConfig, perspective: \"published\", cache: buildCache });\n\n const getClient = (astro?: HasLocals): BetterCMSReadClient => {\n const bcms = astro?.locals?.bcms;\n if (bcms?.draft) {\n // Explicitly uncached: a draft read exists to show the edit that just happened.\n return createClient({\n ...clientConfig,\n perspective: \"drafts\",\n previewToken: bcms.token,\n cache: false,\n });\n }\n return published;\n };\n\n return {\n client: published,\n getClient,\n loadEntry: (astro, slug, opts) => getClient(astro).getEntry(slug, opts),\n loadEntries: (astro, opts) => getClient(astro).listEntries(opts),\n loadPage: (astro, slug) => getClient(astro).getPage(slug),\n loadComponents: (astro) => getClient(astro).listComponents(),\n loadLayout: (astro) => getClient(astro).getLayout(),\n loadForms: (astro, opts) => getClient(astro).listForms(opts),\n };\n}\n\n/**\n * Build the live SSE URL for `<BcmsLive>`. The key is a query param because\n * browser EventSource cannot send headers — use a content:read key.\n *\n * `depth` sets the hydration depth of the document the stream pushes (default 1,\n * matching the delivery API). `drafts` streams autosaves so a preview surface\n * repaints as the editor types — it needs a `content:read:draft` key, and the\n * server ignores the flag for any other key rather than failing the connection.\n * Never enable it on a public site.\n */\nexport function liveSrc(opts: {\n apiUrl: string;\n workspace: string;\n apiKey: string;\n depth?: 0 | 1 | 2;\n drafts?: boolean;\n}): string {\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const q = new URLSearchParams({ key: opts.apiKey });\n if (opts.depth !== undefined) q.set(\"depth\", String(opts.depth));\n if (opts.drafts) q.set(\"drafts\", \"1\");\n return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;\n}\n\n/**\n * The published content snapshot, read at request time on a server BetterCMS hosts (it rewrites the\n * file on every publish) and from `bcms-content.json` at build time. Read content through this —\n * never `import` bcms-content.json or `import.meta.glob` it: a bundled copy is frozen at build, and\n * the platform has to rebuild the whole site for every publish to change it.\n */\nexport { readPublishedContent, publishedEntry, type PublishedContent } from \"@bettercms-ai/sdk\";\n\n// ── Signed-cookie helpers (HMAC-SHA256, base64url) ──────────────────────────\n\nconst enc = new TextEncoder();\n\nfunction base64url(bytes: ArrayBuffer): string {\n const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));\n return b.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function hmac(value: string, secret: string): Promise<string> {\n const key = await crypto.subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n return base64url(await crypto.subtle.sign(\"HMAC\", key, enc.encode(value)));\n}\n\n/** Sign a value as `value.sig` for tamper-evident cookie storage. */\nexport async function signValue(value: string, secret: string): Promise<string> {\n return `${value}.${await hmac(value, secret)}`;\n}\n\n/** Verify a signed value, returning the original value or null if tampered. */\nexport async function verifyValue(signed: string, secret: string): Promise<string | null> {\n const idx = signed.lastIndexOf(\".\");\n if (idx < 0) return null;\n const value = signed.slice(0, idx);\n const sig = signed.slice(idx + 1);\n const expected = await hmac(value, secret);\n // Constant-time-ish compare via length + char accumulation.\n if (sig.length !== expected.length) return null;\n let diff = 0;\n for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);\n return diff === 0 ? value : null;\n}\n\n/**\n * The draft-cookie signing secret, `process.env` FIRST.\n *\n * Vite may INLINE `import.meta.env.BCMS_DRAFT_SECRET` at build time, and the value present\n * when the customer's site is built is not the value the hosting container is given at start\n * — BetterCMS derives it per project and injects it as a real env var. Reading the runtime\n * env first is what makes managed hosting work without asking anyone to rebuild. The\n * `import.meta.env` read stays as the fallback for `astro dev` and for adapters with no\n * `process` (guarded, since touching `import.meta.env` outside Vite throws).\n */\nexport function draftSecret(): string | undefined {\n const fromProcess =\n typeof process !== \"undefined\" ? (process.env?.BCMS_DRAFT_SECRET as string | undefined) : undefined;\n if (fromProcess) return fromProcess;\n try {\n return import.meta.env?.BCMS_DRAFT_SECRET as string | undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Only same-origin relative paths. Blocks \"//evil.com\", \"https://evil.com\", etc. */\nexport function safeRedirectPath(to: string): string {\n return to.startsWith(\"/\") && !to.startsWith(\"//\") ? to : \"/\";\n}\n\n/** Decode a preview-token JWT payload (UNVERIFIED) to read its `entrySlug`. */\nexport function decodeTokenSlug(token: string): string | null {\n try {\n const part = token.split(\".\")[1];\n if (!part) return null;\n const json = JSON.parse(\n atob(part.replace(/-/g, \"+\").replace(/_/g, \"/\")),\n ) as { entrySlug?: string };\n return json.entrySlug ?? null;\n } catch {\n return null;\n }\n}\n","/**\n * @bettercms-ai/astro — the BetterCMS adapter for Astro.\n *\n * import { defineConfig } from \"astro/config\";\n * import bettercms from \"@bettercms-ai/astro\";\n *\n * export default defineConfig({\n * integrations: [bettercms({ apiUrl, workspace })],\n * output: \"server\",\n * });\n *\n * Then in any page: `import { loadPage } from \"bettercms:client\"`.\n */\nexport { bettercms, default } from \"./integration.js\";\nexport {\n resolveConfig,\n DRAFT_COOKIE,\n type BetterCMSAstroOptions,\n type PublicConfig,\n type ResolvedConfig,\n} from \"./config.js\";\nexport { liveSrc } from \"./runtime.js\";\n// C3/C4: a site's own section root declares that it paints block.style.\nexport { RESPONSIVE_CSS } from \"@bettercms-ai/types\";\n// A `link` value (or a button's `href`) is a string or a link object: `linkHref` reads either as the href.\nexport { linkHref, type LinkValue } from \"@bettercms-ai/types\";\nexport { sectionRootAttrs, type SectionRootAttrs, type SectionRootBlock } from \"./section-root.js\";\nexport { fieldAttrs, fieldStyleOf, type FieldAttrs, entryAttrs, entryField, type EntryRef, type EntryTransform, type EntryFieldAttrs } from \"./field-attrs.js\";\nexport { sectionColumnAttrs, type SectionColumnAttrs } from \"./section-root.js\";\nexport type { BcmsLocals, BetterCMSRuntime, RuntimeConfig } from \"./runtime.js\";\n\n// Image URL builder, re-exported for `<BcmsImage>` and host code.\nexport {\n default as imageUrlBuilder,\n imageUrl,\n imageValueUrl,\n imageValueObjectPosition,\n} from \"@bettercms-ai/image-url\";\nexport type {\n ImageUrlBuilder,\n ImageSource,\n ImageFormat,\n ImageFit,\n ImageCrop,\n ImageFocal,\n ImageValueUrlOptions,\n} from \"@bettercms-ai/image-url\";\n\n// Delivery read types, re-exported for typing page/entry data.\nexport type {\n Perspective,\n DeliveryEntry,\n DeliveryPage,\n DeliveryList,\n PageMetaJson,\n SiteSeoDefaults,\n ContentBlock,\n DeliveredLayout,\n} from \"@bettercms-ai/types\";\nexport type { DeliveryForm, DeliveryFormField } from \"@bettercms-ai/sdk\";\n\n// SEO (FLO-302): page-over-site meta resolver. In a page's frontmatter,\n// `const seo = resolveSeo(page, siteDefaults)`, then map `seo.*` into `<head>`.\nexport { resolveSeo } from \"@bettercms-ai/sdk\";\nexport type { SeoInput, ResolvedSeo } from \"@bettercms-ai/sdk\";\n","/**\n * C3/C4 — `sectionRootAttrs(block)`: the attributes a site's OWN section root spreads so the\n * BetterCMS editor offers that section's spacing and style handles.\n *\n * <section class=\"hero\" {...sectionRootAttrs(block)}>…</section>\n *\n * The editor shows those handles on a site-rendered page only when EVERY root the site stamps\n * with `data-bcms-block` also carries `data-bcms-style`, which is the site's promise that \"this\n * root's inline style is `blockStyleToCss(block.style)`\" — the same declarations `<BcmsBlocks>`\n * puts on its own wrapper. A root that stamps the block without applying its style would get a\n * capsule that paints the canvas and never ships, so the marker must travel with the style.\n *\n * The marker is present even when the style is EMPTY (then there is no `style` attribute at all):\n * an unstyled section is exactly the one an author is about to give its first spacing edit.\n */\nimport { blockStyleToColumnCss, blockStyleToCss, hideOnAttr, type BlockStyle } from \"@bettercms-ai/types\";\n\nexport interface SectionRootBlock {\n id?: string | null;\n style?: BlockStyle | Record<string, unknown> | null;\n}\n\nexport interface SectionRootAttrs {\n \"data-bcms-block\"?: string;\n \"data-bcms-style\": \"\";\n /** `\"mobile tablet\"` when `style.hideOn` hides it there; the site ships `RESPONSIVE_CSS` for it. */\n \"data-bcms-hide\"?: string;\n /** `blockStyleToCss(block.style)` as a CSS string; absent when the block has no style. */\n style?: string;\n}\n\n/** `paddingTop` → `padding-top`, joined the way `<BcmsBlocks>` writes its wrapper's attribute. */\nfunction cssText(css: Record<string, string | number>): string {\n return Object.entries(css)\n .map(([k, v]) => `${k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}:${v}`)\n .join(\";\");\n}\n\nexport function sectionRootAttrs(block: SectionRootBlock | null | undefined): SectionRootAttrs {\n const css = blockStyleToCss((block?.style ?? undefined) as BlockStyle | undefined);\n const id = typeof block?.id === \"string\" && block.id !== \"\" ? block.id : undefined;\n const hide = hideOnAttr(block?.style);\n return {\n ...(id !== undefined ? { \"data-bcms-block\": id } : {}),\n \"data-bcms-style\": \"\",\n ...(hide ? { \"data-bcms-hide\": hide } : {}),\n ...(css ? { style: cssText(css) } : {}),\n };\n}\n\nexport interface SectionColumnAttrs {\n \"data-bcms-column\": \"\";\n /** `blockStyleToColumnCss(block.style)` as a CSS string; absent when the section has no column. */\n style?: string;\n}\n\n/**\n * C3 — `sectionColumnAttrs(block)`: the attributes a site's own INNER container spreads so the\n * section's content WIDTH (`style.contentWidth`, placed by `contentAlign`) ships.\n *\n * <section {...sectionRootAttrs(block)}>\n * <div class=\"container\" {...sectionColumnAttrs(block)}>…</div>\n * </section>\n *\n * The width is the column BetterCMS draws as `.bcms-block-inner` inside the section's styled box —\n * never on the root itself, whose background must stay full-bleed. `data-bcms-column` is the\n * site's promise that this box's inline style is the column; the editor offers the width handle\n * on a site-rendered page only where every styled root holds one. Max-width plus left/right\n * margins, so the container keeps its own vertical margins and, with no column, its own width.\n */\nexport function sectionColumnAttrs(block: SectionRootBlock | null | undefined): SectionColumnAttrs {\n const css = blockStyleToColumnCss((block?.style ?? undefined) as BlockStyle | undefined);\n return { \"data-bcms-column\": \"\", ...(css ? { style: cssText(css) } : {}) };\n}\n","/**\n * C3 — `fieldAttrs(path, fieldStyles)`: the attributes a site's OWN element spreads to bind a PAGE\n * FIELD and apply that field's stored style, so the BetterCMS editor offers its width and spacing\n * handles and its Align / Size / Weight controls.\n *\n * ---\n * const page = await loadPage(Astro, \"home\");\n * ---\n * <h1 class=\"title\" {...fieldAttrs(\"hero-title\", page?.fieldStyles)}>{entry.data[\"hero-title\"]}</h1>\n *\n * `data-bcms-style` is the site's promise that \"this element's inline style is\n * `fieldStyleToCss(fieldStyles[path])`\". The editor offers a field's handles only on an element that\n * carries it: a handle on one that does not would paint the canvas and never ship. The marker is\n * present even when the field has no style (then there is no `style` attribute at all, byte for\n * byte the element it was): an unstyled field is the one an author is about to give its first edit.\n * Twin: `fieldAttrs` in @bettercms-ai/next (a React style object there).\n */\nimport { fieldStyleCssText, type FieldStyle, type FieldStyles } from \"@bettercms-ai/types\";\n\nexport interface FieldAttrs {\n \"data-bcms-field\": string;\n /** The marker's revision: \"2\" promises `size` (zoom) too. Section roots keep \"\". */\n \"data-bcms-style\": \"2\";\n /** `fieldStyleToCss(fieldStyles[path])` as a CSS string; absent when the field has no style. */\n style?: string;\n}\n\n/** The stored style for `path`, by OWN key only — a path like `constructor` never reads the prototype. */\nexport function fieldStyleOf(path: string, fieldStyles: FieldStyles | Record<string, unknown> | null | undefined): FieldStyle | undefined {\n if (!fieldStyles || typeof fieldStyles !== \"object\" || !Object.prototype.hasOwnProperty.call(fieldStyles, path)) return undefined;\n const style = (fieldStyles as Record<string, unknown>)[path];\n return style && typeof style === \"object\" ? (style as FieldStyle) : undefined;\n}\n\nexport function fieldAttrs(path: string, fieldStyles?: FieldStyles | Record<string, unknown> | null): FieldAttrs {\n const style = fieldStyleCssText(fieldStyleOf(path, fieldStyles));\n return { \"data-bcms-field\": path, \"data-bcms-style\": \"2\", ...(style ? { style } : {}) };\n}\n\n/**\n * P7 — `entryAttrs(entry)` and `entryField(entry, path)`: the attributes a site's OWN card spreads to\n * declare a collection ENTRY, so the editor binds it without guessing from the rendered text.\n *\n * <article {...entryAttrs(post)}><h3 {...entryField(post, \"title\")}>{post.fields.title}</h3></article>\n *\n * The field address is ABSOLUTE (`entry:<id>:<path>`). With a `transform` the value is shown\n * changed (a date formatted, text truncated), so it is declared as `data-bcms-entry-from`: the\n * editor knows where it is but cannot re-render it. Twin: same helpers in @bettercms-ai/next.\n */\nexport type EntryRef = { id?: string } | string;\nexport type EntryTransform = \"format\" | \"truncate\" | \"rendition\" | \"route\";\n/** One key is present: `data-bcms-entry-field`, or `data-bcms-entry-from` when transformed; none without an id. */\nexport interface EntryFieldAttrs {\n \"data-bcms-entry-field\"?: string;\n \"data-bcms-entry-from\"?: string;\n}\n\nfunction entryIdOf(entry: EntryRef): string | null {\n const id = typeof entry === \"string\" ? entry : entry?.id;\n return typeof id === \"string\" && id.trim() !== \"\" ? id : null;\n}\n\n/** An entry with no id (a snapshot written before ids were kept) renders unbound, never fails the build. */\nexport function entryAttrs(entry: EntryRef): { \"data-bcms-entry\"?: string } {\n const id = entryIdOf(entry);\n return id ? { \"data-bcms-entry\": id } : {};\n}\n\nexport function entryField(entry: EntryRef, path: string, opts?: { transform?: EntryTransform }): EntryFieldAttrs {\n const id = entryIdOf(entry);\n if (typeof path !== \"string\" || path.trim() === \"\") throw new Error(`entryField: no field path given for entry ${id ?? \"(no id)\"}`);\n if (!id) return {};\n const address = `entry:${id}:${path}`;\n return opts?.transform ? { \"data-bcms-entry-from\": `${address}|${opts.transform}` } : { \"data-bcms-entry-field\": address };\n}\n"],"mappings":"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","names":["snapshot","env","config","readPublishedContent","default"]}
1
+ {"version":3,"sources":["../src/config.ts","../src/public-config.ts","../src/integration.ts","../src/runtime.ts","../src/index.ts","../src/section-root.ts","../src/field-attrs.ts"],"sourcesContent":["/**\n * Resolved configuration shared by the integration, runtime, middleware and\n * draft routes. Public values are safe to ship to the browser; the API key and\n * draft secret are read from server env at runtime and never live here.\n *\n * Resolution order is options → `PUBLIC_BCMS_*` env → the build snapshot. Config-time\n * only: this module is reached exclusively through `dist/index.js`, which only an\n * `astro.config.*` imports, so the `node:fs` read below never enters a client bundle.\n * Browser code gets the `bettercms:config` virtual module instead.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nimport { resolveBcmsConfig } from \"@bettercms-ai/sdk/config\";\n\nimport { DRAFT_COOKIE } from \"./public-config.js\";\n\nexport { DRAFT_COOKIE };\nconst SNAPSHOT_FILE = \"bcms-content.json\";\n\nexport interface BetterCMSAstroOptions {\n /** Backend base, e.g. `https://api.bettercms.ai`. Falls back to `PUBLIC_BCMS_API_URL`. */\n apiUrl?: string;\n /** Workspace slug. Falls back to `PUBLIC_BCMS_WORKSPACE`. */\n workspace?: string;\n /** Optional project scope for forms. Falls back to `PUBLIC_BCMS_PROJECT_ID`. */\n projectId?: string;\n /** Media host for the image-url builder. Defaults to the prod media host. */\n mediaUrl?: string;\n /** Dashboard base for visual-editing deep links. */\n studioUrl?: string;\n /** Enable the draft-mode middleware + injected draft routes. Default true. */\n draft?: boolean;\n}\n\n/** Browser-safe config — exposed via the `bettercms:config` virtual module. */\nexport interface PublicConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n mediaUrl: string;\n studioUrl?: string;\n draftCookie: string;\n}\n\nexport interface ResolvedConfig extends PublicConfig {\n draft: boolean;\n}\n\n/** The fields of the deploy Action's content snapshot that identify the site. */\ninterface Snapshot {\n apiUrl?: string;\n workspace?: string;\n projectId?: string;\n}\n\n/**\n * Read `bcms-content.json` — the snapshot the deploy Action writes next to the build,\n * BEFORE `astro build` runs — which already carries the workspace this site belongs to.\n *\n * This is what lets `bettercms()` work with no arguments and no env at all. Not reading it\n * was the whole bug: a hosted build had the answer sitting in its own working directory and\n * threw anyway, and every starter template had to reimplement this by hand to survive.\n *\n * Lazy: only consulted when options and env both miss, so the configured path does no I/O.\n * Absent/corrupt file ⇒ `{}`, and the caller raises the actionable error below.\n */\nlet snapshotCache: Snapshot | undefined;\nfunction snapshot(cwd: string): Snapshot {\n if (snapshotCache === undefined) {\n try {\n snapshotCache = JSON.parse(readFileSync(resolve(cwd, SNAPSHOT_FILE), \"utf8\")) as Snapshot;\n } catch {\n snapshotCache = {};\n }\n }\n return snapshotCache;\n}\n\n/** Test seam — the snapshot is memoised for the life of the process. */\nexport function resetSnapshotCache(): void {\n snapshotCache = undefined;\n}\n\nconst required = (name: string, envVar: string, extra: string) =>\n new Error(\n `[@bettercms-ai/astro] \\`${name}\\` is required. Pass \\`bettercms({ ${name} })\\`, set ` +\n `${envVar}, or ${extra}\\n` +\n `On BetterCMS hosting these are injected automatically — if this failed in a hosted ` +\n `build, the repository is running an outdated deploy workflow: publish once, or save ` +\n `Hosting settings, to refresh it.`,\n );\n\n/**\n * Merge integration options with `PUBLIC_BCMS_*` env and the build snapshot, validating the\n * essentials. Precedence: explicit options → env → snapshot.\n */\nexport function resolveConfig(\n options: BetterCMSAstroOptions,\n env: Record<string, string | undefined> = process.env,\n cwd: string = process.cwd(),\n): ResolvedConfig {\n // The precedence every adapter shares (@bettercms-ai/sdk/config); the words of the error are Astro's.\n const { config, missing } = resolveBcmsConfig({ options, env, prefixes: [\"PUBLIC_\"], snapshot: () => snapshot(cwd) });\n // No default origin on purpose: guessing one produces a site that builds cleanly against\n // the wrong backend, which is far harder to notice than this error.\n if (missing.includes(\"apiUrl\")) throw required(\"apiUrl\", \"PUBLIC_BCMS_API_URL\", `run the content fetch so ${SNAPSHOT_FILE} is present.`);\n if (missing.includes(\"workspace\")) {\n throw required(\"workspace\", \"PUBLIC_BCMS_WORKSPACE\", `run the content fetch so ${SNAPSHOT_FILE} is present.`);\n }\n return { ...config, draftCookie: DRAFT_COOKIE, draft: options.draft ?? true };\n}\n\nexport function toPublicConfig(c: ResolvedConfig): PublicConfig {\n const { draft: _draft, ...pub } = c;\n return pub;\n}\n","/**\n * The public config the shipped components read (`BcmsImage`, `BcmsForm`).\n *\n * Where `bettercms()` is registered, its Vite plugin answers this very specifier with the config it\n * resolved, options included (@see integration.ts). Where it is not, this file is what resolves:\n * the same values from the build's environment and its content snapshot.\n *\n * WHY A REAL MODULE. The components imported the virtual `bettercms:config`, which only the\n * integration can resolve. A site that registers it conditionally (ScrewFast:\n * `...(bcmsReady ? [bettercms({})] : [])`) could not compile a page that imported `<BcmsBlocks>`,\n * so `<BcmsSections>` was given no block renderer there and a section added in the editor had\n * nothing to draw it.\n *\n * No `node:fs` import: this is bundled into a server render, an edge runtime's included. Read on\n * use, never once at module scope: a server's environment and snapshot are the run's.\n */\nimport { readPublishedContent } from \"@bettercms-ai/sdk\";\nimport type { PublicConfig } from \"./config.js\";\n\n/**\n * The platform's canonical media host, matching `@bettercms-ai/image-url`'s own\n * `DEFAULT_BASE_URL` and the `CDN_URL` the backend mints asset URLs on.\n *\n * This used to be `media.bettercms.ai`. Both hostnames route to the backend — they are both in\n * `$bcms_api_host` in infra/nginx/backend-lb.conf — so nothing was broken, but a site built by\n * this integration disagreed with the URL the server itself hands out for the same asset. That\n * splits the CDN cache for identical bytes and makes an asset URL's origin depend on which code\n * path produced it, which is the exact drift image-url was moved off this host to end.\n *\n * A project that serves media from the API origin instead (`useCdn: false`) overrides this with\n * `PUBLIC_BCMS_MEDIA_URL` or the `mediaUrl` option.\n */\nexport { DEFAULT_MEDIA_URL } from \"@bettercms-ai/sdk/config\";\nexport { DRAFT_COOKIE } from \"@bettercms-ai/sdk/draft\";\nimport { DEFAULT_MEDIA_URL } from \"@bettercms-ai/sdk/config\";\nimport { DRAFT_COOKIE } from \"@bettercms-ai/sdk/draft\";\n/** The specifier the components import, and the integration's plugin answers. */\nexport const PUBLIC_CONFIG_ID = \"@bettercms-ai/astro/public-config\";\n\n/** The server's runtime env first (hosting injects it), then the build's. */\nfunction env(name: string): string | undefined {\n const fromProcess = typeof process !== \"undefined\" ? process.env?.[name] : undefined;\n if (fromProcess) return fromProcess;\n try {\n return ((import.meta.env as Record<string, string | undefined> | undefined)?.[name]) || undefined;\n } catch {\n return undefined;\n }\n}\n\nconst trim = (url: string): string => url.replace(/\\/+$/, \"\");\nconst text = (value: unknown): string | undefined => (typeof value === \"string\" && value ? value : undefined);\n\n/**\n * The config with no integration: `PUBLIC_BCMS_*`, then the snapshot. Never throws: with neither,\n * `apiUrl` and `workspace` are empty, which a build that never talks to the API renders with.\n */\nexport function fallbackConfig(): PublicConfig {\n const snapshot = (readPublishedContent() ?? {}) as { apiUrl?: unknown; workspace?: unknown; projectId?: unknown };\n return {\n apiUrl: trim(env(\"PUBLIC_BCMS_API_URL\") ?? text(snapshot.apiUrl) ?? \"\"),\n workspace: env(\"PUBLIC_BCMS_WORKSPACE\") ?? text(snapshot.workspace) ?? \"\",\n projectId: env(\"PUBLIC_BCMS_PROJECT_ID\") ?? text(snapshot.projectId),\n mediaUrl: trim(env(\"PUBLIC_BCMS_MEDIA_URL\") ?? DEFAULT_MEDIA_URL),\n studioUrl: env(\"PUBLIC_BCMS_STUDIO_URL\"),\n draftCookie: DRAFT_COOKIE,\n };\n}\n\nconst KEYS = [\"apiUrl\", \"workspace\", \"projectId\", \"mediaUrl\", \"studioUrl\", \"draftCookie\"] as const;\n\nexport const config = Object.defineProperties(\n {},\n Object.fromEntries(KEYS.map((key) => [key, { enumerable: true, get: () => fallbackConfig()[key] }])),\n) as PublicConfig;\nexport default config;\n","/**\n * `bettercms()` — the Astro integration.\n *\n * - Registers two virtual modules:\n * `bettercms:client` → server runtime (client + typed loaders) — has the key.\n * `bettercms:config` → browser-safe public config — no secrets.\n * - Adds the draft-mode middleware and injects the draft enable/disable routes.\n */\nimport type { AstroIntegration } from \"astro\";\nimport {\n resolveConfig,\n toPublicConfig,\n type BetterCMSAstroOptions,\n type ResolvedConfig,\n} from \"./config.js\";\n\nimport { PUBLIC_CONFIG_ID } from \"./public-config.js\";\n\nconst CLIENT_ID = \"bettercms:client\";\nconst CONFIG_ID = \"bettercms:config\";\nconst RESOLVED = (id: string) => `\\0${id}`;\n\nfunction configModule(cfg: ResolvedConfig): string {\n return `export const config = ${JSON.stringify(toPublicConfig(cfg))};\\nexport default config;\\n`;\n}\n\nfunction clientModule(cfg: ResolvedConfig, buildCache: boolean): string {\n // The key is optional: published reads are addressed by the inlined projectId. When one is\n // set it is read from the server's env at run time (a server-output site gets it from its\n // host, not the build), falling back to the build's env; it never lands in a client bundle.\n return [\n `import { createRuntime } from \"@bettercms-ai/astro/runtime\";`,\n `const runtime = createRuntime({`,\n ` apiUrl: ${JSON.stringify(cfg.apiUrl)},`,\n ` workspace: ${JSON.stringify(cfg.workspace)},`,\n ` projectId: ${JSON.stringify(cfg.projectId)},`,\n ` apiKey: globalThis.process?.env?.BCMS_API_KEY ?? import.meta.env.BCMS_API_KEY,`,\n ` buildCache: ${JSON.stringify(buildCache)},`,\n `});`,\n `export const client = runtime.client;`,\n `export const getClient = runtime.getClient;`,\n `export const loadEntry = runtime.loadEntry;`,\n `export const loadEntries = runtime.loadEntries;`,\n `export const loadPage = runtime.loadPage;`,\n `export const loadComponents = runtime.loadComponents;`,\n `export const loadForms = runtime.loadForms;`,\n `export const loadLayout = runtime.loadLayout;`,\n `export default runtime;`,\n ].join(\"\\n\");\n}\n\nfunction virtualPlugin(cfg: ResolvedConfig, buildCache: () => boolean) {\n return {\n name: \"@bettercms-ai/astro:virtual\",\n // Before Vite's own resolver: `PUBLIC_CONFIG_ID` names a real file of this package, and a\n // plugin that runs after the resolver is never asked about a specifier it already resolved.\n enforce: \"pre\" as const,\n resolveId(id: string) {\n if (id === CLIENT_ID || id === CONFIG_ID) return RESOLVED(id);\n // The components' own import: answered with the config resolved here, options included.\n // Without this plugin it resolves to the package's file, which reads the environment.\n if (id === PUBLIC_CONFIG_ID) return RESOLVED(CONFIG_ID);\n return null;\n },\n load(id: string) {\n if (id === RESOLVED(CONFIG_ID)) return configModule(cfg);\n if (id === RESOLVED(CLIENT_ID)) return clientModule(cfg, buildCache());\n return null;\n },\n };\n}\n\nexport function bettercms(options: BetterCMSAstroOptions = {}): AstroIntegration {\n /**\n * Whether identical delivery GETs may be collapsed for the life of the process.\n *\n * Read lazily by the virtual module (see `astro:config:done` below) because only the\n * FINAL config settles `output` and `adapter` — another integration can add an adapter\n * after `astro:config:setup` runs.\n */\n const buildCache = { on: false, command: \"\", resolved: false };\n /** Set in `astro:config:done`, re-run in `astro:build:start` — see both hooks below. */\n let decide: (() => void) | undefined;\n\n return {\n name: \"@bettercms-ai/astro\",\n hooks: {\n \"astro:config:setup\": ({ command, updateConfig, addMiddleware, injectRoute, logger }) => {\n const cfg = resolveConfig(options);\n buildCache.command = command;\n\n updateConfig({ vite: { plugins: [virtualPlugin(cfg, () => buildCache.on)] } });\n\n if (cfg.draft) {\n addMiddleware({ entrypoint: \"@bettercms-ai/astro/middleware\", order: \"pre\" });\n injectRoute({\n pattern: \"/api/bcms/draft/enable\",\n entrypoint: \"@bettercms-ai/astro/draft-enable\",\n prerender: false,\n });\n injectRoute({\n pattern: \"/api/bcms/draft/disable\",\n entrypoint: \"@bettercms-ai/astro/draft-disable\",\n prerender: false,\n });\n logger.info(\"draft mode enabled — /api/bcms/draft/{enable,disable} injected\");\n }\n },\n\n /**\n * Decide whether the build-time request cache is safe, on the FINAL config.\n *\n * A fully static build renders every route from one published dataset, so identical\n * delivery GETs can collapse to a single round-trip. Without that a large site\n * re-fetched its nav/settings/collections once PER ROUTE and exhausted the delivery\n * rate limit mid-build — a 429 the build could not avoid.\n *\n * `command === \"build\"` is NOT sufficient to detect that, and getting this wrong is\n * far worse than the problem it solves: Astro also reports `command === \"build\"` when\n * it compiles `output: \"server\"`, whose bundle then runs as a LONG-LIVED process. A\n * module-lifetime cache there would pin the first response for every URL and serve it\n * to unrelated visitors until the process restarted — permanent stale content on a\n * production site.\n *\n * `buildOutput` (Astro 5) is Astro's OWN answer to \"will this emit a server?\", so it\n * is what we trust when present. Inferring it from `config.adapter` is not equivalent:\n * `setAdapter()` is documented as being called from inside `astro:config:done`, so an\n * adapter integration whose hook runs after this one would still read as absent here.\n *\n * On Astro 4 there is no `buildOutput`, and there `output: \"static\"` genuinely\n * prerenders everything — on-demand rendering required `\"server\"` or `\"hybrid\"`, both\n * of which fail this check. The adapter test stays as a second line of defence.\n */\n \"astro:config:done\": ({ config, logger, ...rest }) => {\n // Astro >= 5 only; undefined on 4, hence the fallback rather than a bare read.\n const buildOutput = (rest as { buildOutput?: \"static\" | \"server\" }).buildOutput;\n const staticOutput =\n buildOutput !== undefined\n ? buildOutput === \"static\"\n : config.output === \"static\" && !config.adapter;\n\n buildCache.on = buildCache.command === \"build\" && staticOutput;\n // Re-checked in astro:build:start below; only announce once it is final.\n decide = () => {\n const late = (rest as { buildOutput?: \"static\" | \"server\" }).buildOutput;\n const stillStatic =\n late !== undefined ? late === \"static\" : config.output === \"static\" && !config.adapter;\n buildCache.on = buildCache.command === \"build\" && stillStatic;\n buildCache.resolved = true;\n if (buildCache.on) {\n logger.info(\"static build — identical content reads will be fetched once\");\n }\n };\n },\n\n /**\n * Last word on the cache, and the reason this is not decided in `astro:config:done`\n * alone: Astro runs every integration's `config:done` in order, and `setAdapter()` is\n * called FROM that hook. An adapter registered by an integration ordered after this one\n * would flip the project to on-demand rendering *after* we had already recorded\n * \"static\" — and the stale `true` would ship a module-lifetime cache into a\n * long-lived server. `astro:build:start` runs after all of them and before Vite loads\n * the virtual module, so re-reading here is the value that is actually true.\n *\n * It never fires for `dev` or `preview`, which is exactly right: both must stay\n * uncached, and `buildCache.on` is only ever set to true under `command === \"build\"`.\n */\n \"astro:build:start\": () => {\n decide?.();\n },\n },\n };\n}\n\nexport default bettercms;\n","/**\n * Runtime for @bettercms-ai/astro — the code the `bettercms:client` virtual\n * module binds to. Provides a published client, per-request draft client\n * selection, typed loaders, and signed-cookie helpers for draft mode.\n *\n * Read-only; no I/O at import time. Safe in Node and edge runtimes (uses Web\n * Crypto + global fetch only).\n */\nimport { createClient, type BetterCMSReadClient } from \"@bettercms-ai/sdk\";\nimport type {\n DeliveredLayout,\n DeliveryComponent,\n DeliveryEntry,\n DeliveryList,\n DeliveryPage,\n} from \"@bettercms-ai/types\";\n\nexport interface RuntimeConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n /** Server-only delivery key (a `content:read:draft` key for drafts). */\n apiKey?: string;\n /**\n * Collapse identical delivery GETs for the life of the process. Set by the integration\n * when Astro's `command === \"build\"` — a static build renders every route from one\n * published dataset, so a repeat read cannot legitimately differ.\n *\n * Never set for `dev` or SSR, where each request must see current content.\n */\n buildCache?: boolean;\n}\n\n/** Per-request draft state, set on `Astro.locals.bcms` by the middleware. */\nexport interface BcmsLocals {\n draft: boolean;\n token?: string;\n}\n\ninterface HasLocals {\n locals: { bcms?: BcmsLocals };\n}\n\nexport interface BetterCMSRuntime {\n /** Published-perspective client (use for SSG / non-draft reads). */\n client: BetterCMSReadClient;\n /** Pick the right client for a request (draft client when draft mode is on). */\n getClient(astro?: HasLocals): BetterCMSReadClient;\n loadEntry<T = Record<string, unknown>>(\n astro: HasLocals,\n slug: string,\n opts?: Parameters<BetterCMSReadClient[\"getEntry\"]>[1],\n ): Promise<DeliveryEntry<T>>;\n loadEntries<T = Record<string, unknown>>(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listEntries\"]>[0],\n ): Promise<DeliveryList<DeliveryEntry<T>>>;\n loadPage(astro: HasLocals, slug: string): Promise<DeliveryPage | null>;\n /** Component definitions for `<BcmsBlocks components={…}>` — draft-side in draft mode. */\n loadComponents(astro: HasLocals): Promise<DeliveryComponent[]>;\n loadLayout(astro: HasLocals): Promise<DeliveredLayout | null>;\n loadForms(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listForms\"]>[0],\n ): Promise<Awaited<ReturnType<BetterCMSReadClient[\"listForms\"]>>>;\n}\n\nexport function createRuntime(config: RuntimeConfig): BetterCMSRuntime {\n const { buildCache, ...clientConfig } = config;\n // One client for the whole build, so the cache below is shared across every route\n // rather than rebuilt per render.\n const published = createClient({ ...clientConfig, perspective: \"published\", cache: buildCache });\n\n const getClient = (astro?: HasLocals): BetterCMSReadClient => {\n const bcms = astro?.locals?.bcms;\n if (bcms?.draft) {\n // Explicitly uncached: a draft read exists to show the edit that just happened.\n return createClient({\n ...clientConfig,\n perspective: \"drafts\",\n previewToken: bcms.token,\n cache: false,\n });\n }\n return published;\n };\n\n return {\n client: published,\n getClient,\n loadEntry: (astro, slug, opts) => getClient(astro).getEntry(slug, opts),\n loadEntries: (astro, opts) => getClient(astro).listEntries(opts),\n loadPage: (astro, slug) => getClient(astro).getPage(slug),\n loadComponents: (astro) => getClient(astro).listComponents(),\n loadLayout: (astro) => getClient(astro).getLayout(),\n loadForms: (astro, opts) => getClient(astro).listForms(opts),\n };\n}\n\n/**\n * Build the live SSE URL for `<BcmsLive>`. The key is a query param because\n * browser EventSource cannot send headers — use a content:read key.\n *\n * `depth` sets the hydration depth of the document the stream pushes (default 1,\n * matching the delivery API). `drafts` streams autosaves so a preview surface\n * repaints as the editor types — it needs a `content:read:draft` key, and the\n * server ignores the flag for any other key rather than failing the connection.\n * Never enable it on a public site.\n */\nexport function liveSrc(opts: {\n apiUrl: string;\n workspace: string;\n apiKey: string;\n depth?: 0 | 1 | 2;\n drafts?: boolean;\n}): string {\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const q = new URLSearchParams({ key: opts.apiKey });\n if (opts.depth !== undefined) q.set(\"depth\", String(opts.depth));\n if (opts.drafts) q.set(\"drafts\", \"1\");\n return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;\n}\n\n/**\n * The published content snapshot, read at request time on a server BetterCMS hosts (it rewrites the\n * file on every publish) and from `bcms-content.json` at build time. Read content through this —\n * never `import` bcms-content.json or `import.meta.glob` it: a bundled copy is frozen at build, and\n * the platform has to rebuild the whole site for every publish to change it.\n */\nexport { readPublishedContent, publishedEntry, type PublishedContent } from \"@bettercms-ai/sdk\";\n\n// Signed-cookie and draft-route helpers: one implementation for every adapter (@bettercms-ai/sdk/draft).\nexport { signValue, verifyValue, safeRedirectPath, decodeTokenSlug } from \"@bettercms-ai/sdk/draft\";\n\n/**\n * The draft-cookie signing secret, `process.env` FIRST.\n *\n * Vite may INLINE `import.meta.env.BCMS_DRAFT_SECRET` at build time, and the value present\n * when the customer's site is built is not the value the hosting container is given at start\n * — BetterCMS derives it per project and injects it as a real env var. Reading the runtime\n * env first is what makes managed hosting work without asking anyone to rebuild. The\n * `import.meta.env` read stays as the fallback for `astro dev` and for adapters with no\n * `process` (guarded, since touching `import.meta.env` outside Vite throws).\n */\nexport function draftSecret(): string | undefined {\n const fromProcess =\n typeof process !== \"undefined\" ? (process.env?.BCMS_DRAFT_SECRET as string | undefined) : undefined;\n if (fromProcess) return fromProcess;\n try {\n return import.meta.env?.BCMS_DRAFT_SECRET as string | undefined;\n } catch {\n return undefined;\n }\n}\n","/**\n * @bettercms-ai/astro — the BetterCMS adapter for Astro.\n *\n * import { defineConfig } from \"astro/config\";\n * import bettercms from \"@bettercms-ai/astro\";\n *\n * export default defineConfig({\n * integrations: [bettercms({ apiUrl, workspace })],\n * output: \"server\",\n * });\n *\n * Then in any page: `import { loadPage } from \"bettercms:client\"`.\n */\nexport { bettercms, default } from \"./integration.js\";\nexport {\n resolveConfig,\n DRAFT_COOKIE,\n type BetterCMSAstroOptions,\n type PublicConfig,\n type ResolvedConfig,\n} from \"./config.js\";\nexport { liveSrc } from \"./runtime.js\";\n// C3/C4: a site's own section root declares that it paints block.style.\nexport { RESPONSIVE_CSS } from \"@bettercms-ai/types\";\n// A `link` value (or a button's `href`) is a string or a link object: `linkHref` reads either as the href.\nexport { linkHref, type LinkValue } from \"@bettercms-ai/types\";\nexport { sectionRootAttrs, type SectionRootAttrs, type SectionRootBlock } from \"./section-root.js\";\nexport { fieldAttrs, fieldStyleOf, type FieldAttrs, entryAttrs, entryField, type EntryRef, type EntryTransform, type EntryFieldAttrs } from \"./field-attrs.js\";\nexport { sectionColumnAttrs, type SectionColumnAttrs } from \"./section-root.js\";\nexport type { BcmsLocals, BetterCMSRuntime, RuntimeConfig } from \"./runtime.js\";\n\n// Image URL builder, re-exported for `<BcmsImage>` and host code.\nexport {\n default as imageUrlBuilder,\n imageUrl,\n imageValueUrl,\n imageValueObjectPosition,\n} from \"@bettercms-ai/image-url\";\nexport type {\n ImageUrlBuilder,\n ImageSource,\n ImageFormat,\n ImageFit,\n ImageCrop,\n ImageFocal,\n ImageValueUrlOptions,\n} from \"@bettercms-ai/image-url\";\n\n// Delivery read types, re-exported for typing page/entry data.\nexport type {\n Perspective,\n DeliveryEntry,\n DeliveryPage,\n DeliveryList,\n PageMetaJson,\n SiteSeoDefaults,\n ContentBlock,\n DeliveredLayout,\n} from \"@bettercms-ai/types\";\nexport type { DeliveryForm, DeliveryFormField } from \"@bettercms-ai/sdk\";\n\n// SEO (FLO-302): page-over-site meta resolver. In a page's frontmatter,\n// `const seo = resolveSeo(page, siteDefaults)`, then map `seo.*` into `<head>`.\nexport { resolveSeo } from \"@bettercms-ai/sdk\";\nexport type { SeoInput, ResolvedSeo } from \"@bettercms-ai/sdk\";\n","/**\n * C3/C4 — `sectionRootAttrs(block)`: the attributes a site's OWN section root spreads so the\n * BetterCMS editor offers that section's spacing and style handles.\n *\n * <section class=\"hero\" {...sectionRootAttrs(block)}>…</section>\n *\n * The editor shows those handles on a site-rendered page only when EVERY root the site stamps\n * with `data-bcms-block` also carries `data-bcms-style`, which is the site's promise that \"this\n * root's inline style is `blockStyleToCss(block.style)`\" — the same declarations `<BcmsBlocks>`\n * puts on its own wrapper. A root that stamps the block without applying its style would get a\n * capsule that paints the canvas and never ships, so the marker must travel with the style.\n *\n * The marker is present even when the style is EMPTY (then there is no `style` attribute at all):\n * an unstyled section is exactly the one an author is about to give its first spacing edit.\n */\nimport { blockStyleToColumnCss, blockStyleToCss, hideOnAttr, type BlockStyle } from \"@bettercms-ai/types\";\n\nexport interface SectionRootBlock {\n id?: string | null;\n style?: BlockStyle | Record<string, unknown> | null;\n}\n\nexport interface SectionRootAttrs {\n \"data-bcms-block\"?: string;\n \"data-bcms-style\": \"\";\n /** `\"mobile tablet\"` when `style.hideOn` hides it there; the site ships `RESPONSIVE_CSS` for it. */\n \"data-bcms-hide\"?: string;\n /** `blockStyleToCss(block.style)` as a CSS string; absent when the block has no style. */\n style?: string;\n}\n\n/** `paddingTop` → `padding-top`, joined the way `<BcmsBlocks>` writes its wrapper's attribute. */\nfunction cssText(css: Record<string, string | number>): string {\n return Object.entries(css)\n .map(([k, v]) => `${k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}:${v}`)\n .join(\";\");\n}\n\nexport function sectionRootAttrs(block: SectionRootBlock | null | undefined): SectionRootAttrs {\n const css = blockStyleToCss((block?.style ?? undefined) as BlockStyle | undefined);\n const id = typeof block?.id === \"string\" && block.id !== \"\" ? block.id : undefined;\n const hide = hideOnAttr(block?.style);\n return {\n ...(id !== undefined ? { \"data-bcms-block\": id } : {}),\n \"data-bcms-style\": \"\",\n ...(hide ? { \"data-bcms-hide\": hide } : {}),\n ...(css ? { style: cssText(css) } : {}),\n };\n}\n\nexport interface SectionColumnAttrs {\n \"data-bcms-column\": \"\";\n /** `blockStyleToColumnCss(block.style)` as a CSS string; absent when the section has no column. */\n style?: string;\n}\n\n/**\n * C3 — `sectionColumnAttrs(block)`: the attributes a site's own INNER container spreads so the\n * section's content WIDTH (`style.contentWidth`, placed by `contentAlign`) ships.\n *\n * <section {...sectionRootAttrs(block)}>\n * <div class=\"container\" {...sectionColumnAttrs(block)}>…</div>\n * </section>\n *\n * The width is the column BetterCMS draws as `.bcms-block-inner` inside the section's styled box —\n * never on the root itself, whose background must stay full-bleed. `data-bcms-column` is the\n * site's promise that this box's inline style is the column; the editor offers the width handle\n * on a site-rendered page only where every styled root holds one. Max-width plus left/right\n * margins, so the container keeps its own vertical margins and, with no column, its own width.\n */\nexport function sectionColumnAttrs(block: SectionRootBlock | null | undefined): SectionColumnAttrs {\n const css = blockStyleToColumnCss((block?.style ?? undefined) as BlockStyle | undefined);\n return { \"data-bcms-column\": \"\", ...(css ? { style: cssText(css) } : {}) };\n}\n","/**\n * C3 — `fieldAttrs(path, fieldStyles)`: the attributes a site's OWN element spreads to bind a PAGE\n * FIELD and apply that field's stored style, so the BetterCMS editor offers its width and spacing\n * handles and its Align / Size / Weight controls.\n *\n * ---\n * const page = await loadPage(Astro, \"home\");\n * ---\n * <h1 class=\"title\" {...fieldAttrs(\"hero-title\", page?.fieldStyles)}>{entry.data[\"hero-title\"]}</h1>\n *\n * `data-bcms-style` is the site's promise that \"this element's inline style is\n * `fieldStyleToCss(fieldStyles[path])`\". The editor offers a field's handles only on an element that\n * carries it: a handle on one that does not would paint the canvas and never ship. The marker is\n * present even when the field has no style (then there is no `style` attribute at all, byte for\n * byte the element it was): an unstyled field is the one an author is about to give its first edit.\n * Twin: `fieldAttrs` in @bettercms-ai/next (a React style object there).\n */\nimport { fieldStyleCssText, type FieldStyle, type FieldStyles } from \"@bettercms-ai/types\";\n\nexport interface FieldAttrs {\n \"data-bcms-field\": string;\n /** The marker's revision: \"2\" promises `size` (zoom) too. Section roots keep \"\". */\n \"data-bcms-style\": \"2\";\n /** `fieldStyleToCss(fieldStyles[path])` as a CSS string; absent when the field has no style. */\n style?: string;\n}\n\n/** The stored style for `path`, by OWN key only — a path like `constructor` never reads the prototype. */\nexport function fieldStyleOf(path: string, fieldStyles: FieldStyles | Record<string, unknown> | null | undefined): FieldStyle | undefined {\n if (!fieldStyles || typeof fieldStyles !== \"object\" || !Object.prototype.hasOwnProperty.call(fieldStyles, path)) return undefined;\n const style = (fieldStyles as Record<string, unknown>)[path];\n return style && typeof style === \"object\" ? (style as FieldStyle) : undefined;\n}\n\nexport function fieldAttrs(path: string, fieldStyles?: FieldStyles | Record<string, unknown> | null): FieldAttrs {\n const style = fieldStyleCssText(fieldStyleOf(path, fieldStyles));\n return { \"data-bcms-field\": path, \"data-bcms-style\": \"2\", ...(style ? { style } : {}) };\n}\n\n/**\n * P7 — `entryAttrs(entry)` and `entryField(entry, path)`: the attributes a site's OWN card spreads to\n * declare a collection ENTRY, so the editor binds it without guessing from the rendered text.\n *\n * <article {...entryAttrs(post)}><h3 {...entryField(post, \"title\")}>{post.fields.title}</h3></article>\n *\n * The field address is ABSOLUTE (`entry:<id>:<path>`). With a `transform` the value is shown\n * changed (a date formatted, text truncated), so it is declared as `data-bcms-entry-from`: the\n * editor knows where it is but cannot re-render it. Twin: same helpers in @bettercms-ai/next.\n */\nexport type EntryRef = { id?: string } | string;\nexport type EntryTransform = \"format\" | \"truncate\" | \"rendition\" | \"route\";\n/** One key is present: `data-bcms-entry-field`, or `data-bcms-entry-from` when transformed; none without an id. */\nexport interface EntryFieldAttrs {\n \"data-bcms-entry-field\"?: string;\n \"data-bcms-entry-from\"?: string;\n}\n\nfunction entryIdOf(entry: EntryRef): string | null {\n const id = typeof entry === \"string\" ? entry : entry?.id;\n return typeof id === \"string\" && id.trim() !== \"\" ? id : null;\n}\n\n/** An entry with no id (a snapshot written before ids were kept) renders unbound, never fails the build. */\nexport function entryAttrs(entry: EntryRef): { \"data-bcms-entry\"?: string } {\n const id = entryIdOf(entry);\n return id ? { \"data-bcms-entry\": id } : {};\n}\n\nexport function entryField(entry: EntryRef, path: string, opts?: { transform?: EntryTransform }): EntryFieldAttrs {\n const id = entryIdOf(entry);\n if (typeof path !== \"string\" || path.trim() === \"\") throw new Error(`entryField: no field path given for entry ${id ?? \"(no id)\"}`);\n if (!id) return {};\n const address = `entry:${id}:${path}`;\n return opts?.transform ? { \"data-bcms-entry-from\": `${address}|${opts.transform}` } : { \"data-bcms-entry-field\": address };\n}\n"],"mappings":"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","names":["DEFAULT_MEDIA_URL","DRAFT_COOKIE","snapshot","env","config","config","readPublishedContent","default"]}
@@ -4,32 +4,7 @@ import config from "bettercms:config";
4
4
  // src/runtime.ts
5
5
  import { createClient } from "@bettercms-ai/sdk";
6
6
  import { readPublishedContent, publishedEntry } from "@bettercms-ai/sdk";
7
- var enc = new TextEncoder();
8
- function base64url(bytes) {
9
- const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));
10
- return b.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
11
- }
12
- async function hmac(value, secret) {
13
- const key = await crypto.subtle.importKey(
14
- "raw",
15
- enc.encode(secret),
16
- { name: "HMAC", hash: "SHA-256" },
17
- false,
18
- ["sign"]
19
- );
20
- return base64url(await crypto.subtle.sign("HMAC", key, enc.encode(value)));
21
- }
22
- async function verifyValue(signed, secret) {
23
- const idx = signed.lastIndexOf(".");
24
- if (idx < 0) return null;
25
- const value = signed.slice(0, idx);
26
- const sig = signed.slice(idx + 1);
27
- const expected = await hmac(value, secret);
28
- if (sig.length !== expected.length) return null;
29
- let diff = 0;
30
- for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);
31
- return diff === 0 ? value : null;
32
- }
7
+ import { signValue, verifyValue, safeRedirectPath, decodeTokenSlug } from "@bettercms-ai/sdk/draft";
33
8
  function draftSecret() {
34
9
  const fromProcess = typeof process !== "undefined" ? process.env?.BCMS_DRAFT_SECRET : void 0;
35
10
  if (fromProcess) return fromProcess;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/middleware-entry.ts","../src/runtime.ts"],"sourcesContent":["/**\n * Draft-mode middleware (registered via `addMiddleware`).\n *\n * Reads the signed draft cookie, verifies it with `BCMS_DRAFT_SECRET`, and\n * publishes the result on `Astro.locals.bcms` so loaders pick the draft client.\n * In draft mode it also stamps `<html data-bcms-draft=\"1\">` on the response — the\n * only thing the BetterCMS canvas trusts as proof that the frame it loaded really is\n * this site rendering a draft, rather than the published page it falls back to.\n */\nimport type { MiddlewareHandler } from \"astro\";\nimport config from \"bettercms:config\";\nimport { draftSecret, verifyValue, type BcmsLocals } from \"./runtime.js\";\n\nexport const onRequest: MiddlewareHandler = async (context, next) => {\n const cookie = context.cookies.get(config.draftCookie)?.value;\n const secret = draftSecret();\n\n let locals: BcmsLocals = { draft: false };\n if (cookie && secret) {\n const token = await verifyValue(cookie, secret);\n if (token) locals = { draft: true, token };\n }\n context.locals.bcms = locals;\n\n const response = await next();\n // Untouched off the draft path: reading the body would buffer every response on the site,\n // including streamed and non-HTML ones, for a marker nobody is looking for.\n if (!locals.draft || !response.headers.get(\"content-type\")?.includes(\"text/html\")) {\n return response;\n }\n const html = await response.text();\n // The rewrite is 20 bytes longer than what the upstream handler measured, so a\n // `content-length` carried over from it truncates the body mid-tag. Drop it and let the\n // server recompute; every other header (cookies, cache, CSP) must survive untouched.\n const headers = new Headers(response.headers);\n headers.delete(\"content-length\");\n return new Response(html.replace(\"<html\", '<html data-bcms-draft=\"1\"'), {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n};\n","/**\n * Runtime for @bettercms-ai/astro — the code the `bettercms:client` virtual\n * module binds to. Provides a published client, per-request draft client\n * selection, typed loaders, and signed-cookie helpers for draft mode.\n *\n * Read-only; no I/O at import time. Safe in Node and edge runtimes (uses Web\n * Crypto + global fetch only).\n */\nimport { createClient, type BetterCMSReadClient } from \"@bettercms-ai/sdk\";\nimport type {\n DeliveredLayout,\n DeliveryComponent,\n DeliveryEntry,\n DeliveryList,\n DeliveryPage,\n} from \"@bettercms-ai/types\";\n\nexport interface RuntimeConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n /** Server-only delivery key (a `content:read:draft` key for drafts). */\n apiKey?: string;\n /**\n * Collapse identical delivery GETs for the life of the process. Set by the integration\n * when Astro's `command === \"build\"` — a static build renders every route from one\n * published dataset, so a repeat read cannot legitimately differ.\n *\n * Never set for `dev` or SSR, where each request must see current content.\n */\n buildCache?: boolean;\n}\n\n/** Per-request draft state, set on `Astro.locals.bcms` by the middleware. */\nexport interface BcmsLocals {\n draft: boolean;\n token?: string;\n}\n\ninterface HasLocals {\n locals: { bcms?: BcmsLocals };\n}\n\nexport interface BetterCMSRuntime {\n /** Published-perspective client (use for SSG / non-draft reads). */\n client: BetterCMSReadClient;\n /** Pick the right client for a request (draft client when draft mode is on). */\n getClient(astro?: HasLocals): BetterCMSReadClient;\n loadEntry<T = Record<string, unknown>>(\n astro: HasLocals,\n slug: string,\n opts?: Parameters<BetterCMSReadClient[\"getEntry\"]>[1],\n ): Promise<DeliveryEntry<T>>;\n loadEntries<T = Record<string, unknown>>(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listEntries\"]>[0],\n ): Promise<DeliveryList<DeliveryEntry<T>>>;\n loadPage(astro: HasLocals, slug: string): Promise<DeliveryPage | null>;\n /** Component definitions for `<BcmsBlocks components={…}>` — draft-side in draft mode. */\n loadComponents(astro: HasLocals): Promise<DeliveryComponent[]>;\n loadLayout(astro: HasLocals): Promise<DeliveredLayout | null>;\n loadForms(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listForms\"]>[0],\n ): Promise<Awaited<ReturnType<BetterCMSReadClient[\"listForms\"]>>>;\n}\n\nexport function createRuntime(config: RuntimeConfig): BetterCMSRuntime {\n const { buildCache, ...clientConfig } = config;\n // One client for the whole build, so the cache below is shared across every route\n // rather than rebuilt per render.\n const published = createClient({ ...clientConfig, perspective: \"published\", cache: buildCache });\n\n const getClient = (astro?: HasLocals): BetterCMSReadClient => {\n const bcms = astro?.locals?.bcms;\n if (bcms?.draft) {\n // Explicitly uncached: a draft read exists to show the edit that just happened.\n return createClient({\n ...clientConfig,\n perspective: \"drafts\",\n previewToken: bcms.token,\n cache: false,\n });\n }\n return published;\n };\n\n return {\n client: published,\n getClient,\n loadEntry: (astro, slug, opts) => getClient(astro).getEntry(slug, opts),\n loadEntries: (astro, opts) => getClient(astro).listEntries(opts),\n loadPage: (astro, slug) => getClient(astro).getPage(slug),\n loadComponents: (astro) => getClient(astro).listComponents(),\n loadLayout: (astro) => getClient(astro).getLayout(),\n loadForms: (astro, opts) => getClient(astro).listForms(opts),\n };\n}\n\n/**\n * Build the live SSE URL for `<BcmsLive>`. The key is a query param because\n * browser EventSource cannot send headers — use a content:read key.\n *\n * `depth` sets the hydration depth of the document the stream pushes (default 1,\n * matching the delivery API). `drafts` streams autosaves so a preview surface\n * repaints as the editor types — it needs a `content:read:draft` key, and the\n * server ignores the flag for any other key rather than failing the connection.\n * Never enable it on a public site.\n */\nexport function liveSrc(opts: {\n apiUrl: string;\n workspace: string;\n apiKey: string;\n depth?: 0 | 1 | 2;\n drafts?: boolean;\n}): string {\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const q = new URLSearchParams({ key: opts.apiKey });\n if (opts.depth !== undefined) q.set(\"depth\", String(opts.depth));\n if (opts.drafts) q.set(\"drafts\", \"1\");\n return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;\n}\n\n/**\n * The published content snapshot, read at request time on a server BetterCMS hosts (it rewrites the\n * file on every publish) and from `bcms-content.json` at build time. Read content through this —\n * never `import` bcms-content.json or `import.meta.glob` it: a bundled copy is frozen at build, and\n * the platform has to rebuild the whole site for every publish to change it.\n */\nexport { readPublishedContent, publishedEntry, type PublishedContent } from \"@bettercms-ai/sdk\";\n\n// ── Signed-cookie helpers (HMAC-SHA256, base64url) ──────────────────────────\n\nconst enc = new TextEncoder();\n\nfunction base64url(bytes: ArrayBuffer): string {\n const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));\n return b.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function hmac(value: string, secret: string): Promise<string> {\n const key = await crypto.subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n return base64url(await crypto.subtle.sign(\"HMAC\", key, enc.encode(value)));\n}\n\n/** Sign a value as `value.sig` for tamper-evident cookie storage. */\nexport async function signValue(value: string, secret: string): Promise<string> {\n return `${value}.${await hmac(value, secret)}`;\n}\n\n/** Verify a signed value, returning the original value or null if tampered. */\nexport async function verifyValue(signed: string, secret: string): Promise<string | null> {\n const idx = signed.lastIndexOf(\".\");\n if (idx < 0) return null;\n const value = signed.slice(0, idx);\n const sig = signed.slice(idx + 1);\n const expected = await hmac(value, secret);\n // Constant-time-ish compare via length + char accumulation.\n if (sig.length !== expected.length) return null;\n let diff = 0;\n for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);\n return diff === 0 ? value : null;\n}\n\n/**\n * The draft-cookie signing secret, `process.env` FIRST.\n *\n * Vite may INLINE `import.meta.env.BCMS_DRAFT_SECRET` at build time, and the value present\n * when the customer's site is built is not the value the hosting container is given at start\n * — BetterCMS derives it per project and injects it as a real env var. Reading the runtime\n * env first is what makes managed hosting work without asking anyone to rebuild. The\n * `import.meta.env` read stays as the fallback for `astro dev` and for adapters with no\n * `process` (guarded, since touching `import.meta.env` outside Vite throws).\n */\nexport function draftSecret(): string | undefined {\n const fromProcess =\n typeof process !== \"undefined\" ? (process.env?.BCMS_DRAFT_SECRET as string | undefined) : undefined;\n if (fromProcess) return fromProcess;\n try {\n return import.meta.env?.BCMS_DRAFT_SECRET as string | undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Only same-origin relative paths. Blocks \"//evil.com\", \"https://evil.com\", etc. */\nexport function safeRedirectPath(to: string): string {\n return to.startsWith(\"/\") && !to.startsWith(\"//\") ? to : \"/\";\n}\n\n/** Decode a preview-token JWT payload (UNVERIFIED) to read its `entrySlug`. */\nexport function decodeTokenSlug(token: string): string | null {\n try {\n const part = token.split(\".\")[1];\n if (!part) return null;\n const json = JSON.parse(\n atob(part.replace(/-/g, \"+\").replace(/_/g, \"/\")),\n ) as { entrySlug?: string };\n return json.entrySlug ?? null;\n } catch {\n return null;\n }\n}\n"],"mappings":";AAUA,OAAO,YAAY;;;ACFnB,SAAS,oBAA8C;AAyHvD,SAAS,sBAAsB,sBAA6C;AAI5E,IAAM,MAAM,IAAI,YAAY;AAE5B,SAAS,UAAU,OAA4B;AAC7C,QAAM,IAAI,KAAK,OAAO,aAAa,GAAG,IAAI,WAAW,KAAK,CAAC,CAAC;AAC5D,SAAO,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AACpE;AAEA,eAAe,KAAK,OAAe,QAAiC;AAClE,QAAM,MAAM,MAAM,OAAO,OAAO;AAAA,IAC9B;AAAA,IACA,IAAI,OAAO,MAAM;AAAA,IACjB,EAAE,MAAM,QAAQ,MAAM,UAAU;AAAA,IAChC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,SAAO,UAAU,MAAM,OAAO,OAAO,KAAK,QAAQ,KAAK,IAAI,OAAO,KAAK,CAAC,CAAC;AAC3E;AAQA,eAAsB,YAAY,QAAgB,QAAwC;AACxF,QAAM,MAAM,OAAO,YAAY,GAAG;AAClC,MAAI,MAAM,EAAG,QAAO;AACpB,QAAM,QAAQ,OAAO,MAAM,GAAG,GAAG;AACjC,QAAM,MAAM,OAAO,MAAM,MAAM,CAAC;AAChC,QAAM,WAAW,MAAM,KAAK,OAAO,MAAM;AAEzC,MAAI,IAAI,WAAW,SAAS,OAAQ,QAAO;AAC3C,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,IAAK,SAAQ,IAAI,WAAW,CAAC,IAAI,SAAS,WAAW,CAAC;AACtF,SAAO,SAAS,IAAI,QAAQ;AAC9B;AAYO,SAAS,cAAkC;AAChD,QAAM,cACJ,OAAO,YAAY,cAAe,QAAQ,KAAK,oBAA2C;AAC5F,MAAI,YAAa,QAAO;AACxB,MAAI;AACF,WAAO,YAAY,KAAK;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ADhLO,IAAM,YAA+B,OAAO,SAAS,SAAS;AACnE,QAAM,SAAS,QAAQ,QAAQ,IAAI,OAAO,WAAW,GAAG;AACxD,QAAM,SAAS,YAAY;AAE3B,MAAI,SAAqB,EAAE,OAAO,MAAM;AACxC,MAAI,UAAU,QAAQ;AACpB,UAAM,QAAQ,MAAM,YAAY,QAAQ,MAAM;AAC9C,QAAI,MAAO,UAAS,EAAE,OAAO,MAAM,MAAM;AAAA,EAC3C;AACA,UAAQ,OAAO,OAAO;AAEtB,QAAM,WAAW,MAAM,KAAK;AAG5B,MAAI,CAAC,OAAO,SAAS,CAAC,SAAS,QAAQ,IAAI,cAAc,GAAG,SAAS,WAAW,GAAG;AACjF,WAAO;AAAA,EACT;AACA,QAAM,OAAO,MAAM,SAAS,KAAK;AAIjC,QAAM,UAAU,IAAI,QAAQ,SAAS,OAAO;AAC5C,UAAQ,OAAO,gBAAgB;AAC/B,SAAO,IAAI,SAAS,KAAK,QAAQ,SAAS,2BAA2B,GAAG;AAAA,IACtE,QAAQ,SAAS;AAAA,IACjB,YAAY,SAAS;AAAA,IACrB;AAAA,EACF,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/middleware-entry.ts","../src/runtime.ts"],"sourcesContent":["/**\n * Draft-mode middleware (registered via `addMiddleware`).\n *\n * Reads the signed draft cookie, verifies it with `BCMS_DRAFT_SECRET`, and\n * publishes the result on `Astro.locals.bcms` so loaders pick the draft client.\n * In draft mode it also stamps `<html data-bcms-draft=\"1\">` on the response — the\n * only thing the BetterCMS canvas trusts as proof that the frame it loaded really is\n * this site rendering a draft, rather than the published page it falls back to.\n */\nimport type { MiddlewareHandler } from \"astro\";\nimport config from \"bettercms:config\";\nimport { draftSecret, verifyValue, type BcmsLocals } from \"./runtime.js\";\n\nexport const onRequest: MiddlewareHandler = async (context, next) => {\n const cookie = context.cookies.get(config.draftCookie)?.value;\n const secret = draftSecret();\n\n let locals: BcmsLocals = { draft: false };\n if (cookie && secret) {\n const token = await verifyValue(cookie, secret);\n if (token) locals = { draft: true, token };\n }\n context.locals.bcms = locals;\n\n const response = await next();\n // Untouched off the draft path: reading the body would buffer every response on the site,\n // including streamed and non-HTML ones, for a marker nobody is looking for.\n if (!locals.draft || !response.headers.get(\"content-type\")?.includes(\"text/html\")) {\n return response;\n }\n const html = await response.text();\n // The rewrite is 20 bytes longer than what the upstream handler measured, so a\n // `content-length` carried over from it truncates the body mid-tag. Drop it and let the\n // server recompute; every other header (cookies, cache, CSP) must survive untouched.\n const headers = new Headers(response.headers);\n headers.delete(\"content-length\");\n return new Response(html.replace(\"<html\", '<html data-bcms-draft=\"1\"'), {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n};\n","/**\n * Runtime for @bettercms-ai/astro — the code the `bettercms:client` virtual\n * module binds to. Provides a published client, per-request draft client\n * selection, typed loaders, and signed-cookie helpers for draft mode.\n *\n * Read-only; no I/O at import time. Safe in Node and edge runtimes (uses Web\n * Crypto + global fetch only).\n */\nimport { createClient, type BetterCMSReadClient } from \"@bettercms-ai/sdk\";\nimport type {\n DeliveredLayout,\n DeliveryComponent,\n DeliveryEntry,\n DeliveryList,\n DeliveryPage,\n} from \"@bettercms-ai/types\";\n\nexport interface RuntimeConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n /** Server-only delivery key (a `content:read:draft` key for drafts). */\n apiKey?: string;\n /**\n * Collapse identical delivery GETs for the life of the process. Set by the integration\n * when Astro's `command === \"build\"` — a static build renders every route from one\n * published dataset, so a repeat read cannot legitimately differ.\n *\n * Never set for `dev` or SSR, where each request must see current content.\n */\n buildCache?: boolean;\n}\n\n/** Per-request draft state, set on `Astro.locals.bcms` by the middleware. */\nexport interface BcmsLocals {\n draft: boolean;\n token?: string;\n}\n\ninterface HasLocals {\n locals: { bcms?: BcmsLocals };\n}\n\nexport interface BetterCMSRuntime {\n /** Published-perspective client (use for SSG / non-draft reads). */\n client: BetterCMSReadClient;\n /** Pick the right client for a request (draft client when draft mode is on). */\n getClient(astro?: HasLocals): BetterCMSReadClient;\n loadEntry<T = Record<string, unknown>>(\n astro: HasLocals,\n slug: string,\n opts?: Parameters<BetterCMSReadClient[\"getEntry\"]>[1],\n ): Promise<DeliveryEntry<T>>;\n loadEntries<T = Record<string, unknown>>(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listEntries\"]>[0],\n ): Promise<DeliveryList<DeliveryEntry<T>>>;\n loadPage(astro: HasLocals, slug: string): Promise<DeliveryPage | null>;\n /** Component definitions for `<BcmsBlocks components={…}>` — draft-side in draft mode. */\n loadComponents(astro: HasLocals): Promise<DeliveryComponent[]>;\n loadLayout(astro: HasLocals): Promise<DeliveredLayout | null>;\n loadForms(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listForms\"]>[0],\n ): Promise<Awaited<ReturnType<BetterCMSReadClient[\"listForms\"]>>>;\n}\n\nexport function createRuntime(config: RuntimeConfig): BetterCMSRuntime {\n const { buildCache, ...clientConfig } = config;\n // One client for the whole build, so the cache below is shared across every route\n // rather than rebuilt per render.\n const published = createClient({ ...clientConfig, perspective: \"published\", cache: buildCache });\n\n const getClient = (astro?: HasLocals): BetterCMSReadClient => {\n const bcms = astro?.locals?.bcms;\n if (bcms?.draft) {\n // Explicitly uncached: a draft read exists to show the edit that just happened.\n return createClient({\n ...clientConfig,\n perspective: \"drafts\",\n previewToken: bcms.token,\n cache: false,\n });\n }\n return published;\n };\n\n return {\n client: published,\n getClient,\n loadEntry: (astro, slug, opts) => getClient(astro).getEntry(slug, opts),\n loadEntries: (astro, opts) => getClient(astro).listEntries(opts),\n loadPage: (astro, slug) => getClient(astro).getPage(slug),\n loadComponents: (astro) => getClient(astro).listComponents(),\n loadLayout: (astro) => getClient(astro).getLayout(),\n loadForms: (astro, opts) => getClient(astro).listForms(opts),\n };\n}\n\n/**\n * Build the live SSE URL for `<BcmsLive>`. The key is a query param because\n * browser EventSource cannot send headers — use a content:read key.\n *\n * `depth` sets the hydration depth of the document the stream pushes (default 1,\n * matching the delivery API). `drafts` streams autosaves so a preview surface\n * repaints as the editor types — it needs a `content:read:draft` key, and the\n * server ignores the flag for any other key rather than failing the connection.\n * Never enable it on a public site.\n */\nexport function liveSrc(opts: {\n apiUrl: string;\n workspace: string;\n apiKey: string;\n depth?: 0 | 1 | 2;\n drafts?: boolean;\n}): string {\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const q = new URLSearchParams({ key: opts.apiKey });\n if (opts.depth !== undefined) q.set(\"depth\", String(opts.depth));\n if (opts.drafts) q.set(\"drafts\", \"1\");\n return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;\n}\n\n/**\n * The published content snapshot, read at request time on a server BetterCMS hosts (it rewrites the\n * file on every publish) and from `bcms-content.json` at build time. Read content through this —\n * never `import` bcms-content.json or `import.meta.glob` it: a bundled copy is frozen at build, and\n * the platform has to rebuild the whole site for every publish to change it.\n */\nexport { readPublishedContent, publishedEntry, type PublishedContent } from \"@bettercms-ai/sdk\";\n\n// Signed-cookie and draft-route helpers: one implementation for every adapter (@bettercms-ai/sdk/draft).\nexport { signValue, verifyValue, safeRedirectPath, decodeTokenSlug } from \"@bettercms-ai/sdk/draft\";\n\n/**\n * The draft-cookie signing secret, `process.env` FIRST.\n *\n * Vite may INLINE `import.meta.env.BCMS_DRAFT_SECRET` at build time, and the value present\n * when the customer's site is built is not the value the hosting container is given at start\n * — BetterCMS derives it per project and injects it as a real env var. Reading the runtime\n * env first is what makes managed hosting work without asking anyone to rebuild. The\n * `import.meta.env` read stays as the fallback for `astro dev` and for adapters with no\n * `process` (guarded, since touching `import.meta.env` outside Vite throws).\n */\nexport function draftSecret(): string | undefined {\n const fromProcess =\n typeof process !== \"undefined\" ? (process.env?.BCMS_DRAFT_SECRET as string | undefined) : undefined;\n if (fromProcess) return fromProcess;\n try {\n return import.meta.env?.BCMS_DRAFT_SECRET as string | undefined;\n } catch {\n return undefined;\n }\n}\n"],"mappings":";AAUA,OAAO,YAAY;;;ACFnB,SAAS,oBAA8C;AAyHvD,SAAS,sBAAsB,sBAA6C;AAG5E,SAAS,WAAW,aAAa,kBAAkB,uBAAuB;AAYnE,SAAS,cAAkC;AAChD,QAAM,cACJ,OAAO,YAAY,cAAe,QAAQ,KAAK,oBAA2C;AAC5F,MAAI,YAAa,QAAO;AACxB,MAAI;AACF,WAAO,YAAY,KAAK;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AD5IO,IAAM,YAA+B,OAAO,SAAS,SAAS;AACnE,QAAM,SAAS,QAAQ,QAAQ,IAAI,OAAO,WAAW,GAAG;AACxD,QAAM,SAAS,YAAY;AAE3B,MAAI,SAAqB,EAAE,OAAO,MAAM;AACxC,MAAI,UAAU,QAAQ;AACpB,UAAM,QAAQ,MAAM,YAAY,QAAQ,MAAM;AAC9C,QAAI,MAAO,UAAS,EAAE,OAAO,MAAM,MAAM;AAAA,EAC3C;AACA,UAAQ,OAAO,OAAO;AAEtB,QAAM,WAAW,MAAM,KAAK;AAG5B,MAAI,CAAC,OAAO,SAAS,CAAC,SAAS,QAAQ,IAAI,cAAc,GAAG,SAAS,WAAW,GAAG;AACjF,WAAO;AAAA,EACT;AACA,QAAM,OAAO,MAAM,SAAS,KAAK;AAIjC,QAAM,UAAU,IAAI,QAAQ,SAAS,OAAO;AAC5C,UAAQ,OAAO,gBAAgB;AAC/B,SAAO,IAAI,SAAS,KAAK,QAAQ,SAAS,2BAA2B,GAAG;AAAA,IACtE,QAAQ,SAAS;AAAA,IACjB,YAAY,SAAS;AAAA,IACrB;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -1 +1,14 @@
1
- export { a as DEFAULT_MEDIA_URL, D as DRAFT_COOKIE, b as PUBLIC_CONFIG_ID, c as config, c as default, f as fallbackConfig } from './config-CU7bp5dd.js';
1
+ import { P as PublicConfig } from './config-DUVkCo5R.js';
2
+ export { DEFAULT_MEDIA_URL } from '@bettercms-ai/sdk/config';
3
+ export { DRAFT_COOKIE } from '@bettercms-ai/sdk/draft';
4
+
5
+ /** The specifier the components import, and the integration's plugin answers. */
6
+ declare const PUBLIC_CONFIG_ID = "@bettercms-ai/astro/public-config";
7
+ /**
8
+ * The config with no integration: `PUBLIC_BCMS_*`, then the snapshot. Never throws: with neither,
9
+ * `apiUrl` and `workspace` are empty, which a build that never talks to the API renders with.
10
+ */
11
+ declare function fallbackConfig(): PublicConfig;
12
+ declare const config: PublicConfig;
13
+
14
+ export { PUBLIC_CONFIG_ID, config, config as default, fallbackConfig };
@@ -1,7 +1,9 @@
1
1
  // src/public-config.ts
2
2
  import { readPublishedContent } from "@bettercms-ai/sdk";
3
- var DEFAULT_MEDIA_URL = "https://cdn.bettercms.ai";
4
- var DRAFT_COOKIE = "bcms-draft";
3
+ import { DEFAULT_MEDIA_URL } from "@bettercms-ai/sdk/config";
4
+ import { DRAFT_COOKIE } from "@bettercms-ai/sdk/draft";
5
+ import { DEFAULT_MEDIA_URL as DEFAULT_MEDIA_URL2 } from "@bettercms-ai/sdk/config";
6
+ import { DRAFT_COOKIE as DRAFT_COOKIE2 } from "@bettercms-ai/sdk/draft";
5
7
  var PUBLIC_CONFIG_ID = "@bettercms-ai/astro/public-config";
6
8
  function env(name) {
7
9
  const fromProcess = typeof process !== "undefined" ? process.env?.[name] : void 0;
@@ -20,9 +22,9 @@ function fallbackConfig() {
20
22
  apiUrl: trim(env("PUBLIC_BCMS_API_URL") ?? text(snapshot.apiUrl) ?? ""),
21
23
  workspace: env("PUBLIC_BCMS_WORKSPACE") ?? text(snapshot.workspace) ?? "",
22
24
  projectId: env("PUBLIC_BCMS_PROJECT_ID") ?? text(snapshot.projectId),
23
- mediaUrl: trim(env("PUBLIC_BCMS_MEDIA_URL") ?? DEFAULT_MEDIA_URL),
25
+ mediaUrl: trim(env("PUBLIC_BCMS_MEDIA_URL") ?? DEFAULT_MEDIA_URL2),
24
26
  studioUrl: env("PUBLIC_BCMS_STUDIO_URL"),
25
- draftCookie: DRAFT_COOKIE
27
+ draftCookie: DRAFT_COOKIE2
26
28
  };
27
29
  }
28
30
  var KEYS = ["apiUrl", "workspace", "projectId", "mediaUrl", "studioUrl", "draftCookie"];
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/public-config.ts"],"sourcesContent":["/**\n * The public config the shipped components read (`BcmsImage`, `BcmsForm`).\n *\n * Where `bettercms()` is registered, its Vite plugin answers this very specifier with the config it\n * resolved, options included (@see integration.ts). Where it is not, this file is what resolves:\n * the same values from the build's environment and its content snapshot.\n *\n * WHY A REAL MODULE. The components imported the virtual `bettercms:config`, which only the\n * integration can resolve. A site that registers it conditionally (ScrewFast:\n * `...(bcmsReady ? [bettercms({})] : [])`) could not compile a page that imported `<BcmsBlocks>`,\n * so `<BcmsSections>` was given no block renderer there and a section added in the editor had\n * nothing to draw it.\n *\n * No `node:fs` import: this is bundled into a server render, an edge runtime's included. Read on\n * use, never once at module scope: a server's environment and snapshot are the run's.\n */\nimport { readPublishedContent } from \"@bettercms-ai/sdk\";\nimport type { PublicConfig } from \"./config.js\";\n\n/**\n * The platform's canonical media host, matching `@bettercms-ai/image-url`'s own\n * `DEFAULT_BASE_URL` and the `CDN_URL` the backend mints asset URLs on.\n *\n * This used to be `media.bettercms.ai`. Both hostnames route to the backend — they are both in\n * `$bcms_api_host` in infra/nginx/backend-lb.conf — so nothing was broken, but a site built by\n * this integration disagreed with the URL the server itself hands out for the same asset. That\n * splits the CDN cache for identical bytes and makes an asset URL's origin depend on which code\n * path produced it, which is the exact drift image-url was moved off this host to end.\n *\n * A project that serves media from the API origin instead (`useCdn: false`) overrides this with\n * `PUBLIC_BCMS_MEDIA_URL` or the `mediaUrl` option.\n */\nexport const DEFAULT_MEDIA_URL = \"https://cdn.bettercms.ai\";\nexport const DRAFT_COOKIE = \"bcms-draft\";\n/** The specifier the components import, and the integration's plugin answers. */\nexport const PUBLIC_CONFIG_ID = \"@bettercms-ai/astro/public-config\";\n\n/** The server's runtime env first (hosting injects it), then the build's. */\nfunction env(name: string): string | undefined {\n const fromProcess = typeof process !== \"undefined\" ? process.env?.[name] : undefined;\n if (fromProcess) return fromProcess;\n try {\n return ((import.meta.env as Record<string, string | undefined> | undefined)?.[name]) || undefined;\n } catch {\n return undefined;\n }\n}\n\nconst trim = (url: string): string => url.replace(/\\/+$/, \"\");\nconst text = (value: unknown): string | undefined => (typeof value === \"string\" && value ? value : undefined);\n\n/**\n * The config with no integration: `PUBLIC_BCMS_*`, then the snapshot. Never throws: with neither,\n * `apiUrl` and `workspace` are empty, which a build that never talks to the API renders with.\n */\nexport function fallbackConfig(): PublicConfig {\n const snapshot = (readPublishedContent() ?? {}) as { apiUrl?: unknown; workspace?: unknown; projectId?: unknown };\n return {\n apiUrl: trim(env(\"PUBLIC_BCMS_API_URL\") ?? text(snapshot.apiUrl) ?? \"\"),\n workspace: env(\"PUBLIC_BCMS_WORKSPACE\") ?? text(snapshot.workspace) ?? \"\",\n projectId: env(\"PUBLIC_BCMS_PROJECT_ID\") ?? text(snapshot.projectId),\n mediaUrl: trim(env(\"PUBLIC_BCMS_MEDIA_URL\") ?? DEFAULT_MEDIA_URL),\n studioUrl: env(\"PUBLIC_BCMS_STUDIO_URL\"),\n draftCookie: DRAFT_COOKIE,\n };\n}\n\nconst KEYS = [\"apiUrl\", \"workspace\", \"projectId\", \"mediaUrl\", \"studioUrl\", \"draftCookie\"] as const;\n\nexport const config = Object.defineProperties(\n {},\n Object.fromEntries(KEYS.map((key) => [key, { enumerable: true, get: () => fallbackConfig()[key] }])),\n) as PublicConfig;\nexport default config;\n"],"mappings":";AAgBA,SAAS,4BAA4B;AAgB9B,IAAM,oBAAoB;AAC1B,IAAM,eAAe;AAErB,IAAM,mBAAmB;AAGhC,SAAS,IAAI,MAAkC;AAC7C,QAAM,cAAc,OAAO,YAAY,cAAc,QAAQ,MAAM,IAAI,IAAI;AAC3E,MAAI,YAAa,QAAO;AACxB,MAAI;AACF,WAAS,YAAY,MAAyD,IAAI,KAAM;AAAA,EAC1F,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAM,OAAO,CAAC,QAAwB,IAAI,QAAQ,QAAQ,EAAE;AAC5D,IAAM,OAAO,CAAC,UAAwC,OAAO,UAAU,YAAY,QAAQ,QAAQ;AAM5F,SAAS,iBAA+B;AAC7C,QAAM,WAAY,qBAAqB,KAAK,CAAC;AAC7C,SAAO;AAAA,IACL,QAAQ,KAAK,IAAI,qBAAqB,KAAK,KAAK,SAAS,MAAM,KAAK,EAAE;AAAA,IACtE,WAAW,IAAI,uBAAuB,KAAK,KAAK,SAAS,SAAS,KAAK;AAAA,IACvE,WAAW,IAAI,wBAAwB,KAAK,KAAK,SAAS,SAAS;AAAA,IACnE,UAAU,KAAK,IAAI,uBAAuB,KAAK,iBAAiB;AAAA,IAChE,WAAW,IAAI,wBAAwB;AAAA,IACvC,aAAa;AAAA,EACf;AACF;AAEA,IAAM,OAAO,CAAC,UAAU,aAAa,aAAa,YAAY,aAAa,aAAa;AAEjF,IAAM,SAAS,OAAO;AAAA,EAC3B,CAAC;AAAA,EACD,OAAO,YAAY,KAAK,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,YAAY,MAAM,KAAK,MAAM,eAAe,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;AACrG;AACA,IAAO,wBAAQ;","names":[]}
1
+ {"version":3,"sources":["../src/public-config.ts"],"sourcesContent":["/**\n * The public config the shipped components read (`BcmsImage`, `BcmsForm`).\n *\n * Where `bettercms()` is registered, its Vite plugin answers this very specifier with the config it\n * resolved, options included (@see integration.ts). Where it is not, this file is what resolves:\n * the same values from the build's environment and its content snapshot.\n *\n * WHY A REAL MODULE. The components imported the virtual `bettercms:config`, which only the\n * integration can resolve. A site that registers it conditionally (ScrewFast:\n * `...(bcmsReady ? [bettercms({})] : [])`) could not compile a page that imported `<BcmsBlocks>`,\n * so `<BcmsSections>` was given no block renderer there and a section added in the editor had\n * nothing to draw it.\n *\n * No `node:fs` import: this is bundled into a server render, an edge runtime's included. Read on\n * use, never once at module scope: a server's environment and snapshot are the run's.\n */\nimport { readPublishedContent } from \"@bettercms-ai/sdk\";\nimport type { PublicConfig } from \"./config.js\";\n\n/**\n * The platform's canonical media host, matching `@bettercms-ai/image-url`'s own\n * `DEFAULT_BASE_URL` and the `CDN_URL` the backend mints asset URLs on.\n *\n * This used to be `media.bettercms.ai`. Both hostnames route to the backend — they are both in\n * `$bcms_api_host` in infra/nginx/backend-lb.conf — so nothing was broken, but a site built by\n * this integration disagreed with the URL the server itself hands out for the same asset. That\n * splits the CDN cache for identical bytes and makes an asset URL's origin depend on which code\n * path produced it, which is the exact drift image-url was moved off this host to end.\n *\n * A project that serves media from the API origin instead (`useCdn: false`) overrides this with\n * `PUBLIC_BCMS_MEDIA_URL` or the `mediaUrl` option.\n */\nexport { DEFAULT_MEDIA_URL } from \"@bettercms-ai/sdk/config\";\nexport { DRAFT_COOKIE } from \"@bettercms-ai/sdk/draft\";\nimport { DEFAULT_MEDIA_URL } from \"@bettercms-ai/sdk/config\";\nimport { DRAFT_COOKIE } from \"@bettercms-ai/sdk/draft\";\n/** The specifier the components import, and the integration's plugin answers. */\nexport const PUBLIC_CONFIG_ID = \"@bettercms-ai/astro/public-config\";\n\n/** The server's runtime env first (hosting injects it), then the build's. */\nfunction env(name: string): string | undefined {\n const fromProcess = typeof process !== \"undefined\" ? process.env?.[name] : undefined;\n if (fromProcess) return fromProcess;\n try {\n return ((import.meta.env as Record<string, string | undefined> | undefined)?.[name]) || undefined;\n } catch {\n return undefined;\n }\n}\n\nconst trim = (url: string): string => url.replace(/\\/+$/, \"\");\nconst text = (value: unknown): string | undefined => (typeof value === \"string\" && value ? value : undefined);\n\n/**\n * The config with no integration: `PUBLIC_BCMS_*`, then the snapshot. Never throws: with neither,\n * `apiUrl` and `workspace` are empty, which a build that never talks to the API renders with.\n */\nexport function fallbackConfig(): PublicConfig {\n const snapshot = (readPublishedContent() ?? {}) as { apiUrl?: unknown; workspace?: unknown; projectId?: unknown };\n return {\n apiUrl: trim(env(\"PUBLIC_BCMS_API_URL\") ?? text(snapshot.apiUrl) ?? \"\"),\n workspace: env(\"PUBLIC_BCMS_WORKSPACE\") ?? text(snapshot.workspace) ?? \"\",\n projectId: env(\"PUBLIC_BCMS_PROJECT_ID\") ?? text(snapshot.projectId),\n mediaUrl: trim(env(\"PUBLIC_BCMS_MEDIA_URL\") ?? DEFAULT_MEDIA_URL),\n studioUrl: env(\"PUBLIC_BCMS_STUDIO_URL\"),\n draftCookie: DRAFT_COOKIE,\n };\n}\n\nconst KEYS = [\"apiUrl\", \"workspace\", \"projectId\", \"mediaUrl\", \"studioUrl\", \"draftCookie\"] as const;\n\nexport const config = Object.defineProperties(\n {},\n Object.fromEntries(KEYS.map((key) => [key, { enumerable: true, get: () => fallbackConfig()[key] }])),\n) as PublicConfig;\nexport default config;\n"],"mappings":";AAgBA,SAAS,4BAA4B;AAgBrC,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;AAC7B,SAAS,qBAAAA,0BAAyB;AAClC,SAAS,gBAAAC,qBAAoB;AAEtB,IAAM,mBAAmB;AAGhC,SAAS,IAAI,MAAkC;AAC7C,QAAM,cAAc,OAAO,YAAY,cAAc,QAAQ,MAAM,IAAI,IAAI;AAC3E,MAAI,YAAa,QAAO;AACxB,MAAI;AACF,WAAS,YAAY,MAAyD,IAAI,KAAM;AAAA,EAC1F,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAM,OAAO,CAAC,QAAwB,IAAI,QAAQ,QAAQ,EAAE;AAC5D,IAAM,OAAO,CAAC,UAAwC,OAAO,UAAU,YAAY,QAAQ,QAAQ;AAM5F,SAAS,iBAA+B;AAC7C,QAAM,WAAY,qBAAqB,KAAK,CAAC;AAC7C,SAAO;AAAA,IACL,QAAQ,KAAK,IAAI,qBAAqB,KAAK,KAAK,SAAS,MAAM,KAAK,EAAE;AAAA,IACtE,WAAW,IAAI,uBAAuB,KAAK,KAAK,SAAS,SAAS,KAAK;AAAA,IACvE,WAAW,IAAI,wBAAwB,KAAK,KAAK,SAAS,SAAS;AAAA,IACnE,UAAU,KAAK,IAAI,uBAAuB,KAAKD,kBAAiB;AAAA,IAChE,WAAW,IAAI,wBAAwB;AAAA,IACvC,aAAaC;AAAA,EACf;AACF;AAEA,IAAM,OAAO,CAAC,UAAU,aAAa,aAAa,YAAY,aAAa,aAAa;AAEjF,IAAM,SAAS,OAAO;AAAA,EAC3B,CAAC;AAAA,EACD,OAAO,YAAY,KAAK,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,YAAY,MAAM,KAAK,MAAM,eAAe,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;AACrG;AACA,IAAO,wBAAQ;","names":["DEFAULT_MEDIA_URL","DRAFT_COOKIE"]}
@@ -4,10 +4,7 @@ import config from "bettercms:config";
4
4
  // src/runtime.ts
5
5
  import { createClient } from "@bettercms-ai/sdk";
6
6
  import { readPublishedContent, publishedEntry } from "@bettercms-ai/sdk";
7
- var enc = new TextEncoder();
8
- function safeRedirectPath(to) {
9
- return to.startsWith("/") && !to.startsWith("//") ? to : "/";
10
- }
7
+ import { signValue, verifyValue, safeRedirectPath, decodeTokenSlug } from "@bettercms-ai/sdk/draft";
11
8
 
12
9
  // src/routes/draft-disable.ts
13
10
  var GET = async (context) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/routes/draft-disable.ts","../../src/runtime.ts"],"sourcesContent":["/**\n * GET /api/bcms/draft/disable?redirect=/path — clear the draft cookie.\n */\nimport type { APIRoute } from \"astro\";\nimport config from \"bettercms:config\";\nimport { safeRedirectPath } from \"../runtime.js\";\n\nexport const GET: APIRoute = async (context) => {\n const redirectTo = new URL(context.request.url).searchParams.get(\"redirect\") ?? \"/\";\n context.cookies.delete(config.draftCookie, { path: \"/\" });\n return context.redirect(safeRedirectPath(redirectTo), 302);\n};\n","/**\n * Runtime for @bettercms-ai/astro — the code the `bettercms:client` virtual\n * module binds to. Provides a published client, per-request draft client\n * selection, typed loaders, and signed-cookie helpers for draft mode.\n *\n * Read-only; no I/O at import time. Safe in Node and edge runtimes (uses Web\n * Crypto + global fetch only).\n */\nimport { createClient, type BetterCMSReadClient } from \"@bettercms-ai/sdk\";\nimport type {\n DeliveredLayout,\n DeliveryComponent,\n DeliveryEntry,\n DeliveryList,\n DeliveryPage,\n} from \"@bettercms-ai/types\";\n\nexport interface RuntimeConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n /** Server-only delivery key (a `content:read:draft` key for drafts). */\n apiKey?: string;\n /**\n * Collapse identical delivery GETs for the life of the process. Set by the integration\n * when Astro's `command === \"build\"` — a static build renders every route from one\n * published dataset, so a repeat read cannot legitimately differ.\n *\n * Never set for `dev` or SSR, where each request must see current content.\n */\n buildCache?: boolean;\n}\n\n/** Per-request draft state, set on `Astro.locals.bcms` by the middleware. */\nexport interface BcmsLocals {\n draft: boolean;\n token?: string;\n}\n\ninterface HasLocals {\n locals: { bcms?: BcmsLocals };\n}\n\nexport interface BetterCMSRuntime {\n /** Published-perspective client (use for SSG / non-draft reads). */\n client: BetterCMSReadClient;\n /** Pick the right client for a request (draft client when draft mode is on). */\n getClient(astro?: HasLocals): BetterCMSReadClient;\n loadEntry<T = Record<string, unknown>>(\n astro: HasLocals,\n slug: string,\n opts?: Parameters<BetterCMSReadClient[\"getEntry\"]>[1],\n ): Promise<DeliveryEntry<T>>;\n loadEntries<T = Record<string, unknown>>(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listEntries\"]>[0],\n ): Promise<DeliveryList<DeliveryEntry<T>>>;\n loadPage(astro: HasLocals, slug: string): Promise<DeliveryPage | null>;\n /** Component definitions for `<BcmsBlocks components={…}>` — draft-side in draft mode. */\n loadComponents(astro: HasLocals): Promise<DeliveryComponent[]>;\n loadLayout(astro: HasLocals): Promise<DeliveredLayout | null>;\n loadForms(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listForms\"]>[0],\n ): Promise<Awaited<ReturnType<BetterCMSReadClient[\"listForms\"]>>>;\n}\n\nexport function createRuntime(config: RuntimeConfig): BetterCMSRuntime {\n const { buildCache, ...clientConfig } = config;\n // One client for the whole build, so the cache below is shared across every route\n // rather than rebuilt per render.\n const published = createClient({ ...clientConfig, perspective: \"published\", cache: buildCache });\n\n const getClient = (astro?: HasLocals): BetterCMSReadClient => {\n const bcms = astro?.locals?.bcms;\n if (bcms?.draft) {\n // Explicitly uncached: a draft read exists to show the edit that just happened.\n return createClient({\n ...clientConfig,\n perspective: \"drafts\",\n previewToken: bcms.token,\n cache: false,\n });\n }\n return published;\n };\n\n return {\n client: published,\n getClient,\n loadEntry: (astro, slug, opts) => getClient(astro).getEntry(slug, opts),\n loadEntries: (astro, opts) => getClient(astro).listEntries(opts),\n loadPage: (astro, slug) => getClient(astro).getPage(slug),\n loadComponents: (astro) => getClient(astro).listComponents(),\n loadLayout: (astro) => getClient(astro).getLayout(),\n loadForms: (astro, opts) => getClient(astro).listForms(opts),\n };\n}\n\n/**\n * Build the live SSE URL for `<BcmsLive>`. The key is a query param because\n * browser EventSource cannot send headers — use a content:read key.\n *\n * `depth` sets the hydration depth of the document the stream pushes (default 1,\n * matching the delivery API). `drafts` streams autosaves so a preview surface\n * repaints as the editor types — it needs a `content:read:draft` key, and the\n * server ignores the flag for any other key rather than failing the connection.\n * Never enable it on a public site.\n */\nexport function liveSrc(opts: {\n apiUrl: string;\n workspace: string;\n apiKey: string;\n depth?: 0 | 1 | 2;\n drafts?: boolean;\n}): string {\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const q = new URLSearchParams({ key: opts.apiKey });\n if (opts.depth !== undefined) q.set(\"depth\", String(opts.depth));\n if (opts.drafts) q.set(\"drafts\", \"1\");\n return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;\n}\n\n/**\n * The published content snapshot, read at request time on a server BetterCMS hosts (it rewrites the\n * file on every publish) and from `bcms-content.json` at build time. Read content through this —\n * never `import` bcms-content.json or `import.meta.glob` it: a bundled copy is frozen at build, and\n * the platform has to rebuild the whole site for every publish to change it.\n */\nexport { readPublishedContent, publishedEntry, type PublishedContent } from \"@bettercms-ai/sdk\";\n\n// ── Signed-cookie helpers (HMAC-SHA256, base64url) ──────────────────────────\n\nconst enc = new TextEncoder();\n\nfunction base64url(bytes: ArrayBuffer): string {\n const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));\n return b.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function hmac(value: string, secret: string): Promise<string> {\n const key = await crypto.subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n return base64url(await crypto.subtle.sign(\"HMAC\", key, enc.encode(value)));\n}\n\n/** Sign a value as `value.sig` for tamper-evident cookie storage. */\nexport async function signValue(value: string, secret: string): Promise<string> {\n return `${value}.${await hmac(value, secret)}`;\n}\n\n/** Verify a signed value, returning the original value or null if tampered. */\nexport async function verifyValue(signed: string, secret: string): Promise<string | null> {\n const idx = signed.lastIndexOf(\".\");\n if (idx < 0) return null;\n const value = signed.slice(0, idx);\n const sig = signed.slice(idx + 1);\n const expected = await hmac(value, secret);\n // Constant-time-ish compare via length + char accumulation.\n if (sig.length !== expected.length) return null;\n let diff = 0;\n for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);\n return diff === 0 ? value : null;\n}\n\n/**\n * The draft-cookie signing secret, `process.env` FIRST.\n *\n * Vite may INLINE `import.meta.env.BCMS_DRAFT_SECRET` at build time, and the value present\n * when the customer's site is built is not the value the hosting container is given at start\n * — BetterCMS derives it per project and injects it as a real env var. Reading the runtime\n * env first is what makes managed hosting work without asking anyone to rebuild. The\n * `import.meta.env` read stays as the fallback for `astro dev` and for adapters with no\n * `process` (guarded, since touching `import.meta.env` outside Vite throws).\n */\nexport function draftSecret(): string | undefined {\n const fromProcess =\n typeof process !== \"undefined\" ? (process.env?.BCMS_DRAFT_SECRET as string | undefined) : undefined;\n if (fromProcess) return fromProcess;\n try {\n return import.meta.env?.BCMS_DRAFT_SECRET as string | undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Only same-origin relative paths. Blocks \"//evil.com\", \"https://evil.com\", etc. */\nexport function safeRedirectPath(to: string): string {\n return to.startsWith(\"/\") && !to.startsWith(\"//\") ? to : \"/\";\n}\n\n/** Decode a preview-token JWT payload (UNVERIFIED) to read its `entrySlug`. */\nexport function decodeTokenSlug(token: string): string | null {\n try {\n const part = token.split(\".\")[1];\n if (!part) return null;\n const json = JSON.parse(\n atob(part.replace(/-/g, \"+\").replace(/_/g, \"/\")),\n ) as { entrySlug?: string };\n return json.entrySlug ?? null;\n } catch {\n return null;\n }\n}\n"],"mappings":";AAIA,OAAO,YAAY;;;ACInB,SAAS,oBAA8C;AAyHvD,SAAS,sBAAsB,sBAA6C;AAI5E,IAAM,MAAM,IAAI,YAAY;AA2DrB,SAAS,iBAAiB,IAAoB;AACnD,SAAO,GAAG,WAAW,GAAG,KAAK,CAAC,GAAG,WAAW,IAAI,IAAI,KAAK;AAC3D;;;AD3LO,IAAM,MAAgB,OAAO,YAAY;AAC9C,QAAM,aAAa,IAAI,IAAI,QAAQ,QAAQ,GAAG,EAAE,aAAa,IAAI,UAAU,KAAK;AAChF,UAAQ,QAAQ,OAAO,OAAO,aAAa,EAAE,MAAM,IAAI,CAAC;AACxD,SAAO,QAAQ,SAAS,iBAAiB,UAAU,GAAG,GAAG;AAC3D;","names":[]}
1
+ {"version":3,"sources":["../../src/routes/draft-disable.ts","../../src/runtime.ts"],"sourcesContent":["/**\n * GET /api/bcms/draft/disable?redirect=/path — clear the draft cookie.\n */\nimport type { APIRoute } from \"astro\";\nimport config from \"bettercms:config\";\nimport { safeRedirectPath } from \"../runtime.js\";\n\nexport const GET: APIRoute = async (context) => {\n const redirectTo = new URL(context.request.url).searchParams.get(\"redirect\") ?? \"/\";\n context.cookies.delete(config.draftCookie, { path: \"/\" });\n return context.redirect(safeRedirectPath(redirectTo), 302);\n};\n","/**\n * Runtime for @bettercms-ai/astro — the code the `bettercms:client` virtual\n * module binds to. Provides a published client, per-request draft client\n * selection, typed loaders, and signed-cookie helpers for draft mode.\n *\n * Read-only; no I/O at import time. Safe in Node and edge runtimes (uses Web\n * Crypto + global fetch only).\n */\nimport { createClient, type BetterCMSReadClient } from \"@bettercms-ai/sdk\";\nimport type {\n DeliveredLayout,\n DeliveryComponent,\n DeliveryEntry,\n DeliveryList,\n DeliveryPage,\n} from \"@bettercms-ai/types\";\n\nexport interface RuntimeConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n /** Server-only delivery key (a `content:read:draft` key for drafts). */\n apiKey?: string;\n /**\n * Collapse identical delivery GETs for the life of the process. Set by the integration\n * when Astro's `command === \"build\"` — a static build renders every route from one\n * published dataset, so a repeat read cannot legitimately differ.\n *\n * Never set for `dev` or SSR, where each request must see current content.\n */\n buildCache?: boolean;\n}\n\n/** Per-request draft state, set on `Astro.locals.bcms` by the middleware. */\nexport interface BcmsLocals {\n draft: boolean;\n token?: string;\n}\n\ninterface HasLocals {\n locals: { bcms?: BcmsLocals };\n}\n\nexport interface BetterCMSRuntime {\n /** Published-perspective client (use for SSG / non-draft reads). */\n client: BetterCMSReadClient;\n /** Pick the right client for a request (draft client when draft mode is on). */\n getClient(astro?: HasLocals): BetterCMSReadClient;\n loadEntry<T = Record<string, unknown>>(\n astro: HasLocals,\n slug: string,\n opts?: Parameters<BetterCMSReadClient[\"getEntry\"]>[1],\n ): Promise<DeliveryEntry<T>>;\n loadEntries<T = Record<string, unknown>>(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listEntries\"]>[0],\n ): Promise<DeliveryList<DeliveryEntry<T>>>;\n loadPage(astro: HasLocals, slug: string): Promise<DeliveryPage | null>;\n /** Component definitions for `<BcmsBlocks components={…}>` — draft-side in draft mode. */\n loadComponents(astro: HasLocals): Promise<DeliveryComponent[]>;\n loadLayout(astro: HasLocals): Promise<DeliveredLayout | null>;\n loadForms(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listForms\"]>[0],\n ): Promise<Awaited<ReturnType<BetterCMSReadClient[\"listForms\"]>>>;\n}\n\nexport function createRuntime(config: RuntimeConfig): BetterCMSRuntime {\n const { buildCache, ...clientConfig } = config;\n // One client for the whole build, so the cache below is shared across every route\n // rather than rebuilt per render.\n const published = createClient({ ...clientConfig, perspective: \"published\", cache: buildCache });\n\n const getClient = (astro?: HasLocals): BetterCMSReadClient => {\n const bcms = astro?.locals?.bcms;\n if (bcms?.draft) {\n // Explicitly uncached: a draft read exists to show the edit that just happened.\n return createClient({\n ...clientConfig,\n perspective: \"drafts\",\n previewToken: bcms.token,\n cache: false,\n });\n }\n return published;\n };\n\n return {\n client: published,\n getClient,\n loadEntry: (astro, slug, opts) => getClient(astro).getEntry(slug, opts),\n loadEntries: (astro, opts) => getClient(astro).listEntries(opts),\n loadPage: (astro, slug) => getClient(astro).getPage(slug),\n loadComponents: (astro) => getClient(astro).listComponents(),\n loadLayout: (astro) => getClient(astro).getLayout(),\n loadForms: (astro, opts) => getClient(astro).listForms(opts),\n };\n}\n\n/**\n * Build the live SSE URL for `<BcmsLive>`. The key is a query param because\n * browser EventSource cannot send headers — use a content:read key.\n *\n * `depth` sets the hydration depth of the document the stream pushes (default 1,\n * matching the delivery API). `drafts` streams autosaves so a preview surface\n * repaints as the editor types — it needs a `content:read:draft` key, and the\n * server ignores the flag for any other key rather than failing the connection.\n * Never enable it on a public site.\n */\nexport function liveSrc(opts: {\n apiUrl: string;\n workspace: string;\n apiKey: string;\n depth?: 0 | 1 | 2;\n drafts?: boolean;\n}): string {\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const q = new URLSearchParams({ key: opts.apiKey });\n if (opts.depth !== undefined) q.set(\"depth\", String(opts.depth));\n if (opts.drafts) q.set(\"drafts\", \"1\");\n return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;\n}\n\n/**\n * The published content snapshot, read at request time on a server BetterCMS hosts (it rewrites the\n * file on every publish) and from `bcms-content.json` at build time. Read content through this —\n * never `import` bcms-content.json or `import.meta.glob` it: a bundled copy is frozen at build, and\n * the platform has to rebuild the whole site for every publish to change it.\n */\nexport { readPublishedContent, publishedEntry, type PublishedContent } from \"@bettercms-ai/sdk\";\n\n// Signed-cookie and draft-route helpers: one implementation for every adapter (@bettercms-ai/sdk/draft).\nexport { signValue, verifyValue, safeRedirectPath, decodeTokenSlug } from \"@bettercms-ai/sdk/draft\";\n\n/**\n * The draft-cookie signing secret, `process.env` FIRST.\n *\n * Vite may INLINE `import.meta.env.BCMS_DRAFT_SECRET` at build time, and the value present\n * when the customer's site is built is not the value the hosting container is given at start\n * — BetterCMS derives it per project and injects it as a real env var. Reading the runtime\n * env first is what makes managed hosting work without asking anyone to rebuild. The\n * `import.meta.env` read stays as the fallback for `astro dev` and for adapters with no\n * `process` (guarded, since touching `import.meta.env` outside Vite throws).\n */\nexport function draftSecret(): string | undefined {\n const fromProcess =\n typeof process !== \"undefined\" ? (process.env?.BCMS_DRAFT_SECRET as string | undefined) : undefined;\n if (fromProcess) return fromProcess;\n try {\n return import.meta.env?.BCMS_DRAFT_SECRET as string | undefined;\n } catch {\n return undefined;\n }\n}\n"],"mappings":";AAIA,OAAO,YAAY;;;ACInB,SAAS,oBAA8C;AAyHvD,SAAS,sBAAsB,sBAA6C;AAG5E,SAAS,WAAW,aAAa,kBAAkB,uBAAuB;;;AD7HnE,IAAM,MAAgB,OAAO,YAAY;AAC9C,QAAM,aAAa,IAAI,IAAI,QAAQ,QAAQ,GAAG,EAAE,aAAa,IAAI,UAAU,KAAK;AAChF,UAAQ,QAAQ,OAAO,OAAO,aAAa,EAAE,MAAM,IAAI,CAAC;AACxD,SAAO,QAAQ,SAAS,iBAAiB,UAAU,GAAG,GAAG;AAC3D;","names":[]}
@@ -4,24 +4,7 @@ import config from "bettercms:config";
4
4
  // src/runtime.ts
5
5
  import { createClient } from "@bettercms-ai/sdk";
6
6
  import { readPublishedContent, publishedEntry } from "@bettercms-ai/sdk";
7
- var enc = new TextEncoder();
8
- function base64url(bytes) {
9
- const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));
10
- return b.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
11
- }
12
- async function hmac(value, secret) {
13
- const key = await crypto.subtle.importKey(
14
- "raw",
15
- enc.encode(secret),
16
- { name: "HMAC", hash: "SHA-256" },
17
- false,
18
- ["sign"]
19
- );
20
- return base64url(await crypto.subtle.sign("HMAC", key, enc.encode(value)));
21
- }
22
- async function signValue(value, secret) {
23
- return `${value}.${await hmac(value, secret)}`;
24
- }
7
+ import { signValue, verifyValue, safeRedirectPath, decodeTokenSlug } from "@bettercms-ai/sdk/draft";
25
8
  function draftSecret() {
26
9
  const fromProcess = typeof process !== "undefined" ? process.env?.BCMS_DRAFT_SECRET : void 0;
27
10
  if (fromProcess) return fromProcess;
@@ -31,21 +14,6 @@ function draftSecret() {
31
14
  return void 0;
32
15
  }
33
16
  }
34
- function safeRedirectPath(to) {
35
- return to.startsWith("/") && !to.startsWith("//") ? to : "/";
36
- }
37
- function decodeTokenSlug(token) {
38
- try {
39
- const part = token.split(".")[1];
40
- if (!part) return null;
41
- const json = JSON.parse(
42
- atob(part.replace(/-/g, "+").replace(/_/g, "/"))
43
- );
44
- return json.entrySlug ?? null;
45
- } catch {
46
- return null;
47
- }
48
- }
49
17
 
50
18
  // src/routes/draft-enable.ts
51
19
  var GET = async (context) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/routes/draft-enable.ts","../../src/runtime.ts"],"sourcesContent":["/**\n * GET /api/bcms/draft/enable?token=<jwt>&redirect=/path\n *\n * Validates the preview token against the backend `/preview/:slug` endpoint\n * (the backend is the signature authority), then sets a signed draft cookie and\n * redirects. Requires `BCMS_DRAFT_SECRET` in the server env.\n */\nimport type { APIRoute } from \"astro\";\nimport config from \"bettercms:config\";\nimport { decodeTokenSlug, draftSecret, safeRedirectPath, signValue } from \"../runtime.js\";\n\nexport const GET: APIRoute = async (context) => {\n const url = new URL(context.request.url);\n const token = url.searchParams.get(\"token\");\n const redirectTo = url.searchParams.get(\"redirect\") ?? \"/\";\n const secret = draftSecret();\n\n if (!secret) return new Response(\"BCMS_DRAFT_SECRET is not configured\", { status: 500 });\n if (!token) return new Response(\"Missing token\", { status: 400 });\n\n const slug = decodeTokenSlug(token);\n if (!slug) return new Response(\"Malformed token\", { status: 400 });\n\n const res = await fetch(\n `${config.apiUrl}/api/v1/preview/${encodeURIComponent(slug)}?token=${encodeURIComponent(token)}`,\n );\n if (!res.ok) return new Response(\"Invalid or expired token\", { status: 401 });\n\n context.cookies.set(config.draftCookie, await signValue(token, secret), {\n httpOnly: true,\n sameSite: \"lax\",\n secure: url.protocol === \"https:\",\n path: \"/\",\n maxAge: 60 * 60 * 24, // 24h, matching the preview-token lifetime\n });\n\n return context.redirect(safeRedirectPath(redirectTo), 302);\n};\n","/**\n * Runtime for @bettercms-ai/astro — the code the `bettercms:client` virtual\n * module binds to. Provides a published client, per-request draft client\n * selection, typed loaders, and signed-cookie helpers for draft mode.\n *\n * Read-only; no I/O at import time. Safe in Node and edge runtimes (uses Web\n * Crypto + global fetch only).\n */\nimport { createClient, type BetterCMSReadClient } from \"@bettercms-ai/sdk\";\nimport type {\n DeliveredLayout,\n DeliveryComponent,\n DeliveryEntry,\n DeliveryList,\n DeliveryPage,\n} from \"@bettercms-ai/types\";\n\nexport interface RuntimeConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n /** Server-only delivery key (a `content:read:draft` key for drafts). */\n apiKey?: string;\n /**\n * Collapse identical delivery GETs for the life of the process. Set by the integration\n * when Astro's `command === \"build\"` — a static build renders every route from one\n * published dataset, so a repeat read cannot legitimately differ.\n *\n * Never set for `dev` or SSR, where each request must see current content.\n */\n buildCache?: boolean;\n}\n\n/** Per-request draft state, set on `Astro.locals.bcms` by the middleware. */\nexport interface BcmsLocals {\n draft: boolean;\n token?: string;\n}\n\ninterface HasLocals {\n locals: { bcms?: BcmsLocals };\n}\n\nexport interface BetterCMSRuntime {\n /** Published-perspective client (use for SSG / non-draft reads). */\n client: BetterCMSReadClient;\n /** Pick the right client for a request (draft client when draft mode is on). */\n getClient(astro?: HasLocals): BetterCMSReadClient;\n loadEntry<T = Record<string, unknown>>(\n astro: HasLocals,\n slug: string,\n opts?: Parameters<BetterCMSReadClient[\"getEntry\"]>[1],\n ): Promise<DeliveryEntry<T>>;\n loadEntries<T = Record<string, unknown>>(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listEntries\"]>[0],\n ): Promise<DeliveryList<DeliveryEntry<T>>>;\n loadPage(astro: HasLocals, slug: string): Promise<DeliveryPage | null>;\n /** Component definitions for `<BcmsBlocks components={…}>` — draft-side in draft mode. */\n loadComponents(astro: HasLocals): Promise<DeliveryComponent[]>;\n loadLayout(astro: HasLocals): Promise<DeliveredLayout | null>;\n loadForms(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listForms\"]>[0],\n ): Promise<Awaited<ReturnType<BetterCMSReadClient[\"listForms\"]>>>;\n}\n\nexport function createRuntime(config: RuntimeConfig): BetterCMSRuntime {\n const { buildCache, ...clientConfig } = config;\n // One client for the whole build, so the cache below is shared across every route\n // rather than rebuilt per render.\n const published = createClient({ ...clientConfig, perspective: \"published\", cache: buildCache });\n\n const getClient = (astro?: HasLocals): BetterCMSReadClient => {\n const bcms = astro?.locals?.bcms;\n if (bcms?.draft) {\n // Explicitly uncached: a draft read exists to show the edit that just happened.\n return createClient({\n ...clientConfig,\n perspective: \"drafts\",\n previewToken: bcms.token,\n cache: false,\n });\n }\n return published;\n };\n\n return {\n client: published,\n getClient,\n loadEntry: (astro, slug, opts) => getClient(astro).getEntry(slug, opts),\n loadEntries: (astro, opts) => getClient(astro).listEntries(opts),\n loadPage: (astro, slug) => getClient(astro).getPage(slug),\n loadComponents: (astro) => getClient(astro).listComponents(),\n loadLayout: (astro) => getClient(astro).getLayout(),\n loadForms: (astro, opts) => getClient(astro).listForms(opts),\n };\n}\n\n/**\n * Build the live SSE URL for `<BcmsLive>`. The key is a query param because\n * browser EventSource cannot send headers — use a content:read key.\n *\n * `depth` sets the hydration depth of the document the stream pushes (default 1,\n * matching the delivery API). `drafts` streams autosaves so a preview surface\n * repaints as the editor types — it needs a `content:read:draft` key, and the\n * server ignores the flag for any other key rather than failing the connection.\n * Never enable it on a public site.\n */\nexport function liveSrc(opts: {\n apiUrl: string;\n workspace: string;\n apiKey: string;\n depth?: 0 | 1 | 2;\n drafts?: boolean;\n}): string {\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const q = new URLSearchParams({ key: opts.apiKey });\n if (opts.depth !== undefined) q.set(\"depth\", String(opts.depth));\n if (opts.drafts) q.set(\"drafts\", \"1\");\n return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;\n}\n\n/**\n * The published content snapshot, read at request time on a server BetterCMS hosts (it rewrites the\n * file on every publish) and from `bcms-content.json` at build time. Read content through this —\n * never `import` bcms-content.json or `import.meta.glob` it: a bundled copy is frozen at build, and\n * the platform has to rebuild the whole site for every publish to change it.\n */\nexport { readPublishedContent, publishedEntry, type PublishedContent } from \"@bettercms-ai/sdk\";\n\n// ── Signed-cookie helpers (HMAC-SHA256, base64url) ──────────────────────────\n\nconst enc = new TextEncoder();\n\nfunction base64url(bytes: ArrayBuffer): string {\n const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));\n return b.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function hmac(value: string, secret: string): Promise<string> {\n const key = await crypto.subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n return base64url(await crypto.subtle.sign(\"HMAC\", key, enc.encode(value)));\n}\n\n/** Sign a value as `value.sig` for tamper-evident cookie storage. */\nexport async function signValue(value: string, secret: string): Promise<string> {\n return `${value}.${await hmac(value, secret)}`;\n}\n\n/** Verify a signed value, returning the original value or null if tampered. */\nexport async function verifyValue(signed: string, secret: string): Promise<string | null> {\n const idx = signed.lastIndexOf(\".\");\n if (idx < 0) return null;\n const value = signed.slice(0, idx);\n const sig = signed.slice(idx + 1);\n const expected = await hmac(value, secret);\n // Constant-time-ish compare via length + char accumulation.\n if (sig.length !== expected.length) return null;\n let diff = 0;\n for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);\n return diff === 0 ? value : null;\n}\n\n/**\n * The draft-cookie signing secret, `process.env` FIRST.\n *\n * Vite may INLINE `import.meta.env.BCMS_DRAFT_SECRET` at build time, and the value present\n * when the customer's site is built is not the value the hosting container is given at start\n * — BetterCMS derives it per project and injects it as a real env var. Reading the runtime\n * env first is what makes managed hosting work without asking anyone to rebuild. The\n * `import.meta.env` read stays as the fallback for `astro dev` and for adapters with no\n * `process` (guarded, since touching `import.meta.env` outside Vite throws).\n */\nexport function draftSecret(): string | undefined {\n const fromProcess =\n typeof process !== \"undefined\" ? (process.env?.BCMS_DRAFT_SECRET as string | undefined) : undefined;\n if (fromProcess) return fromProcess;\n try {\n return import.meta.env?.BCMS_DRAFT_SECRET as string | undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Only same-origin relative paths. Blocks \"//evil.com\", \"https://evil.com\", etc. */\nexport function safeRedirectPath(to: string): string {\n return to.startsWith(\"/\") && !to.startsWith(\"//\") ? to : \"/\";\n}\n\n/** Decode a preview-token JWT payload (UNVERIFIED) to read its `entrySlug`. */\nexport function decodeTokenSlug(token: string): string | null {\n try {\n const part = token.split(\".\")[1];\n if (!part) return null;\n const json = JSON.parse(\n atob(part.replace(/-/g, \"+\").replace(/_/g, \"/\")),\n ) as { entrySlug?: string };\n return json.entrySlug ?? null;\n } catch {\n return null;\n }\n}\n"],"mappings":";AAQA,OAAO,YAAY;;;ACAnB,SAAS,oBAA8C;AAyHvD,SAAS,sBAAsB,sBAA6C;AAI5E,IAAM,MAAM,IAAI,YAAY;AAE5B,SAAS,UAAU,OAA4B;AAC7C,QAAM,IAAI,KAAK,OAAO,aAAa,GAAG,IAAI,WAAW,KAAK,CAAC,CAAC;AAC5D,SAAO,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AACpE;AAEA,eAAe,KAAK,OAAe,QAAiC;AAClE,QAAM,MAAM,MAAM,OAAO,OAAO;AAAA,IAC9B;AAAA,IACA,IAAI,OAAO,MAAM;AAAA,IACjB,EAAE,MAAM,QAAQ,MAAM,UAAU;AAAA,IAChC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AACA,SAAO,UAAU,MAAM,OAAO,OAAO,KAAK,QAAQ,KAAK,IAAI,OAAO,KAAK,CAAC,CAAC;AAC3E;AAGA,eAAsB,UAAU,OAAe,QAAiC;AAC9E,SAAO,GAAG,KAAK,IAAI,MAAM,KAAK,OAAO,MAAM,CAAC;AAC9C;AA0BO,SAAS,cAAkC;AAChD,QAAM,cACJ,OAAO,YAAY,cAAe,QAAQ,KAAK,oBAA2C;AAC5F,MAAI,YAAa,QAAO;AACxB,MAAI;AACF,WAAO,YAAY,KAAK;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,iBAAiB,IAAoB;AACnD,SAAO,GAAG,WAAW,GAAG,KAAK,CAAC,GAAG,WAAW,IAAI,IAAI,KAAK;AAC3D;AAGO,SAAS,gBAAgB,OAA8B;AAC5D,MAAI;AACF,UAAM,OAAO,MAAM,MAAM,GAAG,EAAE,CAAC;AAC/B,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,OAAO,KAAK;AAAA,MAChB,KAAK,KAAK,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG,CAAC;AAAA,IACjD;AACA,WAAO,KAAK,aAAa;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ADrMO,IAAM,MAAgB,OAAO,YAAY;AAC9C,QAAM,MAAM,IAAI,IAAI,QAAQ,QAAQ,GAAG;AACvC,QAAM,QAAQ,IAAI,aAAa,IAAI,OAAO;AAC1C,QAAM,aAAa,IAAI,aAAa,IAAI,UAAU,KAAK;AACvD,QAAM,SAAS,YAAY;AAE3B,MAAI,CAAC,OAAQ,QAAO,IAAI,SAAS,uCAAuC,EAAE,QAAQ,IAAI,CAAC;AACvF,MAAI,CAAC,MAAO,QAAO,IAAI,SAAS,iBAAiB,EAAE,QAAQ,IAAI,CAAC;AAEhE,QAAM,OAAO,gBAAgB,KAAK;AAClC,MAAI,CAAC,KAAM,QAAO,IAAI,SAAS,mBAAmB,EAAE,QAAQ,IAAI,CAAC;AAEjE,QAAM,MAAM,MAAM;AAAA,IAChB,GAAG,OAAO,MAAM,mBAAmB,mBAAmB,IAAI,CAAC,UAAU,mBAAmB,KAAK,CAAC;AAAA,EAChG;AACA,MAAI,CAAC,IAAI,GAAI,QAAO,IAAI,SAAS,4BAA4B,EAAE,QAAQ,IAAI,CAAC;AAE5E,UAAQ,QAAQ,IAAI,OAAO,aAAa,MAAM,UAAU,OAAO,MAAM,GAAG;AAAA,IACtE,UAAU;AAAA,IACV,UAAU;AAAA,IACV,QAAQ,IAAI,aAAa;AAAA,IACzB,MAAM;AAAA,IACN,QAAQ,KAAK,KAAK;AAAA;AAAA,EACpB,CAAC;AAED,SAAO,QAAQ,SAAS,iBAAiB,UAAU,GAAG,GAAG;AAC3D;","names":[]}
1
+ {"version":3,"sources":["../../src/routes/draft-enable.ts","../../src/runtime.ts"],"sourcesContent":["/**\n * GET /api/bcms/draft/enable?token=<jwt>&redirect=/path\n *\n * Validates the preview token against the backend `/preview/:slug` endpoint\n * (the backend is the signature authority), then sets a signed draft cookie and\n * redirects. Requires `BCMS_DRAFT_SECRET` in the server env.\n */\nimport type { APIRoute } from \"astro\";\nimport config from \"bettercms:config\";\nimport { decodeTokenSlug, draftSecret, safeRedirectPath, signValue } from \"../runtime.js\";\n\nexport const GET: APIRoute = async (context) => {\n const url = new URL(context.request.url);\n const token = url.searchParams.get(\"token\");\n const redirectTo = url.searchParams.get(\"redirect\") ?? \"/\";\n const secret = draftSecret();\n\n if (!secret) return new Response(\"BCMS_DRAFT_SECRET is not configured\", { status: 500 });\n if (!token) return new Response(\"Missing token\", { status: 400 });\n\n const slug = decodeTokenSlug(token);\n if (!slug) return new Response(\"Malformed token\", { status: 400 });\n\n const res = await fetch(\n `${config.apiUrl}/api/v1/preview/${encodeURIComponent(slug)}?token=${encodeURIComponent(token)}`,\n );\n if (!res.ok) return new Response(\"Invalid or expired token\", { status: 401 });\n\n context.cookies.set(config.draftCookie, await signValue(token, secret), {\n httpOnly: true,\n sameSite: \"lax\",\n secure: url.protocol === \"https:\",\n path: \"/\",\n maxAge: 60 * 60 * 24, // 24h, matching the preview-token lifetime\n });\n\n return context.redirect(safeRedirectPath(redirectTo), 302);\n};\n","/**\n * Runtime for @bettercms-ai/astro — the code the `bettercms:client` virtual\n * module binds to. Provides a published client, per-request draft client\n * selection, typed loaders, and signed-cookie helpers for draft mode.\n *\n * Read-only; no I/O at import time. Safe in Node and edge runtimes (uses Web\n * Crypto + global fetch only).\n */\nimport { createClient, type BetterCMSReadClient } from \"@bettercms-ai/sdk\";\nimport type {\n DeliveredLayout,\n DeliveryComponent,\n DeliveryEntry,\n DeliveryList,\n DeliveryPage,\n} from \"@bettercms-ai/types\";\n\nexport interface RuntimeConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n /** Server-only delivery key (a `content:read:draft` key for drafts). */\n apiKey?: string;\n /**\n * Collapse identical delivery GETs for the life of the process. Set by the integration\n * when Astro's `command === \"build\"` — a static build renders every route from one\n * published dataset, so a repeat read cannot legitimately differ.\n *\n * Never set for `dev` or SSR, where each request must see current content.\n */\n buildCache?: boolean;\n}\n\n/** Per-request draft state, set on `Astro.locals.bcms` by the middleware. */\nexport interface BcmsLocals {\n draft: boolean;\n token?: string;\n}\n\ninterface HasLocals {\n locals: { bcms?: BcmsLocals };\n}\n\nexport interface BetterCMSRuntime {\n /** Published-perspective client (use for SSG / non-draft reads). */\n client: BetterCMSReadClient;\n /** Pick the right client for a request (draft client when draft mode is on). */\n getClient(astro?: HasLocals): BetterCMSReadClient;\n loadEntry<T = Record<string, unknown>>(\n astro: HasLocals,\n slug: string,\n opts?: Parameters<BetterCMSReadClient[\"getEntry\"]>[1],\n ): Promise<DeliveryEntry<T>>;\n loadEntries<T = Record<string, unknown>>(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listEntries\"]>[0],\n ): Promise<DeliveryList<DeliveryEntry<T>>>;\n loadPage(astro: HasLocals, slug: string): Promise<DeliveryPage | null>;\n /** Component definitions for `<BcmsBlocks components={…}>` — draft-side in draft mode. */\n loadComponents(astro: HasLocals): Promise<DeliveryComponent[]>;\n loadLayout(astro: HasLocals): Promise<DeliveredLayout | null>;\n loadForms(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listForms\"]>[0],\n ): Promise<Awaited<ReturnType<BetterCMSReadClient[\"listForms\"]>>>;\n}\n\nexport function createRuntime(config: RuntimeConfig): BetterCMSRuntime {\n const { buildCache, ...clientConfig } = config;\n // One client for the whole build, so the cache below is shared across every route\n // rather than rebuilt per render.\n const published = createClient({ ...clientConfig, perspective: \"published\", cache: buildCache });\n\n const getClient = (astro?: HasLocals): BetterCMSReadClient => {\n const bcms = astro?.locals?.bcms;\n if (bcms?.draft) {\n // Explicitly uncached: a draft read exists to show the edit that just happened.\n return createClient({\n ...clientConfig,\n perspective: \"drafts\",\n previewToken: bcms.token,\n cache: false,\n });\n }\n return published;\n };\n\n return {\n client: published,\n getClient,\n loadEntry: (astro, slug, opts) => getClient(astro).getEntry(slug, opts),\n loadEntries: (astro, opts) => getClient(astro).listEntries(opts),\n loadPage: (astro, slug) => getClient(astro).getPage(slug),\n loadComponents: (astro) => getClient(astro).listComponents(),\n loadLayout: (astro) => getClient(astro).getLayout(),\n loadForms: (astro, opts) => getClient(astro).listForms(opts),\n };\n}\n\n/**\n * Build the live SSE URL for `<BcmsLive>`. The key is a query param because\n * browser EventSource cannot send headers — use a content:read key.\n *\n * `depth` sets the hydration depth of the document the stream pushes (default 1,\n * matching the delivery API). `drafts` streams autosaves so a preview surface\n * repaints as the editor types — it needs a `content:read:draft` key, and the\n * server ignores the flag for any other key rather than failing the connection.\n * Never enable it on a public site.\n */\nexport function liveSrc(opts: {\n apiUrl: string;\n workspace: string;\n apiKey: string;\n depth?: 0 | 1 | 2;\n drafts?: boolean;\n}): string {\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const q = new URLSearchParams({ key: opts.apiKey });\n if (opts.depth !== undefined) q.set(\"depth\", String(opts.depth));\n if (opts.drafts) q.set(\"drafts\", \"1\");\n return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;\n}\n\n/**\n * The published content snapshot, read at request time on a server BetterCMS hosts (it rewrites the\n * file on every publish) and from `bcms-content.json` at build time. Read content through this —\n * never `import` bcms-content.json or `import.meta.glob` it: a bundled copy is frozen at build, and\n * the platform has to rebuild the whole site for every publish to change it.\n */\nexport { readPublishedContent, publishedEntry, type PublishedContent } from \"@bettercms-ai/sdk\";\n\n// Signed-cookie and draft-route helpers: one implementation for every adapter (@bettercms-ai/sdk/draft).\nexport { signValue, verifyValue, safeRedirectPath, decodeTokenSlug } from \"@bettercms-ai/sdk/draft\";\n\n/**\n * The draft-cookie signing secret, `process.env` FIRST.\n *\n * Vite may INLINE `import.meta.env.BCMS_DRAFT_SECRET` at build time, and the value present\n * when the customer's site is built is not the value the hosting container is given at start\n * — BetterCMS derives it per project and injects it as a real env var. Reading the runtime\n * env first is what makes managed hosting work without asking anyone to rebuild. The\n * `import.meta.env` read stays as the fallback for `astro dev` and for adapters with no\n * `process` (guarded, since touching `import.meta.env` outside Vite throws).\n */\nexport function draftSecret(): string | undefined {\n const fromProcess =\n typeof process !== \"undefined\" ? (process.env?.BCMS_DRAFT_SECRET as string | undefined) : undefined;\n if (fromProcess) return fromProcess;\n try {\n return import.meta.env?.BCMS_DRAFT_SECRET as string | undefined;\n } catch {\n return undefined;\n }\n}\n"],"mappings":";AAQA,OAAO,YAAY;;;ACAnB,SAAS,oBAA8C;AAyHvD,SAAS,sBAAsB,sBAA6C;AAG5E,SAAS,WAAW,aAAa,kBAAkB,uBAAuB;AAYnE,SAAS,cAAkC;AAChD,QAAM,cACJ,OAAO,YAAY,cAAe,QAAQ,KAAK,oBAA2C;AAC5F,MAAI,YAAa,QAAO;AACxB,MAAI;AACF,WAAO,YAAY,KAAK;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AD9IO,IAAM,MAAgB,OAAO,YAAY;AAC9C,QAAM,MAAM,IAAI,IAAI,QAAQ,QAAQ,GAAG;AACvC,QAAM,QAAQ,IAAI,aAAa,IAAI,OAAO;AAC1C,QAAM,aAAa,IAAI,aAAa,IAAI,UAAU,KAAK;AACvD,QAAM,SAAS,YAAY;AAE3B,MAAI,CAAC,OAAQ,QAAO,IAAI,SAAS,uCAAuC,EAAE,QAAQ,IAAI,CAAC;AACvF,MAAI,CAAC,MAAO,QAAO,IAAI,SAAS,iBAAiB,EAAE,QAAQ,IAAI,CAAC;AAEhE,QAAM,OAAO,gBAAgB,KAAK;AAClC,MAAI,CAAC,KAAM,QAAO,IAAI,SAAS,mBAAmB,EAAE,QAAQ,IAAI,CAAC;AAEjE,QAAM,MAAM,MAAM;AAAA,IAChB,GAAG,OAAO,MAAM,mBAAmB,mBAAmB,IAAI,CAAC,UAAU,mBAAmB,KAAK,CAAC;AAAA,EAChG;AACA,MAAI,CAAC,IAAI,GAAI,QAAO,IAAI,SAAS,4BAA4B,EAAE,QAAQ,IAAI,CAAC;AAE5E,UAAQ,QAAQ,IAAI,OAAO,aAAa,MAAM,UAAU,OAAO,MAAM,GAAG;AAAA,IACtE,UAAU;AAAA,IACV,UAAU;AAAA,IACV,QAAQ,IAAI,aAAa;AAAA,IACzB,MAAM;AAAA,IACN,QAAQ,KAAK,KAAK;AAAA;AAAA,EACpB,CAAC;AAED,SAAO,QAAQ,SAAS,iBAAiB,UAAU,GAAG,GAAG;AAC3D;","names":[]}
package/dist/runtime.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { BetterCMSReadClient } from '@bettercms-ai/sdk';
2
2
  export { PublishedContent, publishedEntry, readPublishedContent } from '@bettercms-ai/sdk';
3
3
  import { DeliveryEntry, DeliveryList, DeliveryPage, DeliveryComponent, DeliveredLayout } from '@bettercms-ai/types';
4
+ export { decodeTokenSlug, safeRedirectPath, signValue, verifyValue } from '@bettercms-ai/sdk/draft';
4
5
 
5
6
  /**
6
7
  * Runtime for @bettercms-ai/astro — the code the `bettercms:client` virtual
@@ -68,10 +69,6 @@ declare function liveSrc(opts: {
68
69
  drafts?: boolean;
69
70
  }): string;
70
71
 
71
- /** Sign a value as `value.sig` for tamper-evident cookie storage. */
72
- declare function signValue(value: string, secret: string): Promise<string>;
73
- /** Verify a signed value, returning the original value or null if tampered. */
74
- declare function verifyValue(signed: string, secret: string): Promise<string | null>;
75
72
  /**
76
73
  * The draft-cookie signing secret, `process.env` FIRST.
77
74
  *
@@ -83,9 +80,5 @@ declare function verifyValue(signed: string, secret: string): Promise<string | n
83
80
  * `process` (guarded, since touching `import.meta.env` outside Vite throws).
84
81
  */
85
82
  declare function draftSecret(): string | undefined;
86
- /** Only same-origin relative paths. Blocks "//evil.com", "https://evil.com", etc. */
87
- declare function safeRedirectPath(to: string): string;
88
- /** Decode a preview-token JWT payload (UNVERIFIED) to read its `entrySlug`. */
89
- declare function decodeTokenSlug(token: string): string | null;
90
83
 
91
- export { type BcmsLocals, type BetterCMSRuntime, type RuntimeConfig, createRuntime, decodeTokenSlug, draftSecret, liveSrc, safeRedirectPath, signValue, verifyValue };
84
+ export { type BcmsLocals, type BetterCMSRuntime, type RuntimeConfig, createRuntime, draftSecret, liveSrc };
package/dist/runtime.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/runtime.ts
2
2
  import { createClient } from "@bettercms-ai/sdk";
3
3
  import { readPublishedContent, publishedEntry } from "@bettercms-ai/sdk";
4
+ import { signValue, verifyValue, safeRedirectPath, decodeTokenSlug } from "@bettercms-ai/sdk/draft";
4
5
  function createRuntime(config) {
5
6
  const { buildCache, ...clientConfig } = config;
6
7
  const published = createClient({ ...clientConfig, perspective: "published", cache: buildCache });
@@ -34,35 +35,6 @@ function liveSrc(opts) {
34
35
  if (opts.drafts) q.set("drafts", "1");
35
36
  return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;
36
37
  }
37
- var enc = new TextEncoder();
38
- function base64url(bytes) {
39
- const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));
40
- return b.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
41
- }
42
- async function hmac(value, secret) {
43
- const key = await crypto.subtle.importKey(
44
- "raw",
45
- enc.encode(secret),
46
- { name: "HMAC", hash: "SHA-256" },
47
- false,
48
- ["sign"]
49
- );
50
- return base64url(await crypto.subtle.sign("HMAC", key, enc.encode(value)));
51
- }
52
- async function signValue(value, secret) {
53
- return `${value}.${await hmac(value, secret)}`;
54
- }
55
- async function verifyValue(signed, secret) {
56
- const idx = signed.lastIndexOf(".");
57
- if (idx < 0) return null;
58
- const value = signed.slice(0, idx);
59
- const sig = signed.slice(idx + 1);
60
- const expected = await hmac(value, secret);
61
- if (sig.length !== expected.length) return null;
62
- let diff = 0;
63
- for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);
64
- return diff === 0 ? value : null;
65
- }
66
38
  function draftSecret() {
67
39
  const fromProcess = typeof process !== "undefined" ? process.env?.BCMS_DRAFT_SECRET : void 0;
68
40
  if (fromProcess) return fromProcess;
@@ -72,21 +44,6 @@ function draftSecret() {
72
44
  return void 0;
73
45
  }
74
46
  }
75
- function safeRedirectPath(to) {
76
- return to.startsWith("/") && !to.startsWith("//") ? to : "/";
77
- }
78
- function decodeTokenSlug(token) {
79
- try {
80
- const part = token.split(".")[1];
81
- if (!part) return null;
82
- const json = JSON.parse(
83
- atob(part.replace(/-/g, "+").replace(/_/g, "/"))
84
- );
85
- return json.entrySlug ?? null;
86
- } catch {
87
- return null;
88
- }
89
- }
90
47
  export {
91
48
  createRuntime,
92
49
  decodeTokenSlug,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/runtime.ts"],"sourcesContent":["/**\n * Runtime for @bettercms-ai/astro — the code the `bettercms:client` virtual\n * module binds to. Provides a published client, per-request draft client\n * selection, typed loaders, and signed-cookie helpers for draft mode.\n *\n * Read-only; no I/O at import time. Safe in Node and edge runtimes (uses Web\n * Crypto + global fetch only).\n */\nimport { createClient, type BetterCMSReadClient } from \"@bettercms-ai/sdk\";\nimport type {\n DeliveredLayout,\n DeliveryComponent,\n DeliveryEntry,\n DeliveryList,\n DeliveryPage,\n} from \"@bettercms-ai/types\";\n\nexport interface RuntimeConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n /** Server-only delivery key (a `content:read:draft` key for drafts). */\n apiKey?: string;\n /**\n * Collapse identical delivery GETs for the life of the process. Set by the integration\n * when Astro's `command === \"build\"` — a static build renders every route from one\n * published dataset, so a repeat read cannot legitimately differ.\n *\n * Never set for `dev` or SSR, where each request must see current content.\n */\n buildCache?: boolean;\n}\n\n/** Per-request draft state, set on `Astro.locals.bcms` by the middleware. */\nexport interface BcmsLocals {\n draft: boolean;\n token?: string;\n}\n\ninterface HasLocals {\n locals: { bcms?: BcmsLocals };\n}\n\nexport interface BetterCMSRuntime {\n /** Published-perspective client (use for SSG / non-draft reads). */\n client: BetterCMSReadClient;\n /** Pick the right client for a request (draft client when draft mode is on). */\n getClient(astro?: HasLocals): BetterCMSReadClient;\n loadEntry<T = Record<string, unknown>>(\n astro: HasLocals,\n slug: string,\n opts?: Parameters<BetterCMSReadClient[\"getEntry\"]>[1],\n ): Promise<DeliveryEntry<T>>;\n loadEntries<T = Record<string, unknown>>(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listEntries\"]>[0],\n ): Promise<DeliveryList<DeliveryEntry<T>>>;\n loadPage(astro: HasLocals, slug: string): Promise<DeliveryPage | null>;\n /** Component definitions for `<BcmsBlocks components={…}>` — draft-side in draft mode. */\n loadComponents(astro: HasLocals): Promise<DeliveryComponent[]>;\n loadLayout(astro: HasLocals): Promise<DeliveredLayout | null>;\n loadForms(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listForms\"]>[0],\n ): Promise<Awaited<ReturnType<BetterCMSReadClient[\"listForms\"]>>>;\n}\n\nexport function createRuntime(config: RuntimeConfig): BetterCMSRuntime {\n const { buildCache, ...clientConfig } = config;\n // One client for the whole build, so the cache below is shared across every route\n // rather than rebuilt per render.\n const published = createClient({ ...clientConfig, perspective: \"published\", cache: buildCache });\n\n const getClient = (astro?: HasLocals): BetterCMSReadClient => {\n const bcms = astro?.locals?.bcms;\n if (bcms?.draft) {\n // Explicitly uncached: a draft read exists to show the edit that just happened.\n return createClient({\n ...clientConfig,\n perspective: \"drafts\",\n previewToken: bcms.token,\n cache: false,\n });\n }\n return published;\n };\n\n return {\n client: published,\n getClient,\n loadEntry: (astro, slug, opts) => getClient(astro).getEntry(slug, opts),\n loadEntries: (astro, opts) => getClient(astro).listEntries(opts),\n loadPage: (astro, slug) => getClient(astro).getPage(slug),\n loadComponents: (astro) => getClient(astro).listComponents(),\n loadLayout: (astro) => getClient(astro).getLayout(),\n loadForms: (astro, opts) => getClient(astro).listForms(opts),\n };\n}\n\n/**\n * Build the live SSE URL for `<BcmsLive>`. The key is a query param because\n * browser EventSource cannot send headers — use a content:read key.\n *\n * `depth` sets the hydration depth of the document the stream pushes (default 1,\n * matching the delivery API). `drafts` streams autosaves so a preview surface\n * repaints as the editor types — it needs a `content:read:draft` key, and the\n * server ignores the flag for any other key rather than failing the connection.\n * Never enable it on a public site.\n */\nexport function liveSrc(opts: {\n apiUrl: string;\n workspace: string;\n apiKey: string;\n depth?: 0 | 1 | 2;\n drafts?: boolean;\n}): string {\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const q = new URLSearchParams({ key: opts.apiKey });\n if (opts.depth !== undefined) q.set(\"depth\", String(opts.depth));\n if (opts.drafts) q.set(\"drafts\", \"1\");\n return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;\n}\n\n/**\n * The published content snapshot, read at request time on a server BetterCMS hosts (it rewrites the\n * file on every publish) and from `bcms-content.json` at build time. Read content through this —\n * never `import` bcms-content.json or `import.meta.glob` it: a bundled copy is frozen at build, and\n * the platform has to rebuild the whole site for every publish to change it.\n */\nexport { readPublishedContent, publishedEntry, type PublishedContent } from \"@bettercms-ai/sdk\";\n\n// ── Signed-cookie helpers (HMAC-SHA256, base64url) ──────────────────────────\n\nconst enc = new TextEncoder();\n\nfunction base64url(bytes: ArrayBuffer): string {\n const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));\n return b.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function hmac(value: string, secret: string): Promise<string> {\n const key = await crypto.subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n return base64url(await crypto.subtle.sign(\"HMAC\", key, enc.encode(value)));\n}\n\n/** Sign a value as `value.sig` for tamper-evident cookie storage. */\nexport async function signValue(value: string, secret: string): Promise<string> {\n return `${value}.${await hmac(value, secret)}`;\n}\n\n/** Verify a signed value, returning the original value or null if tampered. */\nexport async function verifyValue(signed: string, secret: string): Promise<string | null> {\n const idx = signed.lastIndexOf(\".\");\n if (idx < 0) return null;\n const value = signed.slice(0, idx);\n const sig = signed.slice(idx + 1);\n const expected = await hmac(value, secret);\n // Constant-time-ish compare via length + char accumulation.\n if (sig.length !== expected.length) return null;\n let diff = 0;\n for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);\n return diff === 0 ? value : null;\n}\n\n/**\n * The draft-cookie signing secret, `process.env` FIRST.\n *\n * Vite may INLINE `import.meta.env.BCMS_DRAFT_SECRET` at build time, and the value present\n * when the customer's site is built is not the value the hosting container is given at start\n * — BetterCMS derives it per project and injects it as a real env var. Reading the runtime\n * env first is what makes managed hosting work without asking anyone to rebuild. The\n * `import.meta.env` read stays as the fallback for `astro dev` and for adapters with no\n * `process` (guarded, since touching `import.meta.env` outside Vite throws).\n */\nexport function draftSecret(): string | undefined {\n const fromProcess =\n typeof process !== \"undefined\" ? (process.env?.BCMS_DRAFT_SECRET as string | undefined) : undefined;\n if (fromProcess) return fromProcess;\n try {\n return import.meta.env?.BCMS_DRAFT_SECRET as string | undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Only same-origin relative paths. Blocks \"//evil.com\", \"https://evil.com\", etc. */\nexport function safeRedirectPath(to: string): string {\n return to.startsWith(\"/\") && !to.startsWith(\"//\") ? to : \"/\";\n}\n\n/** Decode a preview-token JWT payload (UNVERIFIED) to read its `entrySlug`. */\nexport function decodeTokenSlug(token: string): string | null {\n try {\n const part = token.split(\".\")[1];\n if (!part) return null;\n const json = JSON.parse(\n atob(part.replace(/-/g, \"+\").replace(/_/g, \"/\")),\n ) as { entrySlug?: string };\n return json.entrySlug ?? null;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/runtime.ts"],"sourcesContent":["/**\n * Runtime for @bettercms-ai/astro — the code the `bettercms:client` virtual\n * module binds to. Provides a published client, per-request draft client\n * selection, typed loaders, and signed-cookie helpers for draft mode.\n *\n * Read-only; no I/O at import time. Safe in Node and edge runtimes (uses Web\n * Crypto + global fetch only).\n */\nimport { createClient, type BetterCMSReadClient } from \"@bettercms-ai/sdk\";\nimport type {\n DeliveredLayout,\n DeliveryComponent,\n DeliveryEntry,\n DeliveryList,\n DeliveryPage,\n} from \"@bettercms-ai/types\";\n\nexport interface RuntimeConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n /** Server-only delivery key (a `content:read:draft` key for drafts). */\n apiKey?: string;\n /**\n * Collapse identical delivery GETs for the life of the process. Set by the integration\n * when Astro's `command === \"build\"` — a static build renders every route from one\n * published dataset, so a repeat read cannot legitimately differ.\n *\n * Never set for `dev` or SSR, where each request must see current content.\n */\n buildCache?: boolean;\n}\n\n/** Per-request draft state, set on `Astro.locals.bcms` by the middleware. */\nexport interface BcmsLocals {\n draft: boolean;\n token?: string;\n}\n\ninterface HasLocals {\n locals: { bcms?: BcmsLocals };\n}\n\nexport interface BetterCMSRuntime {\n /** Published-perspective client (use for SSG / non-draft reads). */\n client: BetterCMSReadClient;\n /** Pick the right client for a request (draft client when draft mode is on). */\n getClient(astro?: HasLocals): BetterCMSReadClient;\n loadEntry<T = Record<string, unknown>>(\n astro: HasLocals,\n slug: string,\n opts?: Parameters<BetterCMSReadClient[\"getEntry\"]>[1],\n ): Promise<DeliveryEntry<T>>;\n loadEntries<T = Record<string, unknown>>(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listEntries\"]>[0],\n ): Promise<DeliveryList<DeliveryEntry<T>>>;\n loadPage(astro: HasLocals, slug: string): Promise<DeliveryPage | null>;\n /** Component definitions for `<BcmsBlocks components={…}>` — draft-side in draft mode. */\n loadComponents(astro: HasLocals): Promise<DeliveryComponent[]>;\n loadLayout(astro: HasLocals): Promise<DeliveredLayout | null>;\n loadForms(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listForms\"]>[0],\n ): Promise<Awaited<ReturnType<BetterCMSReadClient[\"listForms\"]>>>;\n}\n\nexport function createRuntime(config: RuntimeConfig): BetterCMSRuntime {\n const { buildCache, ...clientConfig } = config;\n // One client for the whole build, so the cache below is shared across every route\n // rather than rebuilt per render.\n const published = createClient({ ...clientConfig, perspective: \"published\", cache: buildCache });\n\n const getClient = (astro?: HasLocals): BetterCMSReadClient => {\n const bcms = astro?.locals?.bcms;\n if (bcms?.draft) {\n // Explicitly uncached: a draft read exists to show the edit that just happened.\n return createClient({\n ...clientConfig,\n perspective: \"drafts\",\n previewToken: bcms.token,\n cache: false,\n });\n }\n return published;\n };\n\n return {\n client: published,\n getClient,\n loadEntry: (astro, slug, opts) => getClient(astro).getEntry(slug, opts),\n loadEntries: (astro, opts) => getClient(astro).listEntries(opts),\n loadPage: (astro, slug) => getClient(astro).getPage(slug),\n loadComponents: (astro) => getClient(astro).listComponents(),\n loadLayout: (astro) => getClient(astro).getLayout(),\n loadForms: (astro, opts) => getClient(astro).listForms(opts),\n };\n}\n\n/**\n * Build the live SSE URL for `<BcmsLive>`. The key is a query param because\n * browser EventSource cannot send headers — use a content:read key.\n *\n * `depth` sets the hydration depth of the document the stream pushes (default 1,\n * matching the delivery API). `drafts` streams autosaves so a preview surface\n * repaints as the editor types — it needs a `content:read:draft` key, and the\n * server ignores the flag for any other key rather than failing the connection.\n * Never enable it on a public site.\n */\nexport function liveSrc(opts: {\n apiUrl: string;\n workspace: string;\n apiKey: string;\n depth?: 0 | 1 | 2;\n drafts?: boolean;\n}): string {\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const q = new URLSearchParams({ key: opts.apiKey });\n if (opts.depth !== undefined) q.set(\"depth\", String(opts.depth));\n if (opts.drafts) q.set(\"drafts\", \"1\");\n return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;\n}\n\n/**\n * The published content snapshot, read at request time on a server BetterCMS hosts (it rewrites the\n * file on every publish) and from `bcms-content.json` at build time. Read content through this —\n * never `import` bcms-content.json or `import.meta.glob` it: a bundled copy is frozen at build, and\n * the platform has to rebuild the whole site for every publish to change it.\n */\nexport { readPublishedContent, publishedEntry, type PublishedContent } from \"@bettercms-ai/sdk\";\n\n// Signed-cookie and draft-route helpers: one implementation for every adapter (@bettercms-ai/sdk/draft).\nexport { signValue, verifyValue, safeRedirectPath, decodeTokenSlug } from \"@bettercms-ai/sdk/draft\";\n\n/**\n * The draft-cookie signing secret, `process.env` FIRST.\n *\n * Vite may INLINE `import.meta.env.BCMS_DRAFT_SECRET` at build time, and the value present\n * when the customer's site is built is not the value the hosting container is given at start\n * — BetterCMS derives it per project and injects it as a real env var. Reading the runtime\n * env first is what makes managed hosting work without asking anyone to rebuild. The\n * `import.meta.env` read stays as the fallback for `astro dev` and for adapters with no\n * `process` (guarded, since touching `import.meta.env` outside Vite throws).\n */\nexport function draftSecret(): string | undefined {\n const fromProcess =\n typeof process !== \"undefined\" ? (process.env?.BCMS_DRAFT_SECRET as string | undefined) : undefined;\n if (fromProcess) return fromProcess;\n try {\n return import.meta.env?.BCMS_DRAFT_SECRET as string | undefined;\n } catch {\n return undefined;\n }\n}\n"],"mappings":";AAQA,SAAS,oBAA8C;AAyHvD,SAAS,sBAAsB,sBAA6C;AAG5E,SAAS,WAAW,aAAa,kBAAkB,uBAAuB;AAjEnE,SAAS,cAAc,QAAyC;AACrE,QAAM,EAAE,YAAY,GAAG,aAAa,IAAI;AAGxC,QAAM,YAAY,aAAa,EAAE,GAAG,cAAc,aAAa,aAAa,OAAO,WAAW,CAAC;AAE/F,QAAM,YAAY,CAAC,UAA2C;AAC5D,UAAM,OAAO,OAAO,QAAQ;AAC5B,QAAI,MAAM,OAAO;AAEf,aAAO,aAAa;AAAA,QAClB,GAAG;AAAA,QACH,aAAa;AAAA,QACb,cAAc,KAAK;AAAA,QACnB,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR;AAAA,IACA,WAAW,CAAC,OAAO,MAAM,SAAS,UAAU,KAAK,EAAE,SAAS,MAAM,IAAI;AAAA,IACtE,aAAa,CAAC,OAAO,SAAS,UAAU,KAAK,EAAE,YAAY,IAAI;AAAA,IAC/D,UAAU,CAAC,OAAO,SAAS,UAAU,KAAK,EAAE,QAAQ,IAAI;AAAA,IACxD,gBAAgB,CAAC,UAAU,UAAU,KAAK,EAAE,eAAe;AAAA,IAC3D,YAAY,CAAC,UAAU,UAAU,KAAK,EAAE,UAAU;AAAA,IAClD,WAAW,CAAC,OAAO,SAAS,UAAU,KAAK,EAAE,UAAU,IAAI;AAAA,EAC7D;AACF;AAYO,SAAS,QAAQ,MAMb;AACT,QAAM,OAAO,KAAK,OAAO,QAAQ,QAAQ,EAAE;AAC3C,QAAM,IAAI,IAAI,gBAAgB,EAAE,KAAK,KAAK,OAAO,CAAC;AAClD,MAAI,KAAK,UAAU,OAAW,GAAE,IAAI,SAAS,OAAO,KAAK,KAAK,CAAC;AAC/D,MAAI,KAAK,OAAQ,GAAE,IAAI,UAAU,GAAG;AACpC,SAAO,GAAG,IAAI,oBAAoB,KAAK,SAAS,SAAS,CAAC;AAC5D;AAuBO,SAAS,cAAkC;AAChD,QAAM,cACJ,OAAO,YAAY,cAAe,QAAQ,KAAK,oBAA2C;AAC5F,MAAI,YAAa,QAAO;AACxB,MAAI;AACF,WAAO,YAAY,KAAK;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
package/dist/sections.js CHANGED
@@ -67,19 +67,7 @@ function stampSoleElement(html, attrs) {
67
67
  // src/runtime.ts
68
68
  import { createClient } from "@bettercms-ai/sdk";
69
69
  import { readPublishedContent, publishedEntry } from "@bettercms-ai/sdk";
70
- var enc = new TextEncoder();
71
- function decodeTokenSlug(token) {
72
- try {
73
- const part = token.split(".")[1];
74
- if (!part) return null;
75
- const json = JSON.parse(
76
- atob(part.replace(/-/g, "+").replace(/_/g, "/"))
77
- );
78
- return json.entrySlug ?? null;
79
- } catch {
80
- return null;
81
- }
82
- }
70
+ import { signValue, verifyValue, safeRedirectPath, decodeTokenSlug } from "@bettercms-ai/sdk/draft";
83
71
 
84
72
  // src/section-root.ts
85
73
  import { blockStyleToColumnCss, blockStyleToCss, hideOnAttr } from "@bettercms-ai/types";
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sections.ts","../src/bindings.ts","../src/runtime.ts","../src/section-root.ts"],"sourcesContent":["/**\n * What `<BcmsSections>` (components/BcmsSections.astro) is made of, kept as a `.ts` module so it is\n * testable without an Astro compiler: where the page's block list comes from, and how a block's\n * stamp lands on the root of the section the page's own code rendered.\n *\n * The order itself is `planSections` in @bettercms-ai/types, shared with @bettercms-ai/next.\n */\nimport { readPublishedContent } from \"@bettercms-ai/sdk\";\nimport type { ContentBlock } from \"@bettercms-ai/types\";\nimport { stampSoleElement, tagEnd } from \"./bindings.js\";\nimport { decodeTokenSlug } from \"./runtime.js\";\nimport { sectionRootAttrs } from \"./section-root.js\";\n\nexport { sectionRootAttrs };\n\n/** The attribute every root `<BcmsSections>` stamps carries: which page's list placed it. */\nexport const SECTIONS_ATTR = \"data-bcms-sections\";\n/** On the box around a section that is not one element. @see stampSectionRoot */\nexport const SECTION_BOX_ATTR = \"data-bcms-box\";\n\n/**\n * The inline style of a section's box: `display:contents` while the block has no style, and a real\n * box (`flow-root`, unless the style sets a display of its own) carrying it once it has.\n */\nexport const sectionBoxStyle = (style: string | undefined): string =>\n !style ? \"display:contents\" : /(?:^|;)display:/.test(style) ? style : `display:flow-root;${style}`;\n\n/** The comments around the rendered list: where the SDK-drawn sections of `page` start and end. */\nexport const sectionsStart = (page: string): string => `<!--bcms-sections:start page=\"${page.replace(/[^\\w./-]/g, \"\")}\"-->`;\nexport const SECTIONS_END = \"<!--bcms-sections:end-->\";\n\ntype Snapshot = { apiUrl?: string | null; pages?: unknown[]; components?: unknown[]; forms?: unknown[] };\n\n/**\n * The published snapshot's view of one page: its blocks (null when the page is not there), and what draws them.\n *\n * A collection page and the listing page beside it share a slug (`blog`), and the snapshot marks\n * the collection's (`template`). `template` reads that one, whose list is its item template's\n * (`[slug].astro` draws it on every entry); a plain read takes a page of its own route, and falls\n * back to the only row there is. Twin: `sectionsFor` in @bettercms-ai/next.\n */\nexport function publishedSections(page: string, template = false): { blocks: ContentBlock[] | null; components: unknown[]; forms: unknown[]; apiUrl: string | null } {\n const snapshot = readPublishedContent() as Snapshot | null;\n const rows = (Array.isArray(snapshot?.pages) ? snapshot.pages : []).filter((p) => (p as { slug?: unknown } | null)?.slug === page) as { template?: unknown; blocks?: unknown; blockJson?: unknown }[];\n const collection = rows.find((r) => r.template === true);\n const row = template ? collection : (rows.find((r) => r.template !== true) ?? rows[0]);\n const blocks = row?.blocks ?? row?.blockJson;\n return {\n blocks: Array.isArray(blocks) ? (blocks as ContentBlock[]) : null,\n components: Array.isArray(snapshot?.components) ? snapshot.components : [],\n forms: Array.isArray(snapshot?.forms) ? snapshot.forms : [],\n apiUrl: typeof snapshot?.apiUrl === \"string\" ? snapshot.apiUrl : null,\n };\n}\n\n/** The API a draft read goes to: the server's runtime env first (hosting injects it), then the build's. */\nexport function sectionsApiUrl(fallback: string | null): string | null {\n const fromProcess = typeof process !== \"undefined\" ? process.env?.PUBLIC_BCMS_API_URL : undefined;\n if (fromProcess) return fromProcess;\n try {\n return (import.meta.env?.PUBLIC_BCMS_API_URL as string | undefined) ?? fallback;\n } catch {\n return fallback;\n }\n}\n\n/**\n * The DRAFT block list of `page`, when this request is the Visual Editor's draft render of it.\n *\n * The editor enters draft mode with a PAGE preview token (the middleware puts it on\n * `Astro.locals.bcms`); the public preview endpoint that validated it returns that page's draft\n * blocks. The token names ONE page, so any other page renders published. Null on any failure: the\n * page then renders its published structure, never an error.\n */\nexport async function draftSections(page: string, token: string, apiUrl: string | null): Promise<ContentBlock[] | null> {\n if (!apiUrl || decodeTokenSlug(token) !== page) return null;\n try {\n const res = await fetch(`${apiUrl.replace(/\\/+$/, \"\")}/api/v1/preview/${encodeURIComponent(page)}?token=${encodeURIComponent(token)}`, {\n signal: AbortSignal.timeout(5000),\n });\n if (!res.ok) return null;\n const blocks = ((await res.json()) as { data?: { blockJson?: unknown } })?.data?.blockJson;\n return Array.isArray(blocks) ? (blocks as ContentBlock[]) : null;\n } catch {\n return null;\n }\n}\n\nconst escapeAttr = (value: string): string => value.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\");\n\n/**\n * A rendered slot with its block's stamp on the section's OWN root.\n *\n * One element: the attributes are spliced into its start tag and nothing else moves, so\n * `main > section`, `:first-child` and every animation target stay what the author wrote. Its own\n * `style` is kept and the block's declarations follow it, the order the in-place painter writes.\n * A root that already stamps a block is the site's own work and is left as it is.\n *\n * Anything else (text, several roots, a framework island) has no root to stamp, so a box of ours\n * carries the stamp. With no style on the block it is `display:contents`: it draws nothing, and the\n * page lays out as written. A box that draws nothing cannot show a padding, so a block WITH a\n * style gets `display:flow-root` and the style on it (@see sectionBoxStyle). It is marked\n * `data-bcms-style` like any root, and `data-bcms-box` tells the in-place painter to make the same\n * switch when the first style is published between builds.\n *\n * THE ONE LIMIT: a styled box is one block box. In a flex or grid parent its children are then one\n * item instead of several, and their margins stop collapsing with the neighbours'. A section with\n * one root element has no box and no such limit.\n */\nexport function stampSectionRoot(html: string, block: { id?: string | null; style?: unknown }, page: string): string {\n const attrs = sectionRootAttrs(block as Parameters<typeof sectionRootAttrs>[0]);\n const id = attrs[\"data-bcms-block\"];\n if (id === undefined) return html;\n if (stampSoleElement(html, {}) === null) {\n if (html.trim() === \"\") return html;\n const hide = attrs[\"data-bcms-hide\"];\n return (\n `<div style=\"${escapeAttr(sectionBoxStyle(attrs.style))}\" data-bcms-block=\"${escapeAttr(id)}\" data-bcms-style=\"\"` +\n `${hide ? ` data-bcms-hide=\"${escapeAttr(hide)}\"` : \"\"} ${SECTION_BOX_ATTR}=\"\" ${SECTIONS_ATTR}=\"${escapeAttr(page)}\">${html}</div>`\n );\n }\n const start = html.search(/\\S/);\n const end = tagEnd(html, start);\n let tag = html.slice(start, end);\n if (/\\sdata-bcms-block[\\s=>/]/.test(`${tag}>`)) return html;\n const selfClosed = tag.endsWith(\"/\");\n if (selfClosed) tag = tag.slice(0, -1);\n tag = tag.replace(/\\s+$/, \"\");\n let style = attrs.style;\n const own = /\\sstyle\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)')/.exec(tag);\n if (own && style) {\n // The site's declarations first, the block's after: a later declaration wins, as the painter\n // orders them. Written where the site's own attribute stood, as React's `cloneElement` does.\n const merged = `${(own[1] ?? own[2] ?? \"\").replace(/[;\\s]+$/, \"\")};${style}`;\n tag = `${tag.slice(0, own.index)} style=\"${escapeAttr(merged)}\"${tag.slice(own.index + own[0].length)}`;\n style = undefined;\n }\n const added = Object.entries({ ...attrs, style, [SECTIONS_ATTR]: page })\n .filter((entry): entry is [string, string] => entry[1] !== undefined)\n .map(([name, value]) => ` ${name}=\"${escapeAttr(value)}\"`)\n .join(\"\");\n return html.slice(0, start) + tag + added + (selfClosed ? \" /\" : \"\") + html.slice(end);\n}\n","/**\n * Visual Editor addresses for the Astro renderer.\n *\n * Identical contract to `@bettercms-ai/next`'s `bindings.ts`, and the SAME reason for existing:\n * inside a component instance the definition's block ids address nothing, because they are\n * shared by every instance of it. Stamping them produces an orphan on one placement and a\n * duplicate that drops BOTH on two.\n *\n * Kept as a `.ts` module rather than inlined in the `.astro` frontmatter so the logic is\n * testable without an Astro compiler, and so the two SDKs cannot drift from each other by\n * having one file edited and not the other.\n */\nimport type { BindingKind } from \"@bettercms-ai/types\";\nimport { formatPropsAttribute } from \"@bettercms-ai/types\";\n\nexport interface BindingCtx {\n /** Present only inside a component instance, where it is AUTHORITATIVE. */\n fieldAddresses?: Map<string, string>;\n}\n\n/** The address a write lands on, or null when none exists. */\nexport function addressFor(id: string | undefined, prop: string, ctx: BindingCtx): string | null {\n if (!id) return null;\n const key = `${id}__${prop}`;\n if (ctx.fieldAddresses) return ctx.fieldAddresses.get(key) ?? null;\n return key;\n}\n\n/**\n * Attributes for a value rendered as this element's own content. Astro omits `undefined`\n * attributes, so an empty object spreads to nothing.\n */\nexport function bind(\n id: string | undefined,\n prop: string,\n kind: BindingKind,\n ctx: BindingCtx,\n): Record<string, string> {\n const address = addressFor(id, prop, ctx);\n return address ? { \"data-bcms-field\": address, \"data-bcms-kind\": kind } : {};\n}\n\n/**\n * Attributes for values that live in an ATTRIBUTE of this element. `data-bcms-field` addresses\n * one element and an element cannot hold two, so these ride `data-bcms-props`:\n * `<address>|<kind>|<domAttribute>`, semicolon separated.\n *\n * An entry whose value is absent is dropped — `setFieldPath` refuses to create a final key, so\n * the address would be a write the editor rejects.\n */\nexport function bindProps(\n id: string | undefined,\n entries: readonly { prop: string; kind: BindingKind; attr: string; present: boolean }[],\n ctx: BindingCtx,\n): Record<string, string> {\n const resolved = entries.flatMap(({ prop, kind, attr, present }) => {\n if (!present) return [];\n const address = addressFor(id, prop, ctx);\n return address ? [{ address, kind, attr }] : [];\n });\n return resolved.length ? { \"data-bcms-props\": formatPropsAttribute(resolved) } : {};\n}\n\nconst VOID_TAGS = new Set([\"area\", \"base\", \"br\", \"col\", \"embed\", \"hr\", \"img\", \"input\", \"link\", \"meta\", \"source\", \"track\", \"wbr\"]);\nconst RAW_TEXT_TAGS = new Set([\"script\", \"style\", \"textarea\", \"title\"]);\n\n/** Where the tag opened at `from` (`<`) ends, quotes respected; -1 when it never does. */\nexport function tagEnd(html: string, from: number): number {\n let quote = \"\";\n for (let i = from; i < html.length; i++) {\n const c = html[i]!;\n if (quote) {\n if (c === quote) quote = \"\";\n } else if (c === '\"' || c === \"'\") quote = c;\n else if (c === \">\") return i;\n }\n return -1;\n}\n\n/**\n * `<BcmsField>`'s child, when it is ONE element: the same markup with the binding attributes on\n * that element's own tag. Null when the rendered slot is anything else (text, several elements,\n * an element that already declares a field, a framework island), and the caller wraps as before.\n *\n * Why not always wrap: the wrapper is a `<span>` between the author's parent and the author's\n * element. `.hero > h1`, `gsap.utils.toArray(\".hero > *\")` and `:first-child` all stop matching,\n * and an animation that targeted the heading targets nothing. With the attributes on the element\n * itself the page's DOM is exactly the one the author wrote.\n *\n * A scan, not a parser: it only has to find where the first element ends. Anything it is not\n * sure of answers null, which is the old behaviour.\n */\nexport function stampSoleElement(html: string, attrs: Record<string, string | undefined>): string | null {\n const start = html.search(/\\S/);\n const open = /^<([a-zA-Z][\\w:-]*)/.exec(html.slice(start));\n if (start < 0 || !open) return null;\n const tag = open[1]!.toLowerCase();\n const openEnd = tagEnd(html, start);\n // A custom element is a framework's own wrapper (`astro-island`): not the author's element.\n if (openEnd < 0 || tag.includes(\"-\") || /\\sdata-bcms-(?:layout-)?field[\\s=>/]/.test(html.slice(start, openEnd + 1))) return null;\n let i = openEnd + 1;\n if (!VOID_TAGS.has(tag) && html[openEnd - 1] !== \"/\") {\n let depth = 1;\n let raw = RAW_TEXT_TAGS.has(tag) ? tag : \"\";\n while (depth > 0) {\n const lt = html.indexOf(\"<\", i);\n if (lt < 0) return null;\n if (raw) {\n const close = html.toLowerCase().indexOf(`</${raw}`, i);\n if (close < 0) return null;\n i = tagEnd(html, close) + 1;\n if (i === 0) return null;\n raw = \"\";\n depth--;\n continue;\n }\n if (html.startsWith(\"<!--\", lt)) {\n const end = html.indexOf(\"-->\", lt + 4);\n if (end < 0) return null;\n i = end + 3;\n continue;\n }\n const end = tagEnd(html, lt);\n if (end < 0) return null;\n const name = /^<\\/?([a-zA-Z][\\w:-]*)/.exec(html.slice(lt, end + 1))?.[1]?.toLowerCase();\n if (name && html[lt + 1] === \"/\") depth--;\n else if (name && !VOID_TAGS.has(name) && html[end - 1] !== \"/\") {\n depth++;\n if (RAW_TEXT_TAGS.has(name)) raw = name;\n }\n i = end + 1;\n }\n }\n if (html.slice(i).trim() !== \"\") return null;\n const escape = (value: string) => value.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\");\n const added = Object.entries(attrs)\n .filter((entry): entry is [string, string] => entry[1] !== undefined)\n .map(([name, value]) => ` ${name}=\"${escape(value)}\"`)\n .join(\"\");\n // Last on the tag, where React's `cloneElement` puts them: the two lanes emit the same bytes.\n const selfClosed = html[openEnd - 1] === \"/\";\n const at = selfClosed ? openEnd - 1 : openEnd;\n return html.slice(0, at).replace(/\\s+$/, \"\") + added + (selfClosed ? \" \" : \"\") + html.slice(at);\n}\n","/**\n * Runtime for @bettercms-ai/astro — the code the `bettercms:client` virtual\n * module binds to. Provides a published client, per-request draft client\n * selection, typed loaders, and signed-cookie helpers for draft mode.\n *\n * Read-only; no I/O at import time. Safe in Node and edge runtimes (uses Web\n * Crypto + global fetch only).\n */\nimport { createClient, type BetterCMSReadClient } from \"@bettercms-ai/sdk\";\nimport type {\n DeliveredLayout,\n DeliveryComponent,\n DeliveryEntry,\n DeliveryList,\n DeliveryPage,\n} from \"@bettercms-ai/types\";\n\nexport interface RuntimeConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n /** Server-only delivery key (a `content:read:draft` key for drafts). */\n apiKey?: string;\n /**\n * Collapse identical delivery GETs for the life of the process. Set by the integration\n * when Astro's `command === \"build\"` — a static build renders every route from one\n * published dataset, so a repeat read cannot legitimately differ.\n *\n * Never set for `dev` or SSR, where each request must see current content.\n */\n buildCache?: boolean;\n}\n\n/** Per-request draft state, set on `Astro.locals.bcms` by the middleware. */\nexport interface BcmsLocals {\n draft: boolean;\n token?: string;\n}\n\ninterface HasLocals {\n locals: { bcms?: BcmsLocals };\n}\n\nexport interface BetterCMSRuntime {\n /** Published-perspective client (use for SSG / non-draft reads). */\n client: BetterCMSReadClient;\n /** Pick the right client for a request (draft client when draft mode is on). */\n getClient(astro?: HasLocals): BetterCMSReadClient;\n loadEntry<T = Record<string, unknown>>(\n astro: HasLocals,\n slug: string,\n opts?: Parameters<BetterCMSReadClient[\"getEntry\"]>[1],\n ): Promise<DeliveryEntry<T>>;\n loadEntries<T = Record<string, unknown>>(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listEntries\"]>[0],\n ): Promise<DeliveryList<DeliveryEntry<T>>>;\n loadPage(astro: HasLocals, slug: string): Promise<DeliveryPage | null>;\n /** Component definitions for `<BcmsBlocks components={…}>` — draft-side in draft mode. */\n loadComponents(astro: HasLocals): Promise<DeliveryComponent[]>;\n loadLayout(astro: HasLocals): Promise<DeliveredLayout | null>;\n loadForms(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listForms\"]>[0],\n ): Promise<Awaited<ReturnType<BetterCMSReadClient[\"listForms\"]>>>;\n}\n\nexport function createRuntime(config: RuntimeConfig): BetterCMSRuntime {\n const { buildCache, ...clientConfig } = config;\n // One client for the whole build, so the cache below is shared across every route\n // rather than rebuilt per render.\n const published = createClient({ ...clientConfig, perspective: \"published\", cache: buildCache });\n\n const getClient = (astro?: HasLocals): BetterCMSReadClient => {\n const bcms = astro?.locals?.bcms;\n if (bcms?.draft) {\n // Explicitly uncached: a draft read exists to show the edit that just happened.\n return createClient({\n ...clientConfig,\n perspective: \"drafts\",\n previewToken: bcms.token,\n cache: false,\n });\n }\n return published;\n };\n\n return {\n client: published,\n getClient,\n loadEntry: (astro, slug, opts) => getClient(astro).getEntry(slug, opts),\n loadEntries: (astro, opts) => getClient(astro).listEntries(opts),\n loadPage: (astro, slug) => getClient(astro).getPage(slug),\n loadComponents: (astro) => getClient(astro).listComponents(),\n loadLayout: (astro) => getClient(astro).getLayout(),\n loadForms: (astro, opts) => getClient(astro).listForms(opts),\n };\n}\n\n/**\n * Build the live SSE URL for `<BcmsLive>`. The key is a query param because\n * browser EventSource cannot send headers — use a content:read key.\n *\n * `depth` sets the hydration depth of the document the stream pushes (default 1,\n * matching the delivery API). `drafts` streams autosaves so a preview surface\n * repaints as the editor types — it needs a `content:read:draft` key, and the\n * server ignores the flag for any other key rather than failing the connection.\n * Never enable it on a public site.\n */\nexport function liveSrc(opts: {\n apiUrl: string;\n workspace: string;\n apiKey: string;\n depth?: 0 | 1 | 2;\n drafts?: boolean;\n}): string {\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const q = new URLSearchParams({ key: opts.apiKey });\n if (opts.depth !== undefined) q.set(\"depth\", String(opts.depth));\n if (opts.drafts) q.set(\"drafts\", \"1\");\n return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;\n}\n\n/**\n * The published content snapshot, read at request time on a server BetterCMS hosts (it rewrites the\n * file on every publish) and from `bcms-content.json` at build time. Read content through this —\n * never `import` bcms-content.json or `import.meta.glob` it: a bundled copy is frozen at build, and\n * the platform has to rebuild the whole site for every publish to change it.\n */\nexport { readPublishedContent, publishedEntry, type PublishedContent } from \"@bettercms-ai/sdk\";\n\n// ── Signed-cookie helpers (HMAC-SHA256, base64url) ──────────────────────────\n\nconst enc = new TextEncoder();\n\nfunction base64url(bytes: ArrayBuffer): string {\n const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));\n return b.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function hmac(value: string, secret: string): Promise<string> {\n const key = await crypto.subtle.importKey(\n \"raw\",\n enc.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n return base64url(await crypto.subtle.sign(\"HMAC\", key, enc.encode(value)));\n}\n\n/** Sign a value as `value.sig` for tamper-evident cookie storage. */\nexport async function signValue(value: string, secret: string): Promise<string> {\n return `${value}.${await hmac(value, secret)}`;\n}\n\n/** Verify a signed value, returning the original value or null if tampered. */\nexport async function verifyValue(signed: string, secret: string): Promise<string | null> {\n const idx = signed.lastIndexOf(\".\");\n if (idx < 0) return null;\n const value = signed.slice(0, idx);\n const sig = signed.slice(idx + 1);\n const expected = await hmac(value, secret);\n // Constant-time-ish compare via length + char accumulation.\n if (sig.length !== expected.length) return null;\n let diff = 0;\n for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);\n return diff === 0 ? value : null;\n}\n\n/**\n * The draft-cookie signing secret, `process.env` FIRST.\n *\n * Vite may INLINE `import.meta.env.BCMS_DRAFT_SECRET` at build time, and the value present\n * when the customer's site is built is not the value the hosting container is given at start\n * — BetterCMS derives it per project and injects it as a real env var. Reading the runtime\n * env first is what makes managed hosting work without asking anyone to rebuild. The\n * `import.meta.env` read stays as the fallback for `astro dev` and for adapters with no\n * `process` (guarded, since touching `import.meta.env` outside Vite throws).\n */\nexport function draftSecret(): string | undefined {\n const fromProcess =\n typeof process !== \"undefined\" ? (process.env?.BCMS_DRAFT_SECRET as string | undefined) : undefined;\n if (fromProcess) return fromProcess;\n try {\n return import.meta.env?.BCMS_DRAFT_SECRET as string | undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Only same-origin relative paths. Blocks \"//evil.com\", \"https://evil.com\", etc. */\nexport function safeRedirectPath(to: string): string {\n return to.startsWith(\"/\") && !to.startsWith(\"//\") ? to : \"/\";\n}\n\n/** Decode a preview-token JWT payload (UNVERIFIED) to read its `entrySlug`. */\nexport function decodeTokenSlug(token: string): string | null {\n try {\n const part = token.split(\".\")[1];\n if (!part) return null;\n const json = JSON.parse(\n atob(part.replace(/-/g, \"+\").replace(/_/g, \"/\")),\n ) as { entrySlug?: string };\n return json.entrySlug ?? null;\n } catch {\n return null;\n }\n}\n","/**\n * C3/C4 — `sectionRootAttrs(block)`: the attributes a site's OWN section root spreads so the\n * BetterCMS editor offers that section's spacing and style handles.\n *\n * <section class=\"hero\" {...sectionRootAttrs(block)}>…</section>\n *\n * The editor shows those handles on a site-rendered page only when EVERY root the site stamps\n * with `data-bcms-block` also carries `data-bcms-style`, which is the site's promise that \"this\n * root's inline style is `blockStyleToCss(block.style)`\" — the same declarations `<BcmsBlocks>`\n * puts on its own wrapper. A root that stamps the block without applying its style would get a\n * capsule that paints the canvas and never ships, so the marker must travel with the style.\n *\n * The marker is present even when the style is EMPTY (then there is no `style` attribute at all):\n * an unstyled section is exactly the one an author is about to give its first spacing edit.\n */\nimport { blockStyleToColumnCss, blockStyleToCss, hideOnAttr, type BlockStyle } from \"@bettercms-ai/types\";\n\nexport interface SectionRootBlock {\n id?: string | null;\n style?: BlockStyle | Record<string, unknown> | null;\n}\n\nexport interface SectionRootAttrs {\n \"data-bcms-block\"?: string;\n \"data-bcms-style\": \"\";\n /** `\"mobile tablet\"` when `style.hideOn` hides it there; the site ships `RESPONSIVE_CSS` for it. */\n \"data-bcms-hide\"?: string;\n /** `blockStyleToCss(block.style)` as a CSS string; absent when the block has no style. */\n style?: string;\n}\n\n/** `paddingTop` → `padding-top`, joined the way `<BcmsBlocks>` writes its wrapper's attribute. */\nfunction cssText(css: Record<string, string | number>): string {\n return Object.entries(css)\n .map(([k, v]) => `${k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}:${v}`)\n .join(\";\");\n}\n\nexport function sectionRootAttrs(block: SectionRootBlock | null | undefined): SectionRootAttrs {\n const css = blockStyleToCss((block?.style ?? undefined) as BlockStyle | undefined);\n const id = typeof block?.id === \"string\" && block.id !== \"\" ? block.id : undefined;\n const hide = hideOnAttr(block?.style);\n return {\n ...(id !== undefined ? { \"data-bcms-block\": id } : {}),\n \"data-bcms-style\": \"\",\n ...(hide ? { \"data-bcms-hide\": hide } : {}),\n ...(css ? { style: cssText(css) } : {}),\n };\n}\n\nexport interface SectionColumnAttrs {\n \"data-bcms-column\": \"\";\n /** `blockStyleToColumnCss(block.style)` as a CSS string; absent when the section has no column. */\n style?: string;\n}\n\n/**\n * C3 — `sectionColumnAttrs(block)`: the attributes a site's own INNER container spreads so the\n * section's content WIDTH (`style.contentWidth`, placed by `contentAlign`) ships.\n *\n * <section {...sectionRootAttrs(block)}>\n * <div class=\"container\" {...sectionColumnAttrs(block)}>…</div>\n * </section>\n *\n * The width is the column BetterCMS draws as `.bcms-block-inner` inside the section's styled box —\n * never on the root itself, whose background must stay full-bleed. `data-bcms-column` is the\n * site's promise that this box's inline style is the column; the editor offers the width handle\n * on a site-rendered page only where every styled root holds one. Max-width plus left/right\n * margins, so the container keeps its own vertical margins and, with no column, its own width.\n */\nexport function sectionColumnAttrs(block: SectionRootBlock | null | undefined): SectionColumnAttrs {\n const css = blockStyleToColumnCss((block?.style ?? undefined) as BlockStyle | undefined);\n return { \"data-bcms-column\": \"\", ...(css ? { style: cssText(css) } : {}) };\n}\n"],"mappings":"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","names":["readPublishedContent","end","readPublishedContent"]}
1
+ {"version":3,"sources":["../src/sections.ts","../src/bindings.ts","../src/runtime.ts","../src/section-root.ts"],"sourcesContent":["/**\n * What `<BcmsSections>` (components/BcmsSections.astro) is made of, kept as a `.ts` module so it is\n * testable without an Astro compiler: where the page's block list comes from, and how a block's\n * stamp lands on the root of the section the page's own code rendered.\n *\n * The order itself is `planSections` in @bettercms-ai/types, shared with @bettercms-ai/next.\n */\nimport { readPublishedContent } from \"@bettercms-ai/sdk\";\nimport type { ContentBlock } from \"@bettercms-ai/types\";\nimport { stampSoleElement, tagEnd } from \"./bindings.js\";\nimport { decodeTokenSlug } from \"./runtime.js\";\nimport { sectionRootAttrs } from \"./section-root.js\";\n\nexport { sectionRootAttrs };\n\n/** The attribute every root `<BcmsSections>` stamps carries: which page's list placed it. */\nexport const SECTIONS_ATTR = \"data-bcms-sections\";\n/** On the box around a section that is not one element. @see stampSectionRoot */\nexport const SECTION_BOX_ATTR = \"data-bcms-box\";\n\n/**\n * The inline style of a section's box: `display:contents` while the block has no style, and a real\n * box (`flow-root`, unless the style sets a display of its own) carrying it once it has.\n */\nexport const sectionBoxStyle = (style: string | undefined): string =>\n !style ? \"display:contents\" : /(?:^|;)display:/.test(style) ? style : `display:flow-root;${style}`;\n\n/** The comments around the rendered list: where the SDK-drawn sections of `page` start and end. */\nexport const sectionsStart = (page: string): string => `<!--bcms-sections:start page=\"${page.replace(/[^\\w./-]/g, \"\")}\"-->`;\nexport const SECTIONS_END = \"<!--bcms-sections:end-->\";\n\ntype Snapshot = { apiUrl?: string | null; pages?: unknown[]; components?: unknown[]; forms?: unknown[] };\n\n/**\n * The published snapshot's view of one page: its blocks (null when the page is not there), and what draws them.\n *\n * A collection page and the listing page beside it share a slug (`blog`), and the snapshot marks\n * the collection's (`template`). `template` reads that one, whose list is its item template's\n * (`[slug].astro` draws it on every entry); a plain read takes a page of its own route, and falls\n * back to the only row there is. Twin: `sectionsFor` in @bettercms-ai/next.\n */\nexport function publishedSections(page: string, template = false): { blocks: ContentBlock[] | null; components: unknown[]; forms: unknown[]; apiUrl: string | null } {\n const snapshot = readPublishedContent() as Snapshot | null;\n const rows = (Array.isArray(snapshot?.pages) ? snapshot.pages : []).filter((p) => (p as { slug?: unknown } | null)?.slug === page) as { template?: unknown; blocks?: unknown; blockJson?: unknown }[];\n const collection = rows.find((r) => r.template === true);\n const row = template ? collection : (rows.find((r) => r.template !== true) ?? rows[0]);\n const blocks = row?.blocks ?? row?.blockJson;\n return {\n blocks: Array.isArray(blocks) ? (blocks as ContentBlock[]) : null,\n components: Array.isArray(snapshot?.components) ? snapshot.components : [],\n forms: Array.isArray(snapshot?.forms) ? snapshot.forms : [],\n apiUrl: typeof snapshot?.apiUrl === \"string\" ? snapshot.apiUrl : null,\n };\n}\n\n/** The API a draft read goes to: the server's runtime env first (hosting injects it), then the build's. */\nexport function sectionsApiUrl(fallback: string | null): string | null {\n const fromProcess = typeof process !== \"undefined\" ? process.env?.PUBLIC_BCMS_API_URL : undefined;\n if (fromProcess) return fromProcess;\n try {\n return (import.meta.env?.PUBLIC_BCMS_API_URL as string | undefined) ?? fallback;\n } catch {\n return fallback;\n }\n}\n\n/**\n * The DRAFT block list of `page`, when this request is the Visual Editor's draft render of it.\n *\n * The editor enters draft mode with a PAGE preview token (the middleware puts it on\n * `Astro.locals.bcms`); the public preview endpoint that validated it returns that page's draft\n * blocks. The token names ONE page, so any other page renders published. Null on any failure: the\n * page then renders its published structure, never an error.\n */\nexport async function draftSections(page: string, token: string, apiUrl: string | null): Promise<ContentBlock[] | null> {\n if (!apiUrl || decodeTokenSlug(token) !== page) return null;\n try {\n const res = await fetch(`${apiUrl.replace(/\\/+$/, \"\")}/api/v1/preview/${encodeURIComponent(page)}?token=${encodeURIComponent(token)}`, {\n signal: AbortSignal.timeout(5000),\n });\n if (!res.ok) return null;\n const blocks = ((await res.json()) as { data?: { blockJson?: unknown } })?.data?.blockJson;\n return Array.isArray(blocks) ? (blocks as ContentBlock[]) : null;\n } catch {\n return null;\n }\n}\n\nconst escapeAttr = (value: string): string => value.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\");\n\n/**\n * A rendered slot with its block's stamp on the section's OWN root.\n *\n * One element: the attributes are spliced into its start tag and nothing else moves, so\n * `main > section`, `:first-child` and every animation target stay what the author wrote. Its own\n * `style` is kept and the block's declarations follow it, the order the in-place painter writes.\n * A root that already stamps a block is the site's own work and is left as it is.\n *\n * Anything else (text, several roots, a framework island) has no root to stamp, so a box of ours\n * carries the stamp. With no style on the block it is `display:contents`: it draws nothing, and the\n * page lays out as written. A box that draws nothing cannot show a padding, so a block WITH a\n * style gets `display:flow-root` and the style on it (@see sectionBoxStyle). It is marked\n * `data-bcms-style` like any root, and `data-bcms-box` tells the in-place painter to make the same\n * switch when the first style is published between builds.\n *\n * THE ONE LIMIT: a styled box is one block box. In a flex or grid parent its children are then one\n * item instead of several, and their margins stop collapsing with the neighbours'. A section with\n * one root element has no box and no such limit.\n */\nexport function stampSectionRoot(html: string, block: { id?: string | null; style?: unknown }, page: string): string {\n const attrs = sectionRootAttrs(block as Parameters<typeof sectionRootAttrs>[0]);\n const id = attrs[\"data-bcms-block\"];\n if (id === undefined) return html;\n if (stampSoleElement(html, {}) === null) {\n if (html.trim() === \"\") return html;\n const hide = attrs[\"data-bcms-hide\"];\n return (\n `<div style=\"${escapeAttr(sectionBoxStyle(attrs.style))}\" data-bcms-block=\"${escapeAttr(id)}\" data-bcms-style=\"\"` +\n `${hide ? ` data-bcms-hide=\"${escapeAttr(hide)}\"` : \"\"} ${SECTION_BOX_ATTR}=\"\" ${SECTIONS_ATTR}=\"${escapeAttr(page)}\">${html}</div>`\n );\n }\n const start = html.search(/\\S/);\n const end = tagEnd(html, start);\n let tag = html.slice(start, end);\n if (/\\sdata-bcms-block[\\s=>/]/.test(`${tag}>`)) return html;\n const selfClosed = tag.endsWith(\"/\");\n if (selfClosed) tag = tag.slice(0, -1);\n tag = tag.replace(/\\s+$/, \"\");\n let style = attrs.style;\n const own = /\\sstyle\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)')/.exec(tag);\n if (own && style) {\n // The site's declarations first, the block's after: a later declaration wins, as the painter\n // orders them. Written where the site's own attribute stood, as React's `cloneElement` does.\n const merged = `${(own[1] ?? own[2] ?? \"\").replace(/[;\\s]+$/, \"\")};${style}`;\n tag = `${tag.slice(0, own.index)} style=\"${escapeAttr(merged)}\"${tag.slice(own.index + own[0].length)}`;\n style = undefined;\n }\n const added = Object.entries({ ...attrs, style, [SECTIONS_ATTR]: page })\n .filter((entry): entry is [string, string] => entry[1] !== undefined)\n .map(([name, value]) => ` ${name}=\"${escapeAttr(value)}\"`)\n .join(\"\");\n return html.slice(0, start) + tag + added + (selfClosed ? \" /\" : \"\") + html.slice(end);\n}\n","/**\n * Visual Editor addresses for the Astro renderer.\n *\n * Identical contract to `@bettercms-ai/next`'s `bindings.ts`, and the SAME reason for existing:\n * inside a component instance the definition's block ids address nothing, because they are\n * shared by every instance of it. Stamping them produces an orphan on one placement and a\n * duplicate that drops BOTH on two.\n *\n * Kept as a `.ts` module rather than inlined in the `.astro` frontmatter so the logic is\n * testable without an Astro compiler, and so the two SDKs cannot drift from each other by\n * having one file edited and not the other.\n */\nimport type { BindingKind } from \"@bettercms-ai/types\";\nimport { formatPropsAttribute } from \"@bettercms-ai/types\";\n\nexport interface BindingCtx {\n /** Present only inside a component instance, where it is AUTHORITATIVE. */\n fieldAddresses?: Map<string, string>;\n}\n\n/** The address a write lands on, or null when none exists. */\nexport function addressFor(id: string | undefined, prop: string, ctx: BindingCtx): string | null {\n if (!id) return null;\n const key = `${id}__${prop}`;\n if (ctx.fieldAddresses) return ctx.fieldAddresses.get(key) ?? null;\n return key;\n}\n\n/**\n * Attributes for a value rendered as this element's own content. Astro omits `undefined`\n * attributes, so an empty object spreads to nothing.\n */\nexport function bind(\n id: string | undefined,\n prop: string,\n kind: BindingKind,\n ctx: BindingCtx,\n): Record<string, string> {\n const address = addressFor(id, prop, ctx);\n return address ? { \"data-bcms-field\": address, \"data-bcms-kind\": kind } : {};\n}\n\n/**\n * Attributes for values that live in an ATTRIBUTE of this element. `data-bcms-field` addresses\n * one element and an element cannot hold two, so these ride `data-bcms-props`:\n * `<address>|<kind>|<domAttribute>`, semicolon separated.\n *\n * An entry whose value is absent is dropped — `setFieldPath` refuses to create a final key, so\n * the address would be a write the editor rejects.\n */\nexport function bindProps(\n id: string | undefined,\n entries: readonly { prop: string; kind: BindingKind; attr: string; present: boolean }[],\n ctx: BindingCtx,\n): Record<string, string> {\n const resolved = entries.flatMap(({ prop, kind, attr, present }) => {\n if (!present) return [];\n const address = addressFor(id, prop, ctx);\n return address ? [{ address, kind, attr }] : [];\n });\n return resolved.length ? { \"data-bcms-props\": formatPropsAttribute(resolved) } : {};\n}\n\nconst VOID_TAGS = new Set([\"area\", \"base\", \"br\", \"col\", \"embed\", \"hr\", \"img\", \"input\", \"link\", \"meta\", \"source\", \"track\", \"wbr\"]);\nconst RAW_TEXT_TAGS = new Set([\"script\", \"style\", \"textarea\", \"title\"]);\n\n/** Where the tag opened at `from` (`<`) ends, quotes respected; -1 when it never does. */\nexport function tagEnd(html: string, from: number): number {\n let quote = \"\";\n for (let i = from; i < html.length; i++) {\n const c = html[i]!;\n if (quote) {\n if (c === quote) quote = \"\";\n } else if (c === '\"' || c === \"'\") quote = c;\n else if (c === \">\") return i;\n }\n return -1;\n}\n\n/**\n * `<BcmsField>`'s child, when it is ONE element: the same markup with the binding attributes on\n * that element's own tag. Null when the rendered slot is anything else (text, several elements,\n * an element that already declares a field, a framework island), and the caller wraps as before.\n *\n * Why not always wrap: the wrapper is a `<span>` between the author's parent and the author's\n * element. `.hero > h1`, `gsap.utils.toArray(\".hero > *\")` and `:first-child` all stop matching,\n * and an animation that targeted the heading targets nothing. With the attributes on the element\n * itself the page's DOM is exactly the one the author wrote.\n *\n * A scan, not a parser: it only has to find where the first element ends. Anything it is not\n * sure of answers null, which is the old behaviour.\n */\nexport function stampSoleElement(html: string, attrs: Record<string, string | undefined>): string | null {\n const start = html.search(/\\S/);\n const open = /^<([a-zA-Z][\\w:-]*)/.exec(html.slice(start));\n if (start < 0 || !open) return null;\n const tag = open[1]!.toLowerCase();\n const openEnd = tagEnd(html, start);\n // A custom element is a framework's own wrapper (`astro-island`): not the author's element.\n if (openEnd < 0 || tag.includes(\"-\") || /\\sdata-bcms-(?:layout-)?field[\\s=>/]/.test(html.slice(start, openEnd + 1))) return null;\n let i = openEnd + 1;\n if (!VOID_TAGS.has(tag) && html[openEnd - 1] !== \"/\") {\n let depth = 1;\n let raw = RAW_TEXT_TAGS.has(tag) ? tag : \"\";\n while (depth > 0) {\n const lt = html.indexOf(\"<\", i);\n if (lt < 0) return null;\n if (raw) {\n const close = html.toLowerCase().indexOf(`</${raw}`, i);\n if (close < 0) return null;\n i = tagEnd(html, close) + 1;\n if (i === 0) return null;\n raw = \"\";\n depth--;\n continue;\n }\n if (html.startsWith(\"<!--\", lt)) {\n const end = html.indexOf(\"-->\", lt + 4);\n if (end < 0) return null;\n i = end + 3;\n continue;\n }\n const end = tagEnd(html, lt);\n if (end < 0) return null;\n const name = /^<\\/?([a-zA-Z][\\w:-]*)/.exec(html.slice(lt, end + 1))?.[1]?.toLowerCase();\n if (name && html[lt + 1] === \"/\") depth--;\n else if (name && !VOID_TAGS.has(name) && html[end - 1] !== \"/\") {\n depth++;\n if (RAW_TEXT_TAGS.has(name)) raw = name;\n }\n i = end + 1;\n }\n }\n if (html.slice(i).trim() !== \"\") return null;\n const escape = (value: string) => value.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\");\n const added = Object.entries(attrs)\n .filter((entry): entry is [string, string] => entry[1] !== undefined)\n .map(([name, value]) => ` ${name}=\"${escape(value)}\"`)\n .join(\"\");\n // Last on the tag, where React's `cloneElement` puts them: the two lanes emit the same bytes.\n const selfClosed = html[openEnd - 1] === \"/\";\n const at = selfClosed ? openEnd - 1 : openEnd;\n return html.slice(0, at).replace(/\\s+$/, \"\") + added + (selfClosed ? \" \" : \"\") + html.slice(at);\n}\n","/**\n * Runtime for @bettercms-ai/astro — the code the `bettercms:client` virtual\n * module binds to. Provides a published client, per-request draft client\n * selection, typed loaders, and signed-cookie helpers for draft mode.\n *\n * Read-only; no I/O at import time. Safe in Node and edge runtimes (uses Web\n * Crypto + global fetch only).\n */\nimport { createClient, type BetterCMSReadClient } from \"@bettercms-ai/sdk\";\nimport type {\n DeliveredLayout,\n DeliveryComponent,\n DeliveryEntry,\n DeliveryList,\n DeliveryPage,\n} from \"@bettercms-ai/types\";\n\nexport interface RuntimeConfig {\n apiUrl: string;\n workspace: string;\n projectId?: string;\n /** Server-only delivery key (a `content:read:draft` key for drafts). */\n apiKey?: string;\n /**\n * Collapse identical delivery GETs for the life of the process. Set by the integration\n * when Astro's `command === \"build\"` — a static build renders every route from one\n * published dataset, so a repeat read cannot legitimately differ.\n *\n * Never set for `dev` or SSR, where each request must see current content.\n */\n buildCache?: boolean;\n}\n\n/** Per-request draft state, set on `Astro.locals.bcms` by the middleware. */\nexport interface BcmsLocals {\n draft: boolean;\n token?: string;\n}\n\ninterface HasLocals {\n locals: { bcms?: BcmsLocals };\n}\n\nexport interface BetterCMSRuntime {\n /** Published-perspective client (use for SSG / non-draft reads). */\n client: BetterCMSReadClient;\n /** Pick the right client for a request (draft client when draft mode is on). */\n getClient(astro?: HasLocals): BetterCMSReadClient;\n loadEntry<T = Record<string, unknown>>(\n astro: HasLocals,\n slug: string,\n opts?: Parameters<BetterCMSReadClient[\"getEntry\"]>[1],\n ): Promise<DeliveryEntry<T>>;\n loadEntries<T = Record<string, unknown>>(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listEntries\"]>[0],\n ): Promise<DeliveryList<DeliveryEntry<T>>>;\n loadPage(astro: HasLocals, slug: string): Promise<DeliveryPage | null>;\n /** Component definitions for `<BcmsBlocks components={…}>` — draft-side in draft mode. */\n loadComponents(astro: HasLocals): Promise<DeliveryComponent[]>;\n loadLayout(astro: HasLocals): Promise<DeliveredLayout | null>;\n loadForms(\n astro: HasLocals,\n opts?: Parameters<BetterCMSReadClient[\"listForms\"]>[0],\n ): Promise<Awaited<ReturnType<BetterCMSReadClient[\"listForms\"]>>>;\n}\n\nexport function createRuntime(config: RuntimeConfig): BetterCMSRuntime {\n const { buildCache, ...clientConfig } = config;\n // One client for the whole build, so the cache below is shared across every route\n // rather than rebuilt per render.\n const published = createClient({ ...clientConfig, perspective: \"published\", cache: buildCache });\n\n const getClient = (astro?: HasLocals): BetterCMSReadClient => {\n const bcms = astro?.locals?.bcms;\n if (bcms?.draft) {\n // Explicitly uncached: a draft read exists to show the edit that just happened.\n return createClient({\n ...clientConfig,\n perspective: \"drafts\",\n previewToken: bcms.token,\n cache: false,\n });\n }\n return published;\n };\n\n return {\n client: published,\n getClient,\n loadEntry: (astro, slug, opts) => getClient(astro).getEntry(slug, opts),\n loadEntries: (astro, opts) => getClient(astro).listEntries(opts),\n loadPage: (astro, slug) => getClient(astro).getPage(slug),\n loadComponents: (astro) => getClient(astro).listComponents(),\n loadLayout: (astro) => getClient(astro).getLayout(),\n loadForms: (astro, opts) => getClient(astro).listForms(opts),\n };\n}\n\n/**\n * Build the live SSE URL for `<BcmsLive>`. The key is a query param because\n * browser EventSource cannot send headers — use a content:read key.\n *\n * `depth` sets the hydration depth of the document the stream pushes (default 1,\n * matching the delivery API). `drafts` streams autosaves so a preview surface\n * repaints as the editor types — it needs a `content:read:draft` key, and the\n * server ignores the flag for any other key rather than failing the connection.\n * Never enable it on a public site.\n */\nexport function liveSrc(opts: {\n apiUrl: string;\n workspace: string;\n apiKey: string;\n depth?: 0 | 1 | 2;\n drafts?: boolean;\n}): string {\n const base = opts.apiUrl.replace(/\\/+$/, \"\");\n const q = new URLSearchParams({ key: opts.apiKey });\n if (opts.depth !== undefined) q.set(\"depth\", String(opts.depth));\n if (opts.drafts) q.set(\"drafts\", \"1\");\n return `${base}/api/v1/delivery/${opts.workspace}/live?${q}`;\n}\n\n/**\n * The published content snapshot, read at request time on a server BetterCMS hosts (it rewrites the\n * file on every publish) and from `bcms-content.json` at build time. Read content through this —\n * never `import` bcms-content.json or `import.meta.glob` it: a bundled copy is frozen at build, and\n * the platform has to rebuild the whole site for every publish to change it.\n */\nexport { readPublishedContent, publishedEntry, type PublishedContent } from \"@bettercms-ai/sdk\";\n\n// Signed-cookie and draft-route helpers: one implementation for every adapter (@bettercms-ai/sdk/draft).\nexport { signValue, verifyValue, safeRedirectPath, decodeTokenSlug } from \"@bettercms-ai/sdk/draft\";\n\n/**\n * The draft-cookie signing secret, `process.env` FIRST.\n *\n * Vite may INLINE `import.meta.env.BCMS_DRAFT_SECRET` at build time, and the value present\n * when the customer's site is built is not the value the hosting container is given at start\n * — BetterCMS derives it per project and injects it as a real env var. Reading the runtime\n * env first is what makes managed hosting work without asking anyone to rebuild. The\n * `import.meta.env` read stays as the fallback for `astro dev` and for adapters with no\n * `process` (guarded, since touching `import.meta.env` outside Vite throws).\n */\nexport function draftSecret(): string | undefined {\n const fromProcess =\n typeof process !== \"undefined\" ? (process.env?.BCMS_DRAFT_SECRET as string | undefined) : undefined;\n if (fromProcess) return fromProcess;\n try {\n return import.meta.env?.BCMS_DRAFT_SECRET as string | undefined;\n } catch {\n return undefined;\n }\n}\n","/**\n * C3/C4 — `sectionRootAttrs(block)`: the attributes a site's OWN section root spreads so the\n * BetterCMS editor offers that section's spacing and style handles.\n *\n * <section class=\"hero\" {...sectionRootAttrs(block)}>…</section>\n *\n * The editor shows those handles on a site-rendered page only when EVERY root the site stamps\n * with `data-bcms-block` also carries `data-bcms-style`, which is the site's promise that \"this\n * root's inline style is `blockStyleToCss(block.style)`\" — the same declarations `<BcmsBlocks>`\n * puts on its own wrapper. A root that stamps the block without applying its style would get a\n * capsule that paints the canvas and never ships, so the marker must travel with the style.\n *\n * The marker is present even when the style is EMPTY (then there is no `style` attribute at all):\n * an unstyled section is exactly the one an author is about to give its first spacing edit.\n */\nimport { blockStyleToColumnCss, blockStyleToCss, hideOnAttr, type BlockStyle } from \"@bettercms-ai/types\";\n\nexport interface SectionRootBlock {\n id?: string | null;\n style?: BlockStyle | Record<string, unknown> | null;\n}\n\nexport interface SectionRootAttrs {\n \"data-bcms-block\"?: string;\n \"data-bcms-style\": \"\";\n /** `\"mobile tablet\"` when `style.hideOn` hides it there; the site ships `RESPONSIVE_CSS` for it. */\n \"data-bcms-hide\"?: string;\n /** `blockStyleToCss(block.style)` as a CSS string; absent when the block has no style. */\n style?: string;\n}\n\n/** `paddingTop` → `padding-top`, joined the way `<BcmsBlocks>` writes its wrapper's attribute. */\nfunction cssText(css: Record<string, string | number>): string {\n return Object.entries(css)\n .map(([k, v]) => `${k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}:${v}`)\n .join(\";\");\n}\n\nexport function sectionRootAttrs(block: SectionRootBlock | null | undefined): SectionRootAttrs {\n const css = blockStyleToCss((block?.style ?? undefined) as BlockStyle | undefined);\n const id = typeof block?.id === \"string\" && block.id !== \"\" ? block.id : undefined;\n const hide = hideOnAttr(block?.style);\n return {\n ...(id !== undefined ? { \"data-bcms-block\": id } : {}),\n \"data-bcms-style\": \"\",\n ...(hide ? { \"data-bcms-hide\": hide } : {}),\n ...(css ? { style: cssText(css) } : {}),\n };\n}\n\nexport interface SectionColumnAttrs {\n \"data-bcms-column\": \"\";\n /** `blockStyleToColumnCss(block.style)` as a CSS string; absent when the section has no column. */\n style?: string;\n}\n\n/**\n * C3 — `sectionColumnAttrs(block)`: the attributes a site's own INNER container spreads so the\n * section's content WIDTH (`style.contentWidth`, placed by `contentAlign`) ships.\n *\n * <section {...sectionRootAttrs(block)}>\n * <div class=\"container\" {...sectionColumnAttrs(block)}>…</div>\n * </section>\n *\n * The width is the column BetterCMS draws as `.bcms-block-inner` inside the section's styled box —\n * never on the root itself, whose background must stay full-bleed. `data-bcms-column` is the\n * site's promise that this box's inline style is the column; the editor offers the width handle\n * on a site-rendered page only where every styled root holds one. Max-width plus left/right\n * margins, so the container keeps its own vertical margins and, with no column, its own width.\n */\nexport function sectionColumnAttrs(block: SectionRootBlock | null | undefined): SectionColumnAttrs {\n const css = blockStyleToColumnCss((block?.style ?? undefined) as BlockStyle | undefined);\n return { \"data-bcms-column\": \"\", ...(css ? { style: cssText(css) } : {}) };\n}\n"],"mappings":"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","names":["readPublishedContent","end","readPublishedContent"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bettercms-ai/astro",
3
- "version": "0.28.1",
3
+ "version": "0.28.2",
4
4
  "type": "module",
5
5
  "description": "The BetterCMS adapter for Astro — a `bettercms()` integration, a `bettercms:client` virtual module, typed content loaders, draft preview, and native .astro rendering components.",
6
6
  "main": "./dist/index.js",
@@ -61,9 +61,9 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "@bettercms-ai/image-url": "^0.4.0",
64
- "@bettercms-ai/next": "^0.27.1",
64
+ "@bettercms-ai/next": "^0.28.0",
65
65
  "@bettercms-ai/richtext": "^0.12.0",
66
- "@bettercms-ai/sdk": "^1.31.0",
66
+ "@bettercms-ai/sdk": "^1.33.0",
67
67
  "@bettercms-ai/types": "^1.48.0",
68
68
  "@bettercms-ai/ui": "^0.20.0",
69
69
  "libphonenumber-js": "^1.13.13"