@brandfine/client 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @brandfine/client
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 28bbfe7: Add the `bf.liveChat` namespace for the Live Chat plugin:
8
+ - `liveChat.getConfig()` — server-side bootstrap (`GET /external/live-chat/bootstrap`, broad workspace key): returns the scoped publishable key + widget display config for baking into your site at build/request time.
9
+ - `liveChat.install({ config })` — client-side injector: appends the widget host div (carrying only the publishable key — no secrets in the browser) and the widget script. Idempotent, mirrors `analytics.install()`'s result contract (`installed` / `disabled` / `ssr` / `already-installed`).
10
+
11
+ New exported types: `LiveChatBootstrap`, `LiveChatInstallResult`, `LiveChatInstallOptions`.
12
+
13
+ ## 0.8.0
14
+
15
+ ### Minor Changes
16
+
17
+ - 351b5ae: `bf.analytics.install()` now also loads the Google tag (gtag.js)
18
+ when the workspace's GA4 property was provisioned through
19
+ Brandfine and the customer opted into tag injection. The
20
+ `AnalyticsConfig` type gains an optional `gaMeasurementId` field
21
+ on both variants. Injection is idempotent and deliberately
22
+ no-ops when any gtag loader is already present on the page —
23
+ a hand-installed Google Analytics setup is never double-tagged.
24
+ Note: gtag sets cookies; consent banners remain the site's
25
+ responsibility.
26
+ - 2d9d037: Add `bf.analytics.overview()` — a composed traffic report for the
27
+ workspace (summary KPIs + bucketed timeseries + top pages + referrer
28
+ sources + visitor countries + device classes) served from the new
29
+ `GET /external/analytics/overview` endpoint. Accepts a
30
+ `range` preset (`'24h' | '7d' | '30d' | '90d'`, default `'7d'`) and
31
+ returns one of three shapes: `{ enabled: false }`, `{ enabled: true,
32
+ verified: false }`, or the full payload. New exported types:
33
+ `AnalyticsOverview`, `AnalyticsOverviewRange`. Intended for
34
+ server-side dashboard rendering — this is the same endpoint the
35
+ WordPress plugin's wp-admin insights panel consumes.
36
+
3
37
  ## 0.7.0
4
38
 
5
39
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -22,6 +22,27 @@ var BrandfineApiError = class extends Error {
22
22
  }
23
23
  };
24
24
  var INSTALLED_MARKER = "data-brandfine-analytics";
25
+ var LIVE_CHAT_MARKER = "data-brandfine-live-chat";
26
+ var GTAG_MARKER = "data-brandfine-gtag";
27
+ function injectGoogleTag(measurementId) {
28
+ if (typeof document === "undefined") return;
29
+ const existing = document.querySelector(
30
+ `script[src*="googletagmanager.com/gtag/js"], script[${GTAG_MARKER}]`
31
+ );
32
+ if (existing) return;
33
+ const loader = document.createElement("script");
34
+ loader.async = true;
35
+ loader.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`;
36
+ loader.setAttribute(GTAG_MARKER, measurementId);
37
+ document.head.appendChild(loader);
38
+ const w = window;
39
+ w.dataLayer = w.dataLayer ?? [];
40
+ function gtag(..._args) {
41
+ w.dataLayer.push(arguments);
42
+ }
43
+ gtag("js", /* @__PURE__ */ new Date());
44
+ gtag("config", measurementId);
45
+ }
25
46
  var DEFAULT_USER_AGENT = "@brandfine/client";
26
47
  function createBrandfineClient(config) {
27
48
  if (!config.baseUrl)
@@ -111,11 +132,20 @@ function createBrandfineClient(config) {
111
132
  getConfig() {
112
133
  return get("/external/analytics-config");
113
134
  },
135
+ overview(opts = {}) {
136
+ const range = opts.range ?? "7d";
137
+ return get(
138
+ `/external/analytics/overview?range=${encodeURIComponent(range)}`
139
+ );
140
+ },
114
141
  async install(opts = {}) {
115
142
  if (typeof document === "undefined") {
116
143
  return { installed: false, reason: "ssr" };
117
144
  }
118
145
  const cfg = opts.config ?? await analytics.getConfig();
146
+ if (cfg.gaMeasurementId) {
147
+ injectGoogleTag(cfg.gaMeasurementId);
148
+ }
119
149
  if (!cfg.enabled) {
120
150
  return { installed: false, reason: "disabled" };
121
151
  }
@@ -134,6 +164,43 @@ function createBrandfineClient(config) {
134
164
  return { installed: true, websiteId: cfg.websiteId };
135
165
  }
136
166
  };
167
+ const liveChat = {
168
+ getConfig() {
169
+ return get("/external/live-chat/bootstrap");
170
+ },
171
+ async install(opts) {
172
+ if (typeof document === "undefined") {
173
+ return { installed: false, reason: "ssr" };
174
+ }
175
+ const cfg = opts.config;
176
+ if (!cfg.enabled) {
177
+ return { installed: false, reason: "disabled" };
178
+ }
179
+ const existing = document.querySelector(`[${LIVE_CHAT_MARKER}]`);
180
+ if (existing) {
181
+ return { installed: false, reason: "already-installed" };
182
+ }
183
+ const host = document.createElement("div");
184
+ host.setAttribute("data-bf-live-chat", "");
185
+ host.setAttribute("data-publishable-key", cfg.publishableKey);
186
+ host.setAttribute("data-base-url", baseUrl);
187
+ host.setAttribute(LIVE_CHAT_MARKER, "");
188
+ if (cfg.theme) {
189
+ for (const [key, value] of Object.entries(cfg.theme)) {
190
+ if (key.startsWith("--bf-chat-")) {
191
+ host.style.setProperty(key, value);
192
+ }
193
+ }
194
+ }
195
+ document.body.appendChild(host);
196
+ const script = document.createElement("script");
197
+ script.defer = true;
198
+ script.src = `${baseUrl}${cfg.scriptPath}`;
199
+ script.setAttribute(LIVE_CHAT_MARKER, "script");
200
+ document.head.appendChild(script);
201
+ return { installed: true };
202
+ }
203
+ };
137
204
  const submissions = {
138
205
  async create(input) {
139
206
  const url = `${baseUrl}/external/submissions`;
@@ -209,7 +276,8 @@ function createBrandfineClient(config) {
209
276
  navigations,
210
277
  analytics,
211
278
  submissions,
212
- appointments
279
+ appointments,
280
+ liveChat
213
281
  };
214
282
  }
215
283
  function toIso(d) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `createBrandfineClient` — the SDK's entry point.\n *\n * Returns a stateless, multi-instance-safe handle scoped to a\n * single `(baseUrl, apiKey)` pair. Pattern follows the Stripe /\n * Algolia / OpenAI SDKs — explicit construction with config,\n * namespaced methods (`bf.posts.list(...)`, `bf.workspace.get()`),\n * no module-level singletons.\n *\n * Why factory not module-level state: multi-tenant consumers\n * sometimes need two clients in the same process (e.g. main site\n * + admin preview). Module-level env reading makes that impossible\n * without monkey-patching.\n */\n\nimport type {\n BrandfineCategory,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n\nexport type BrandfineClientConfig = {\n /** Base URL of the Brandfine API. No trailing slash — the client\n * trims one if you pass it anyway. e.g. `https://api.brandfine.co` */\n baseUrl: string\n /** Workspace-scoped API key. Generated from the cms's Workspace\n * settings; identifies which workspace the client talks to. */\n apiKey: string\n /** Optional fetch override. Useful for tests (inject a stub),\n * for runtimes that need a custom implementation (edge workers\n * with non-standard fetch), or to add cross-cutting concerns\n * like tracing / retries. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch\n /** Optional User-Agent header. Falls back to a generic SDK tag. */\n userAgent?: string\n}\n\n/**\n * Structured error thrown by every request helper on non-2xx\n * responses. Carries the raw body so consumers can log it for\n * debugging without re-fetching.\n */\nexport class BrandfineApiError extends Error {\n override readonly name = 'BrandfineApiError'\n readonly status: number\n readonly statusText: string\n readonly body: string\n readonly url: string\n\n constructor(args: {\n status: number\n statusText: string\n body: string\n url: string\n }) {\n super(\n `[brandfine] ${args.status} ${args.statusText} on ${args.url} — ${args.body.slice(0, 200)}`,\n )\n this.status = args.status\n this.statusText = args.statusText\n this.body = args.body\n this.url = args.url\n }\n}\n\ntype RequestOptions = {\n /** When true and the response is 404, return `null` instead of\n * throwing. Used by endpoints where 404 is a meaningful empty\n * state (navigation by key, single post by slug). */\n nullable404?: boolean\n signal?: AbortSignal\n}\n\nexport type BrandfineClient = {\n /** Low-level GET. Reserved for endpoints we don't have a typed\n * helper for yet. Adds the X-Api-Key header automatically. */\n get: <T>(path: string, opts?: RequestOptions) => Promise<T>\n posts: PostsApi\n categories: CategoriesApi\n workspace: WorkspaceApi\n navigations: NavigationsApi\n analytics: AnalyticsApi\n submissions: SubmissionsApi\n appointments: AppointmentsApi\n}\n\ntype PostsApi = {\n /** Paginated list of published posts. Handles the cms's\n * pagination transparently — caller gets a flat array. */\n list: <TConfig = unknown>(\n opts?: ListPostsOptions,\n ) => Promise<BrandfinePost<TConfig>[]>\n /** Single post by per-locale URL slug, scoped to the active\n * locale on the workspace's content. Returns `null` for 404 so\n * callers can render their own \"not found\" page without try/catch. */\n getBySlug: <TConfig = unknown>(\n slug: string,\n ) => Promise<BrandfinePost<TConfig> | null>\n}\n\ntype CategoriesApi = {\n list: (opts?: ListCategoriesOptions) => Promise<BrandfineCategory[]>\n}\n\ntype WorkspaceApi = {\n get: <\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() => Promise<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>\n}\n\ntype NavigationsApi = {\n /** Navigation by its workspace-scoped `key` (e.g. `'header'`).\n * Returns `null` for 404 so consumers can fall back to a\n * hardcoded default without try/catch. `TConfig` narrows each\n * item's `customConfig` (default `unknown`). */\n get: <TConfig = unknown>(\n key: string,\n ) => Promise<BrandfineNavigation<TConfig> | null>\n}\n\nexport type CreateSubmissionInput = {\n /** Required. Display name of the submitter. */\n name: string\n /** Required. Validated server-side. */\n email: string\n /** Optional. Free-text up to 40 chars. */\n phone?: string\n /** Optional. Free-text up to 200 chars. */\n subject?: string\n /** Required. The message body — up to 10,000 chars. */\n message: string\n /** Optional. Where the submission came from — e.g. a route path\n * like `/contact`, or a marketing campaign label. Up to 500 chars. */\n source?: string\n /** Optional. Free-form JSON metadata the consumer attaches; the\n * cms surfaces it verbatim in the submissions admin view. */\n metadata?: Record<string, unknown>\n}\n\nexport type Submission = {\n id: string\n createdAt: string\n}\n\ntype SubmissionsApi = {\n /**\n * Posts a contact-form submission to `POST /external/submissions`\n * for this workspace. The cms surfaces the submission in the\n * Submissions inbox.\n *\n * Throws `BrandfineApiError` on validation failures (400) or\n * any other non-2xx — caller decides whether to surface that as\n * a user-visible error or a silent retry.\n */\n create: (input: CreateSubmissionInput) => Promise<Submission>\n}\n\n// ----------------------------------------------------------------\n// Appointments plugin SDK — pairs with the Appointments embed\n// widget. Consumers who want full control over the booking UI use\n// these methods directly; consumers who want the drop-in widget\n// use the `<script>` embed (which itself uses these methods under\n// the hood). The same `BrandfineClient` instance powers both.\n// ----------------------------------------------------------------\n\nexport type AppointmentSlot = {\n /** UTC ISO 8601 timestamp of the slot start. */\n start: string\n /** UTC ISO 8601 timestamp of the slot end. */\n end: string\n}\n\nexport type AppointmentAvailability = {\n /** False = plugin not activated, or activation row's `enabled`\n * flag is off. Widgets should render a \"not accepting bookings\"\n * state, not throw. */\n enabled: boolean\n /** Source-of-truth IANA timezone for the workspace's business\n * hours. Visitors see slots in their local TZ — use this for\n * the \"(workspace local: HH:MM)\" subtext. */\n timezone: string\n slotDurationMinutes: number\n leadTimeHours: number\n bookingWindowDays: number\n policyText: string | null\n slots: AppointmentSlot[]\n /** UTC ISO 8601. Useful for the widget's date range label. */\n windowStart: string\n windowEnd: string\n}\n\nexport type CreateAppointmentRequestInput = {\n visitorName: string\n visitorEmail: string\n visitorPhone?: string\n visitorMessage?: string\n /** UTC ISO 8601 of the requested slot start. Server re-validates\n * against business hours + busy ranges before accepting. */\n requestedAt: string\n /** Optional cookie-derived session id from the consumer site. */\n visitorSessionId?: string\n}\n\nexport type CreatedAppointmentRequest = {\n id: string\n createdAt: string\n requestedAt: string\n durationMinutes: number\n status: 'PENDING'\n /** Visitor's self-cancel token. Embed it in confirmation\n * emails / on-page UI so the visitor can cancel without an\n * account. One-time use; revoked once any party acts. */\n cancellationToken: string | null\n}\n\ntype AppointmentsApi = {\n /**\n * Available slots for the workspace's booking window.\n * `from` / `to` are optional clamps inside the workspace's\n * configured window — the server ignores ranges outside.\n */\n getAvailability: (opts?: {\n from?: Date | string\n to?: Date | string\n }) => Promise<AppointmentAvailability>\n /**\n * Submit a visitor's appointment request. Server-side validates\n * the slot is still bookable; if it isn't, throws\n * `BrandfineApiError` with status 404 / 409.\n *\n * The visitor's browser does not have any other appointment\n * actions in v1 — post-submission status changes (approve /\n * decline / reschedule) happen via email, driven by the\n * customer in the CMS.\n */\n createRequest: (\n input: CreateAppointmentRequestInput,\n ) => Promise<CreatedAppointmentRequest>\n}\n\nexport type AnalyticsConfig =\n | { enabled: false }\n | { enabled: true; websiteId: string; scriptUrl: string }\n\nexport type AnalyticsInstallResult =\n | { installed: false; reason: 'disabled' | 'ssr' | 'already-installed' }\n | { installed: true; websiteId: string }\n\nexport type InstallOptions = {\n /**\n * Pre-known config. When provided, `install()` skips the round-\n * trip to `/external/analytics-config` and injects the script\n * immediately. Use this when you've baked the values into your\n * build (env vars, CMS-side config dump, etc.) — typical for\n * static sites where the analytics state is decided at deploy\n * time, not per page load.\n *\n * Trade-off vs the default fetch path: if you disable analytics\n * in Brandfine, the tracker keeps loading until your next\n * deploy. That's usually the right trade for static sites\n * (which redeploy on every content change anyway) and the wrong\n * trade for dynamic sites where the api round-trip is cheap\n * relative to the rest of the page.\n *\n * Pass `{ enabled: false }` to force a no-op without touching\n * the api (e.g. to disable analytics for one environment without\n * changing Brandfine's state).\n */\n config?: AnalyticsConfig\n}\n\ntype AnalyticsApi = {\n /**\n * Injects the Brandfine analytics tracker into `document.head`\n * once. Safe to call on every page load — idempotent via a\n * marker attribute on the injected script tag.\n *\n * Two paths:\n * - `install()` — fetches the config from Brandfine, then\n * injects. Reflects enable/disable state on next page load.\n * - `install({ config })` — uses caller-provided config, skips\n * the fetch. Faster, no round-trip; ignores Brandfine state\n * changes until the consumer's next deploy.\n *\n * Returns details about what happened:\n * - `{ installed: true, websiteId }` — script was just injected.\n * - `{ installed: false, reason: 'disabled' }` — config says\n * analytics is off; no-op.\n * - `{ installed: false, reason: 'ssr' }` — no `document` in\n * scope (server-side). Call again on the client.\n * - `{ installed: false, reason: 'already-installed' }` — a\n * prior call (or another tab in the same SPA) already injected.\n *\n * Throws `BrandfineApiError` on non-2xx responses other than the\n * disabled case (which is a valid `{ enabled: false }` body).\n */\n install: (opts?: InstallOptions) => Promise<AnalyticsInstallResult>\n\n /** Lower-level helper — fetches the raw config without touching\n * the DOM. Useful when you want to inject the script yourself\n * (e.g. via a framework's <Script> component for nonce/csp). */\n getConfig: () => Promise<AnalyticsConfig>\n}\n\n/** Attribute we stamp on the injected <script> so `install()` is\n * idempotent across re-renders and SPA route changes. */\nconst INSTALLED_MARKER = 'data-brandfine-analytics'\n\nconst DEFAULT_USER_AGENT = '@brandfine/client'\n\nexport function createBrandfineClient(\n config: BrandfineClientConfig,\n): BrandfineClient {\n if (!config.baseUrl)\n throw new Error('createBrandfineClient: `baseUrl` is required')\n if (!config.apiKey)\n throw new Error('createBrandfineClient: `apiKey` is required')\n\n const baseUrl = config.baseUrl.replace(/\\/$/, '')\n const apiKey = config.apiKey\n // Resolve fetch lazily so consumers in environments without a\n // global fetch can polyfill before constructing the client.\n const fetchImpl: typeof fetch = config.fetch ?? globalThis.fetch\n const userAgent = config.userAgent ?? DEFAULT_USER_AGENT\n\n async function get<T>(path: string, opts: RequestOptions = {}): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'GET',\n headers: {\n 'X-Api-Key': apiKey,\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n signal: opts.signal,\n })\n if (res.status === 404 && opts.nullable404) {\n // Drain the body so the underlying socket can be reused —\n // fetch implementations that don't auto-drain (older Node)\n // can leak otherwise.\n await res.text().catch(() => '')\n return null as T\n }\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as T\n }\n\n const posts: PostsApi = {\n async list<TConfig = unknown>(opts: ListPostsOptions = {}) {\n const out: BrandfinePost<TConfig>[] = []\n let page = 1\n const typeQuery = opts.type ? `&type=${encodeURIComponent(opts.type)}` : ''\n const localeQuery = opts.locale\n ? `&locale=${encodeURIComponent(opts.locale)}`\n : ''\n // Default pagination at the cms's 50-per-page cap. `forceLimit`\n // opts past it for content types that would otherwise need\n // many round-trips.\n const sizeQuery = opts.forceLimit\n ? `&force_limit=${opts.forceLimit}`\n : '&limit=50'\n // Pathological safety brake — 200 pages × 50 = 10k posts. If\n // a workspace ever needs more, callers should hit the API\n // directly with their own pagination logic.\n const MAX_PAGES = 200\n while (page <= MAX_PAGES) {\n const data = await get<BrandfinePostListResponse<TConfig>>(\n `/external/posts?include=content${sizeQuery}&page=${page}${typeQuery}${localeQuery}`,\n )\n out.push(...data.items)\n if (!data.pageInfo.hasNext) break\n page += 1\n }\n return out\n },\n async getBySlug<TConfig = unknown>(slug: string) {\n return get<BrandfinePost<TConfig> | null>(\n `/external/posts/${encodeURIComponent(slug)}`,\n { nullable404: true },\n )\n },\n }\n\n const categories: CategoriesApi = {\n async list(opts: ListCategoriesOptions = {}) {\n const qs = opts.locale ? `?locale=${encodeURIComponent(opts.locale)}` : ''\n const data = await get<{ items: BrandfineCategory[] }>(\n `/external/categories${qs}`,\n )\n return data.items\n },\n }\n\n const workspace: WorkspaceApi = {\n get<\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() {\n return get<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>(\n '/external/workspace',\n )\n },\n }\n\n const navigations: NavigationsApi = {\n get<TConfig = unknown>(key: string) {\n return get<BrandfineNavigation<TConfig> | null>(\n `/external/navigations/${encodeURIComponent(key)}`,\n { nullable404: true },\n )\n },\n }\n\n const analytics: AnalyticsApi = {\n getConfig() {\n return get<AnalyticsConfig>('/external/analytics-config')\n },\n async install(opts: InstallOptions = {}) {\n // SSR safety: nothing to inject without a DOM. Consumers\n // call this from useEffect / onMount, but defensive anyway\n // (some frameworks still execute the file body on the server).\n if (typeof document === 'undefined') {\n return { installed: false, reason: 'ssr' as const }\n }\n\n // Use caller-provided config if present (build-time path),\n // otherwise fetch (runtime path).\n const cfg = opts.config ?? (await analytics.getConfig())\n if (!cfg.enabled) {\n return { installed: false, reason: 'disabled' as const }\n }\n\n // Idempotency: a prior call (StrictMode double-invoke, SPA\n // re-mount, second instance with the same workspace) may\n // have already injected. The marker attribute is the source\n // of truth — checking by script src would also miss the case\n // where two workspaces share the same scriptUrl.\n const existing = document.querySelector<HTMLScriptElement>(\n `script[${INSTALLED_MARKER}=\"${cfg.websiteId}\"]`,\n )\n if (existing) {\n return { installed: false, reason: 'already-installed' as const }\n }\n\n const script = document.createElement('script')\n script.defer = true\n script.src = cfg.scriptUrl\n script.setAttribute('data-website-id', cfg.websiteId)\n // The marker doubles as a sentinel + a debug aid (you can\n // grep the DOM for `data-brandfine-analytics` to confirm\n // an install).\n script.setAttribute(INSTALLED_MARKER, cfg.websiteId)\n document.head.appendChild(script)\n return { installed: true, websiteId: cfg.websiteId }\n },\n }\n\n const submissions: SubmissionsApi = {\n async create(input: CreateSubmissionInput) {\n const url = `${baseUrl}/external/submissions`\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n body: JSON.stringify(input),\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as Submission\n },\n }\n\n /**\n * Shared POST helper for the appointments namespace. The main\n * `get()` helper handles GETs; submissions has its own inline\n * POST because it predates this refactor. New plugin namespaces\n * (appointments first, others to follow) share this one so the\n * error-handling shape stays consistent.\n */\n async function post<T>(\n path: string,\n body: unknown,\n opts: RequestOptions = {},\n ): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n body: JSON.stringify(body),\n signal: opts.signal,\n })\n if (!res.ok) {\n const errBody = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body: errBody,\n url,\n })\n }\n if (res.status === 204) return undefined as T\n return (await res.json()) as T\n }\n\n const appointments: AppointmentsApi = {\n getAvailability(opts = {}) {\n const qs: string[] = []\n if (opts.from) qs.push(`from=${encodeURIComponent(toIso(opts.from))}`)\n if (opts.to) qs.push(`to=${encodeURIComponent(toIso(opts.to))}`)\n const suffix = qs.length ? `?${qs.join('&')}` : ''\n return get<AppointmentAvailability>(\n `/external/appointments/availability${suffix}`,\n )\n },\n createRequest(input) {\n return post<CreatedAppointmentRequest>(\n '/external/appointments/requests',\n input,\n )\n },\n }\n\n return {\n get,\n posts,\n categories,\n workspace,\n navigations,\n analytics,\n submissions,\n appointments,\n }\n}\n\n/** Accepts a Date or an already-ISO string and returns ISO. Saves\n * every caller from `.toISOString()`-ing manually. */\nfunction toIso(d: Date | string): string {\n return typeof d === 'string' ? d : d.toISOString()\n}\n","/**\n * @brandfine/client — root entry.\n *\n * The full SDK surface is exposed here for \"import everything from\n * one place\" usage. Tree-shaking + `sideEffects: false` mean\n * consumers don't pay a bundle cost for what they don't import.\n *\n * Heavier or framework-coupled pieces still live under subpath\n * exports (`@brandfine/client/cache`, `/resolvers`, `/webhook`) so\n * consumers with poor tree-shaking — or who only need one slice —\n * can scope their imports.\n */\n\nexport const SDK_VERSION = '0.0.0' as const\n\nexport {\n BrandfineApiError,\n createBrandfineClient,\n type AnalyticsConfig,\n type AnalyticsInstallResult,\n type BrandfineClient,\n type BrandfineClientConfig,\n type CreateSubmissionInput,\n type InstallOptions,\n type Submission,\n} from './client'\n\nexport {\n createCache,\n createKeyedCache,\n type Cache,\n type CacheOptions,\n type KeyedCache,\n type KeyedCacheOptions,\n} from './cache/index'\n\nexport {\n isLocale,\n localizePath,\n pickLocale,\n resolveNavigation,\n stripLocalePrefix,\n type HydratedNav,\n type HydratedNavItem,\n type LocaleOptions,\n type ResolveNavigationOptions,\n} from './resolvers/index'\n\nexport {\n createBrandfineWebhookHandler,\n parseWebhookPayload,\n verifyWebhookSecret,\n type BrandfineWebhookEvent,\n type BrandfineWebhookHandlerOptions,\n type BrandfineWebhookPayload,\n} from './webhook/index'\n\nexport type {\n BrandfineCategory,\n BrandfineNavItem,\n BrandfineNavItemType,\n BrandfineNavPost,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfinePostTranslation,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `createBrandfineClient` — the SDK's entry point.\n *\n * Returns a stateless, multi-instance-safe handle scoped to a\n * single `(baseUrl, apiKey)` pair. Pattern follows the Stripe /\n * Algolia / OpenAI SDKs — explicit construction with config,\n * namespaced methods (`bf.posts.list(...)`, `bf.workspace.get()`),\n * no module-level singletons.\n *\n * Why factory not module-level state: multi-tenant consumers\n * sometimes need two clients in the same process (e.g. main site\n * + admin preview). Module-level env reading makes that impossible\n * without monkey-patching.\n */\n\nimport type {\n BrandfineCategory,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n\nexport type BrandfineClientConfig = {\n /** Base URL of the Brandfine API. No trailing slash — the client\n * trims one if you pass it anyway. e.g. `https://api.brandfine.co` */\n baseUrl: string\n /** Workspace-scoped API key. Generated from the cms's Workspace\n * settings; identifies which workspace the client talks to. */\n apiKey: string\n /** Optional fetch override. Useful for tests (inject a stub),\n * for runtimes that need a custom implementation (edge workers\n * with non-standard fetch), or to add cross-cutting concerns\n * like tracing / retries. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch\n /** Optional User-Agent header. Falls back to a generic SDK tag. */\n userAgent?: string\n}\n\n/**\n * Structured error thrown by every request helper on non-2xx\n * responses. Carries the raw body so consumers can log it for\n * debugging without re-fetching.\n */\nexport class BrandfineApiError extends Error {\n override readonly name = 'BrandfineApiError'\n readonly status: number\n readonly statusText: string\n readonly body: string\n readonly url: string\n\n constructor(args: {\n status: number\n statusText: string\n body: string\n url: string\n }) {\n super(\n `[brandfine] ${args.status} ${args.statusText} on ${args.url} — ${args.body.slice(0, 200)}`,\n )\n this.status = args.status\n this.statusText = args.statusText\n this.body = args.body\n this.url = args.url\n }\n}\n\ntype RequestOptions = {\n /** When true and the response is 404, return `null` instead of\n * throwing. Used by endpoints where 404 is a meaningful empty\n * state (navigation by key, single post by slug). */\n nullable404?: boolean\n signal?: AbortSignal\n}\n\nexport type BrandfineClient = {\n /** Low-level GET. Reserved for endpoints we don't have a typed\n * helper for yet. Adds the X-Api-Key header automatically. */\n get: <T>(path: string, opts?: RequestOptions) => Promise<T>\n posts: PostsApi\n categories: CategoriesApi\n workspace: WorkspaceApi\n navigations: NavigationsApi\n analytics: AnalyticsApi\n submissions: SubmissionsApi\n appointments: AppointmentsApi\n liveChat: LiveChatApi\n}\n\ntype PostsApi = {\n /** Paginated list of published posts. Handles the cms's\n * pagination transparently — caller gets a flat array. */\n list: <TConfig = unknown>(\n opts?: ListPostsOptions,\n ) => Promise<BrandfinePost<TConfig>[]>\n /** Single post by per-locale URL slug, scoped to the active\n * locale on the workspace's content. Returns `null` for 404 so\n * callers can render their own \"not found\" page without try/catch. */\n getBySlug: <TConfig = unknown>(\n slug: string,\n ) => Promise<BrandfinePost<TConfig> | null>\n}\n\ntype CategoriesApi = {\n list: (opts?: ListCategoriesOptions) => Promise<BrandfineCategory[]>\n}\n\ntype WorkspaceApi = {\n get: <\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() => Promise<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>\n}\n\ntype NavigationsApi = {\n /** Navigation by its workspace-scoped `key` (e.g. `'header'`).\n * Returns `null` for 404 so consumers can fall back to a\n * hardcoded default without try/catch. `TConfig` narrows each\n * item's `customConfig` (default `unknown`). */\n get: <TConfig = unknown>(\n key: string,\n ) => Promise<BrandfineNavigation<TConfig> | null>\n}\n\nexport type CreateSubmissionInput = {\n /** Required. Display name of the submitter. */\n name: string\n /** Required. Validated server-side. */\n email: string\n /** Optional. Free-text up to 40 chars. */\n phone?: string\n /** Optional. Free-text up to 200 chars. */\n subject?: string\n /** Required. The message body — up to 10,000 chars. */\n message: string\n /** Optional. Where the submission came from — e.g. a route path\n * like `/contact`, or a marketing campaign label. Up to 500 chars. */\n source?: string\n /** Optional. Free-form JSON metadata the consumer attaches; the\n * cms surfaces it verbatim in the submissions admin view. */\n metadata?: Record<string, unknown>\n}\n\nexport type Submission = {\n id: string\n createdAt: string\n}\n\ntype SubmissionsApi = {\n /**\n * Posts a contact-form submission to `POST /external/submissions`\n * for this workspace. The cms surfaces the submission in the\n * Submissions inbox.\n *\n * Throws `BrandfineApiError` on validation failures (400) or\n * any other non-2xx — caller decides whether to surface that as\n * a user-visible error or a silent retry.\n */\n create: (input: CreateSubmissionInput) => Promise<Submission>\n}\n\n// ----------------------------------------------------------------\n// Appointments plugin SDK — pairs with the Appointments embed\n// widget. Consumers who want full control over the booking UI use\n// these methods directly; consumers who want the drop-in widget\n// use the `<script>` embed (which itself uses these methods under\n// the hood). The same `BrandfineClient` instance powers both.\n// ----------------------------------------------------------------\n\nexport type AppointmentSlot = {\n /** UTC ISO 8601 timestamp of the slot start. */\n start: string\n /** UTC ISO 8601 timestamp of the slot end. */\n end: string\n}\n\nexport type AppointmentAvailability = {\n /** False = plugin not activated, or activation row's `enabled`\n * flag is off. Widgets should render a \"not accepting bookings\"\n * state, not throw. */\n enabled: boolean\n /** Source-of-truth IANA timezone for the workspace's business\n * hours. Visitors see slots in their local TZ — use this for\n * the \"(workspace local: HH:MM)\" subtext. */\n timezone: string\n slotDurationMinutes: number\n leadTimeHours: number\n bookingWindowDays: number\n policyText: string | null\n slots: AppointmentSlot[]\n /** UTC ISO 8601. Useful for the widget's date range label. */\n windowStart: string\n windowEnd: string\n}\n\nexport type CreateAppointmentRequestInput = {\n visitorName: string\n visitorEmail: string\n visitorPhone?: string\n visitorMessage?: string\n /** UTC ISO 8601 of the requested slot start. Server re-validates\n * against business hours + busy ranges before accepting. */\n requestedAt: string\n /** Optional cookie-derived session id from the consumer site. */\n visitorSessionId?: string\n}\n\nexport type CreatedAppointmentRequest = {\n id: string\n createdAt: string\n requestedAt: string\n durationMinutes: number\n status: 'PENDING'\n /** Visitor's self-cancel token. Embed it in confirmation\n * emails / on-page UI so the visitor can cancel without an\n * account. One-time use; revoked once any party acts. */\n cancellationToken: string | null\n}\n\ntype AppointmentsApi = {\n /**\n * Available slots for the workspace's booking window.\n * `from` / `to` are optional clamps inside the workspace's\n * configured window — the server ignores ranges outside.\n */\n getAvailability: (opts?: {\n from?: Date | string\n to?: Date | string\n }) => Promise<AppointmentAvailability>\n /**\n * Submit a visitor's appointment request. Server-side validates\n * the slot is still bookable; if it isn't, throws\n * `BrandfineApiError` with status 404 / 409.\n *\n * The visitor's browser does not have any other appointment\n * actions in v1 — post-submission status changes (approve /\n * decline / reschedule) happen via email, driven by the\n * customer in the CMS.\n */\n createRequest: (\n input: CreateAppointmentRequestInput,\n ) => Promise<CreatedAppointmentRequest>\n}\n\nexport type AnalyticsConfig =\n | {\n enabled: false\n /** GA4 Measurement ID — present when the workspace's Google\n * Analytics property was provisioned through Brandfine AND\n * the customer opted into tag injection. `install()` loads\n * gtag for it. Note gtag sets cookies: consent banners are\n * your site's responsibility. */\n gaMeasurementId?: string\n }\n | {\n enabled: true\n websiteId: string\n scriptUrl: string\n gaMeasurementId?: string\n }\n\nexport type AnalyticsOverviewRange = '24h' | '7d' | '30d' | '90d'\n\n/**\n * Composed traffic report for the workspace — summary KPIs +\n * bucketed chart data + top pages in one payload. Mirrors\n * `GET /external/analytics/overview` (see the API's\n * `ExternalAnalyticsOverview` type); additive changes only.\n *\n * Three shapes to handle:\n * - `{ enabled: false }` — analytics never enabled for the\n * workspace. Show an enable CTA.\n * - `{ enabled: true, verified: false }` — tracker provisioned\n * but no pageview recorded yet. Show \"waiting for first visit\".\n * - full payload — render the dashboard.\n */\nexport type AnalyticsOverview =\n | { enabled: false }\n | { enabled: true; verified: false }\n | {\n enabled: true\n verified: true\n range: AnalyticsOverviewRange\n summary: {\n visitors: number\n /** Fractional change vs the prior window (0.12 = +12%). */\n visitorsChange: number\n pageviews: number\n pageviewsChange: number\n visits: number\n visitsChange: number\n /** 0..1 fraction. */\n bounceRate: number\n bounceRateChange: number\n avgVisitSeconds: number\n avgVisitSecondsChange: number\n /** Visitors active in the last ~5 minutes. */\n activeNow: number\n }\n /** Bucketed chart data, oldest → newest. Hourly buckets for\n * `24h`, daily otherwise. `t` is an ISO-8601 bucket start. */\n timeseries: Array<{ t: string; visitors: number; pageviews: number }>\n /** Top 10 paths by views in the window. */\n topPages: Array<{ path: string; views: number; visitors: number }>\n /** Top 10 referrer sources by visitors. Empty-string source\n * means direct traffic. */\n sources: Array<{ source: string; visitors: number }>\n /** Top 10 visitor countries (ISO 3166-1 alpha-2 codes —\n * map to display names on your side, e.g. via\n * `Intl.DisplayNames`). */\n countries: Array<{ country: string; visitors: number }>\n /** Visitors by device class (`desktop` / `mobile` /\n * `tablet` / …). */\n devices: Array<{ device: string; visitors: number }>\n }\n\nexport type AnalyticsInstallResult =\n | { installed: false; reason: 'disabled' | 'ssr' | 'already-installed' }\n | { installed: true; websiteId: string }\n\nexport type InstallOptions = {\n /**\n * Pre-known config. When provided, `install()` skips the round-\n * trip to `/external/analytics-config` and injects the script\n * immediately. Use this when you've baked the values into your\n * build (env vars, CMS-side config dump, etc.) — typical for\n * static sites where the analytics state is decided at deploy\n * time, not per page load.\n *\n * Trade-off vs the default fetch path: if you disable analytics\n * in Brandfine, the tracker keeps loading until your next\n * deploy. That's usually the right trade for static sites\n * (which redeploy on every content change anyway) and the wrong\n * trade for dynamic sites where the api round-trip is cheap\n * relative to the rest of the page.\n *\n * Pass `{ enabled: false }` to force a no-op without touching\n * the api (e.g. to disable analytics for one environment without\n * changing Brandfine's state).\n */\n config?: AnalyticsConfig\n}\n\ntype AnalyticsApi = {\n /**\n * Injects the Brandfine analytics tracker into `document.head`\n * once. Safe to call on every page load — idempotent via a\n * marker attribute on the injected script tag.\n *\n * Two paths:\n * - `install()` — fetches the config from Brandfine, then\n * injects. Reflects enable/disable state on next page load.\n * - `install({ config })` — uses caller-provided config, skips\n * the fetch. Faster, no round-trip; ignores Brandfine state\n * changes until the consumer's next deploy.\n *\n * Returns details about what happened:\n * - `{ installed: true, websiteId }` — script was just injected.\n * - `{ installed: false, reason: 'disabled' }` — config says\n * analytics is off; no-op.\n * - `{ installed: false, reason: 'ssr' }` — no `document` in\n * scope (server-side). Call again on the client.\n * - `{ installed: false, reason: 'already-installed' }` — a\n * prior call (or another tab in the same SPA) already injected.\n *\n * Throws `BrandfineApiError` on non-2xx responses other than the\n * disabled case (which is a valid `{ enabled: false }` body).\n */\n install: (opts?: InstallOptions) => Promise<AnalyticsInstallResult>\n\n /** Lower-level helper — fetches the raw config without touching\n * the DOM. Useful when you want to inject the script yourself\n * (e.g. via a framework's <Script> component for nonce/csp). */\n getConfig: () => Promise<AnalyticsConfig>\n\n /**\n * Traffic report for the workspace — summary KPIs, bucketed\n * timeseries for charting, and top pages, in one round-trip.\n * This is a server-to-server read (it returns your site's\n * traffic data); call it from your backend or build step, not\n * from visitor-facing browser code.\n *\n * @param opts.range Window preset. Defaults to `'7d'`.\n */\n overview: (opts?: {\n range?: AnalyticsOverviewRange\n }) => Promise<AnalyticsOverview>\n}\n\nexport type LiveChatBootstrap =\n | { enabled: false }\n | {\n enabled: true\n /** The workspace's SCOPED publishable key — safe to bake into\n * public HTML; it can only start chat conversations. */\n publishableKey: string\n greeting: string | null\n offlineMessage: string | null\n theme: Record<string, string> | null\n /** Widget bundle path relative to the API base URL. */\n scriptPath: string\n }\n\nexport type LiveChatInstallResult =\n | { installed: false; reason: 'disabled' | 'ssr' | 'already-installed' }\n | { installed: true }\n\nexport type LiveChatInstallOptions = {\n /**\n * Pre-known bootstrap. Same build-time/runtime trade-off as the\n * analytics `install()`: pass the value your server half fetched\n * (static-export sites bake it at build time), or omit — NOT\n * recommended in browsers, because fetching here would require\n * the broad key client-side. In practice: always pass `config`\n * from your server half.\n */\n config: LiveChatBootstrap\n}\n\ntype LiveChatApi = {\n /**\n * Fetches the Live Chat bootstrap (publishable key + display\n * config) with the broad workspace key. SERVER-SIDE ONLY — call\n * from your backend, RSC, or build step, never from browser code.\n */\n getConfig: () => Promise<LiveChatBootstrap>\n\n /**\n * Injects the chat widget into the page: appends a host\n * `<div data-bf-live-chat>` carrying the publishable key and the\n * widget `<script>` tag. Idempotent via a marker attribute.\n * Browser-side half of the pair — receives the bootstrap your\n * server half fetched with `getConfig()`.\n *\n * Returns what happened, mirroring `analytics.install()`:\n * - `{ installed: true }` — widget just injected.\n * - `{ installed: false, reason: 'disabled' }` — chat is off.\n * - `{ installed: false, reason: 'ssr' }` — no `document`.\n * - `{ installed: false, reason: 'already-installed' }`.\n */\n install: (opts: LiveChatInstallOptions) => Promise<LiveChatInstallResult>\n}\n\n/** Attribute we stamp on the injected <script> so `install()` is\n * idempotent across re-renders and SPA route changes. */\nconst INSTALLED_MARKER = 'data-brandfine-analytics'\n\n/** Same idempotency contract for the live-chat widget injection. */\nconst LIVE_CHAT_MARKER = 'data-brandfine-live-chat'\n\n/** Marker for the injected Google tag — same idempotency contract. */\nconst GTAG_MARKER = 'data-brandfine-gtag'\n\n/**\n * Inject the Google tag (gtag.js) for an auto-provisioned GA4\n * property. No-ops when ANY gtag script is already on the page —\n * a site that hand-installed Google Analytics must not get a\n * second config (double-counted sessions are worse than a missing\n * tag). Safe to call repeatedly; the marker makes it idempotent.\n */\nfunction injectGoogleTag(measurementId: string): void {\n if (typeof document === 'undefined') return\n const existing = document.querySelector(\n `script[src*=\"googletagmanager.com/gtag/js\"], script[${GTAG_MARKER}]`,\n )\n if (existing) return\n\n const loader = document.createElement('script')\n loader.async = true\n loader.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`\n loader.setAttribute(GTAG_MARKER, measurementId)\n document.head.appendChild(loader)\n\n const w = window as unknown as { dataLayer?: unknown[] }\n w.dataLayer = w.dataLayer ?? []\n // gtag() must push `arguments` (an Arguments object), not a\n // plain array — GA's snippet relies on it.\n function gtag(..._args: unknown[]) {\n // eslint-disable-next-line prefer-rest-params\n w.dataLayer!.push(arguments)\n }\n gtag('js', new Date())\n gtag('config', measurementId)\n}\n\nconst DEFAULT_USER_AGENT = '@brandfine/client'\n\nexport function createBrandfineClient(\n config: BrandfineClientConfig,\n): BrandfineClient {\n if (!config.baseUrl)\n throw new Error('createBrandfineClient: `baseUrl` is required')\n if (!config.apiKey)\n throw new Error('createBrandfineClient: `apiKey` is required')\n\n const baseUrl = config.baseUrl.replace(/\\/$/, '')\n const apiKey = config.apiKey\n // Resolve fetch lazily so consumers in environments without a\n // global fetch can polyfill before constructing the client.\n const fetchImpl: typeof fetch = config.fetch ?? globalThis.fetch\n const userAgent = config.userAgent ?? DEFAULT_USER_AGENT\n\n async function get<T>(path: string, opts: RequestOptions = {}): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'GET',\n headers: {\n 'X-Api-Key': apiKey,\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n signal: opts.signal,\n })\n if (res.status === 404 && opts.nullable404) {\n // Drain the body so the underlying socket can be reused —\n // fetch implementations that don't auto-drain (older Node)\n // can leak otherwise.\n await res.text().catch(() => '')\n return null as T\n }\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as T\n }\n\n const posts: PostsApi = {\n async list<TConfig = unknown>(opts: ListPostsOptions = {}) {\n const out: BrandfinePost<TConfig>[] = []\n let page = 1\n const typeQuery = opts.type ? `&type=${encodeURIComponent(opts.type)}` : ''\n const localeQuery = opts.locale\n ? `&locale=${encodeURIComponent(opts.locale)}`\n : ''\n // Default pagination at the cms's 50-per-page cap. `forceLimit`\n // opts past it for content types that would otherwise need\n // many round-trips.\n const sizeQuery = opts.forceLimit\n ? `&force_limit=${opts.forceLimit}`\n : '&limit=50'\n // Pathological safety brake — 200 pages × 50 = 10k posts. If\n // a workspace ever needs more, callers should hit the API\n // directly with their own pagination logic.\n const MAX_PAGES = 200\n while (page <= MAX_PAGES) {\n const data = await get<BrandfinePostListResponse<TConfig>>(\n `/external/posts?include=content${sizeQuery}&page=${page}${typeQuery}${localeQuery}`,\n )\n out.push(...data.items)\n if (!data.pageInfo.hasNext) break\n page += 1\n }\n return out\n },\n async getBySlug<TConfig = unknown>(slug: string) {\n return get<BrandfinePost<TConfig> | null>(\n `/external/posts/${encodeURIComponent(slug)}`,\n { nullable404: true },\n )\n },\n }\n\n const categories: CategoriesApi = {\n async list(opts: ListCategoriesOptions = {}) {\n const qs = opts.locale ? `?locale=${encodeURIComponent(opts.locale)}` : ''\n const data = await get<{ items: BrandfineCategory[] }>(\n `/external/categories${qs}`,\n )\n return data.items\n },\n }\n\n const workspace: WorkspaceApi = {\n get<\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() {\n return get<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>(\n '/external/workspace',\n )\n },\n }\n\n const navigations: NavigationsApi = {\n get<TConfig = unknown>(key: string) {\n return get<BrandfineNavigation<TConfig> | null>(\n `/external/navigations/${encodeURIComponent(key)}`,\n { nullable404: true },\n )\n },\n }\n\n const analytics: AnalyticsApi = {\n getConfig() {\n return get<AnalyticsConfig>('/external/analytics-config')\n },\n overview(opts = {}) {\n const range = opts.range ?? '7d'\n return get<AnalyticsOverview>(\n `/external/analytics/overview?range=${encodeURIComponent(range)}`,\n )\n },\n async install(opts: InstallOptions = {}) {\n // SSR safety: nothing to inject without a DOM. Consumers\n // call this from useEffect / onMount, but defensive anyway\n // (some frameworks still execute the file body on the server).\n if (typeof document === 'undefined') {\n return { installed: false, reason: 'ssr' as const }\n }\n\n // Use caller-provided config if present (build-time path),\n // otherwise fetch (runtime path).\n const cfg = opts.config ?? (await analytics.getConfig())\n\n // Google tag rides alongside the built-in tracker — injected\n // even when Brandfine analytics itself is off, because the\n // opt-in lives on the GA integration, not on the tracker.\n if (cfg.gaMeasurementId) {\n injectGoogleTag(cfg.gaMeasurementId)\n }\n\n if (!cfg.enabled) {\n return { installed: false, reason: 'disabled' as const }\n }\n\n // Idempotency: a prior call (StrictMode double-invoke, SPA\n // re-mount, second instance with the same workspace) may\n // have already injected. The marker attribute is the source\n // of truth — checking by script src would also miss the case\n // where two workspaces share the same scriptUrl.\n const existing = document.querySelector<HTMLScriptElement>(\n `script[${INSTALLED_MARKER}=\"${cfg.websiteId}\"]`,\n )\n if (existing) {\n return { installed: false, reason: 'already-installed' as const }\n }\n\n const script = document.createElement('script')\n script.defer = true\n script.src = cfg.scriptUrl\n script.setAttribute('data-website-id', cfg.websiteId)\n // The marker doubles as a sentinel + a debug aid (you can\n // grep the DOM for `data-brandfine-analytics` to confirm\n // an install).\n script.setAttribute(INSTALLED_MARKER, cfg.websiteId)\n document.head.appendChild(script)\n return { installed: true, websiteId: cfg.websiteId }\n },\n }\n\n const liveChat: LiveChatApi = {\n getConfig() {\n return get<LiveChatBootstrap>('/external/live-chat/bootstrap')\n },\n async install(opts: LiveChatInstallOptions) {\n if (typeof document === 'undefined') {\n return { installed: false, reason: 'ssr' as const }\n }\n\n const cfg = opts.config\n if (!cfg.enabled) {\n return { installed: false, reason: 'disabled' as const }\n }\n\n const existing = document.querySelector(`[${LIVE_CHAT_MARKER}]`)\n if (existing) {\n return { installed: false, reason: 'already-installed' as const }\n }\n\n // Host div the widget script mounts into. Theme vars ride as\n // inline custom properties — they inherit through the widget's\n // shadow boundary.\n const host = document.createElement('div')\n host.setAttribute('data-bf-live-chat', '')\n host.setAttribute('data-publishable-key', cfg.publishableKey)\n host.setAttribute('data-base-url', baseUrl)\n host.setAttribute(LIVE_CHAT_MARKER, '')\n if (cfg.theme) {\n for (const [key, value] of Object.entries(cfg.theme)) {\n if (key.startsWith('--bf-chat-')) {\n host.style.setProperty(key, value)\n }\n }\n }\n document.body.appendChild(host)\n\n const script = document.createElement('script')\n script.defer = true\n script.src = `${baseUrl}${cfg.scriptPath}`\n script.setAttribute(LIVE_CHAT_MARKER, 'script')\n document.head.appendChild(script)\n\n return { installed: true }\n },\n }\n\n const submissions: SubmissionsApi = {\n async create(input: CreateSubmissionInput) {\n const url = `${baseUrl}/external/submissions`\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n body: JSON.stringify(input),\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as Submission\n },\n }\n\n /**\n * Shared POST helper for the appointments namespace. The main\n * `get()` helper handles GETs; submissions has its own inline\n * POST because it predates this refactor. New plugin namespaces\n * (appointments first, others to follow) share this one so the\n * error-handling shape stays consistent.\n */\n async function post<T>(\n path: string,\n body: unknown,\n opts: RequestOptions = {},\n ): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n body: JSON.stringify(body),\n signal: opts.signal,\n })\n if (!res.ok) {\n const errBody = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body: errBody,\n url,\n })\n }\n if (res.status === 204) return undefined as T\n return (await res.json()) as T\n }\n\n const appointments: AppointmentsApi = {\n getAvailability(opts = {}) {\n const qs: string[] = []\n if (opts.from) qs.push(`from=${encodeURIComponent(toIso(opts.from))}`)\n if (opts.to) qs.push(`to=${encodeURIComponent(toIso(opts.to))}`)\n const suffix = qs.length ? `?${qs.join('&')}` : ''\n return get<AppointmentAvailability>(\n `/external/appointments/availability${suffix}`,\n )\n },\n createRequest(input) {\n return post<CreatedAppointmentRequest>(\n '/external/appointments/requests',\n input,\n )\n },\n }\n\n return {\n get,\n posts,\n categories,\n workspace,\n navigations,\n analytics,\n submissions,\n appointments,\n liveChat,\n }\n}\n\n/** Accepts a Date or an already-ISO string and returns ISO. Saves\n * every caller from `.toISOString()`-ing manually. */\nfunction toIso(d: Date | string): string {\n return typeof d === 'string' ? d : d.toISOString()\n}\n","/**\n * @brandfine/client — root entry.\n *\n * The full SDK surface is exposed here for \"import everything from\n * one place\" usage. Tree-shaking + `sideEffects: false` mean\n * consumers don't pay a bundle cost for what they don't import.\n *\n * Heavier or framework-coupled pieces still live under subpath\n * exports (`@brandfine/client/cache`, `/resolvers`, `/webhook`) so\n * consumers with poor tree-shaking — or who only need one slice —\n * can scope their imports.\n */\n\nexport const SDK_VERSION = '0.0.0' as const\n\nexport {\n BrandfineApiError,\n createBrandfineClient,\n type AnalyticsConfig,\n type AnalyticsInstallResult,\n type AnalyticsOverview,\n type AnalyticsOverviewRange,\n type BrandfineClient,\n type BrandfineClientConfig,\n type CreateSubmissionInput,\n type InstallOptions,\n type Submission,\n} from './client'\n\nexport {\n createCache,\n createKeyedCache,\n type Cache,\n type CacheOptions,\n type KeyedCache,\n type KeyedCacheOptions,\n} from './cache/index'\n\nexport {\n isLocale,\n localizePath,\n pickLocale,\n resolveNavigation,\n stripLocalePrefix,\n type HydratedNav,\n type HydratedNavItem,\n type LocaleOptions,\n type ResolveNavigationOptions,\n} from './resolvers/index'\n\nexport {\n createBrandfineWebhookHandler,\n parseWebhookPayload,\n verifyWebhookSecret,\n type BrandfineWebhookEvent,\n type BrandfineWebhookHandlerOptions,\n type BrandfineWebhookPayload,\n} from './webhook/index'\n\nexport type {\n BrandfineCategory,\n BrandfineNavItem,\n BrandfineNavItemType,\n BrandfineNavPost,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfinePostTranslation,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n"]}
package/dist/index.d.cts CHANGED
@@ -69,6 +69,7 @@ type BrandfineClient = {
69
69
  analytics: AnalyticsApi;
70
70
  submissions: SubmissionsApi;
71
71
  appointments: AppointmentsApi;
72
+ liveChat: LiveChatApi;
72
73
  };
73
74
  type PostsApi = {
74
75
  /** Paginated list of published posts. Handles the cms's
@@ -196,10 +197,89 @@ type AppointmentsApi = {
196
197
  };
197
198
  type AnalyticsConfig = {
198
199
  enabled: false;
200
+ /** GA4 Measurement ID — present when the workspace's Google
201
+ * Analytics property was provisioned through Brandfine AND
202
+ * the customer opted into tag injection. `install()` loads
203
+ * gtag for it. Note gtag sets cookies: consent banners are
204
+ * your site's responsibility. */
205
+ gaMeasurementId?: string;
199
206
  } | {
200
207
  enabled: true;
201
208
  websiteId: string;
202
209
  scriptUrl: string;
210
+ gaMeasurementId?: string;
211
+ };
212
+ type AnalyticsOverviewRange = '24h' | '7d' | '30d' | '90d';
213
+ /**
214
+ * Composed traffic report for the workspace — summary KPIs +
215
+ * bucketed chart data + top pages in one payload. Mirrors
216
+ * `GET /external/analytics/overview` (see the API's
217
+ * `ExternalAnalyticsOverview` type); additive changes only.
218
+ *
219
+ * Three shapes to handle:
220
+ * - `{ enabled: false }` — analytics never enabled for the
221
+ * workspace. Show an enable CTA.
222
+ * - `{ enabled: true, verified: false }` — tracker provisioned
223
+ * but no pageview recorded yet. Show "waiting for first visit".
224
+ * - full payload — render the dashboard.
225
+ */
226
+ type AnalyticsOverview = {
227
+ enabled: false;
228
+ } | {
229
+ enabled: true;
230
+ verified: false;
231
+ } | {
232
+ enabled: true;
233
+ verified: true;
234
+ range: AnalyticsOverviewRange;
235
+ summary: {
236
+ visitors: number;
237
+ /** Fractional change vs the prior window (0.12 = +12%). */
238
+ visitorsChange: number;
239
+ pageviews: number;
240
+ pageviewsChange: number;
241
+ visits: number;
242
+ visitsChange: number;
243
+ /** 0..1 fraction. */
244
+ bounceRate: number;
245
+ bounceRateChange: number;
246
+ avgVisitSeconds: number;
247
+ avgVisitSecondsChange: number;
248
+ /** Visitors active in the last ~5 minutes. */
249
+ activeNow: number;
250
+ };
251
+ /** Bucketed chart data, oldest → newest. Hourly buckets for
252
+ * `24h`, daily otherwise. `t` is an ISO-8601 bucket start. */
253
+ timeseries: Array<{
254
+ t: string;
255
+ visitors: number;
256
+ pageviews: number;
257
+ }>;
258
+ /** Top 10 paths by views in the window. */
259
+ topPages: Array<{
260
+ path: string;
261
+ views: number;
262
+ visitors: number;
263
+ }>;
264
+ /** Top 10 referrer sources by visitors. Empty-string source
265
+ * means direct traffic. */
266
+ sources: Array<{
267
+ source: string;
268
+ visitors: number;
269
+ }>;
270
+ /** Top 10 visitor countries (ISO 3166-1 alpha-2 codes —
271
+ * map to display names on your side, e.g. via
272
+ * `Intl.DisplayNames`). */
273
+ countries: Array<{
274
+ country: string;
275
+ visitors: number;
276
+ }>;
277
+ /** Visitors by device class (`desktop` / `mobile` /
278
+ * `tablet` / …). */
279
+ devices: Array<{
280
+ device: string;
281
+ visitors: number;
282
+ }>;
203
283
  };
204
284
  type AnalyticsInstallResult = {
205
285
  installed: false;
@@ -260,6 +340,70 @@ type AnalyticsApi = {
260
340
  * the DOM. Useful when you want to inject the script yourself
261
341
  * (e.g. via a framework's <Script> component for nonce/csp). */
262
342
  getConfig: () => Promise<AnalyticsConfig>;
343
+ /**
344
+ * Traffic report for the workspace — summary KPIs, bucketed
345
+ * timeseries for charting, and top pages, in one round-trip.
346
+ * This is a server-to-server read (it returns your site's
347
+ * traffic data); call it from your backend or build step, not
348
+ * from visitor-facing browser code.
349
+ *
350
+ * @param opts.range Window preset. Defaults to `'7d'`.
351
+ */
352
+ overview: (opts?: {
353
+ range?: AnalyticsOverviewRange;
354
+ }) => Promise<AnalyticsOverview>;
355
+ };
356
+ type LiveChatBootstrap = {
357
+ enabled: false;
358
+ } | {
359
+ enabled: true;
360
+ /** The workspace's SCOPED publishable key — safe to bake into
361
+ * public HTML; it can only start chat conversations. */
362
+ publishableKey: string;
363
+ greeting: string | null;
364
+ offlineMessage: string | null;
365
+ theme: Record<string, string> | null;
366
+ /** Widget bundle path relative to the API base URL. */
367
+ scriptPath: string;
368
+ };
369
+ type LiveChatInstallResult = {
370
+ installed: false;
371
+ reason: 'disabled' | 'ssr' | 'already-installed';
372
+ } | {
373
+ installed: true;
374
+ };
375
+ type LiveChatInstallOptions = {
376
+ /**
377
+ * Pre-known bootstrap. Same build-time/runtime trade-off as the
378
+ * analytics `install()`: pass the value your server half fetched
379
+ * (static-export sites bake it at build time), or omit — NOT
380
+ * recommended in browsers, because fetching here would require
381
+ * the broad key client-side. In practice: always pass `config`
382
+ * from your server half.
383
+ */
384
+ config: LiveChatBootstrap;
385
+ };
386
+ type LiveChatApi = {
387
+ /**
388
+ * Fetches the Live Chat bootstrap (publishable key + display
389
+ * config) with the broad workspace key. SERVER-SIDE ONLY — call
390
+ * from your backend, RSC, or build step, never from browser code.
391
+ */
392
+ getConfig: () => Promise<LiveChatBootstrap>;
393
+ /**
394
+ * Injects the chat widget into the page: appends a host
395
+ * `<div data-bf-live-chat>` carrying the publishable key and the
396
+ * widget `<script>` tag. Idempotent via a marker attribute.
397
+ * Browser-side half of the pair — receives the bootstrap your
398
+ * server half fetched with `getConfig()`.
399
+ *
400
+ * Returns what happened, mirroring `analytics.install()`:
401
+ * - `{ installed: true }` — widget just injected.
402
+ * - `{ installed: false, reason: 'disabled' }` — chat is off.
403
+ * - `{ installed: false, reason: 'ssr' }` — no `document`.
404
+ * - `{ installed: false, reason: 'already-installed' }`.
405
+ */
406
+ install: (opts: LiveChatInstallOptions) => Promise<LiveChatInstallResult>;
263
407
  };
264
408
  declare function createBrandfineClient(config: BrandfineClientConfig): BrandfineClient;
265
409
 
@@ -277,4 +421,4 @@ declare function createBrandfineClient(config: BrandfineClientConfig): Brandfine
277
421
  */
278
422
  declare const SDK_VERSION: "0.0.0";
279
423
 
280
- export { type AnalyticsConfig, type AnalyticsInstallResult, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type CreateSubmissionInput, type InstallOptions, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, type Submission, createBrandfineClient };
424
+ export { type AnalyticsConfig, type AnalyticsInstallResult, type AnalyticsOverview, type AnalyticsOverviewRange, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type CreateSubmissionInput, type InstallOptions, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, type Submission, createBrandfineClient };
package/dist/index.d.ts CHANGED
@@ -69,6 +69,7 @@ type BrandfineClient = {
69
69
  analytics: AnalyticsApi;
70
70
  submissions: SubmissionsApi;
71
71
  appointments: AppointmentsApi;
72
+ liveChat: LiveChatApi;
72
73
  };
73
74
  type PostsApi = {
74
75
  /** Paginated list of published posts. Handles the cms's
@@ -196,10 +197,89 @@ type AppointmentsApi = {
196
197
  };
197
198
  type AnalyticsConfig = {
198
199
  enabled: false;
200
+ /** GA4 Measurement ID — present when the workspace's Google
201
+ * Analytics property was provisioned through Brandfine AND
202
+ * the customer opted into tag injection. `install()` loads
203
+ * gtag for it. Note gtag sets cookies: consent banners are
204
+ * your site's responsibility. */
205
+ gaMeasurementId?: string;
199
206
  } | {
200
207
  enabled: true;
201
208
  websiteId: string;
202
209
  scriptUrl: string;
210
+ gaMeasurementId?: string;
211
+ };
212
+ type AnalyticsOverviewRange = '24h' | '7d' | '30d' | '90d';
213
+ /**
214
+ * Composed traffic report for the workspace — summary KPIs +
215
+ * bucketed chart data + top pages in one payload. Mirrors
216
+ * `GET /external/analytics/overview` (see the API's
217
+ * `ExternalAnalyticsOverview` type); additive changes only.
218
+ *
219
+ * Three shapes to handle:
220
+ * - `{ enabled: false }` — analytics never enabled for the
221
+ * workspace. Show an enable CTA.
222
+ * - `{ enabled: true, verified: false }` — tracker provisioned
223
+ * but no pageview recorded yet. Show "waiting for first visit".
224
+ * - full payload — render the dashboard.
225
+ */
226
+ type AnalyticsOverview = {
227
+ enabled: false;
228
+ } | {
229
+ enabled: true;
230
+ verified: false;
231
+ } | {
232
+ enabled: true;
233
+ verified: true;
234
+ range: AnalyticsOverviewRange;
235
+ summary: {
236
+ visitors: number;
237
+ /** Fractional change vs the prior window (0.12 = +12%). */
238
+ visitorsChange: number;
239
+ pageviews: number;
240
+ pageviewsChange: number;
241
+ visits: number;
242
+ visitsChange: number;
243
+ /** 0..1 fraction. */
244
+ bounceRate: number;
245
+ bounceRateChange: number;
246
+ avgVisitSeconds: number;
247
+ avgVisitSecondsChange: number;
248
+ /** Visitors active in the last ~5 minutes. */
249
+ activeNow: number;
250
+ };
251
+ /** Bucketed chart data, oldest → newest. Hourly buckets for
252
+ * `24h`, daily otherwise. `t` is an ISO-8601 bucket start. */
253
+ timeseries: Array<{
254
+ t: string;
255
+ visitors: number;
256
+ pageviews: number;
257
+ }>;
258
+ /** Top 10 paths by views in the window. */
259
+ topPages: Array<{
260
+ path: string;
261
+ views: number;
262
+ visitors: number;
263
+ }>;
264
+ /** Top 10 referrer sources by visitors. Empty-string source
265
+ * means direct traffic. */
266
+ sources: Array<{
267
+ source: string;
268
+ visitors: number;
269
+ }>;
270
+ /** Top 10 visitor countries (ISO 3166-1 alpha-2 codes —
271
+ * map to display names on your side, e.g. via
272
+ * `Intl.DisplayNames`). */
273
+ countries: Array<{
274
+ country: string;
275
+ visitors: number;
276
+ }>;
277
+ /** Visitors by device class (`desktop` / `mobile` /
278
+ * `tablet` / …). */
279
+ devices: Array<{
280
+ device: string;
281
+ visitors: number;
282
+ }>;
203
283
  };
204
284
  type AnalyticsInstallResult = {
205
285
  installed: false;
@@ -260,6 +340,70 @@ type AnalyticsApi = {
260
340
  * the DOM. Useful when you want to inject the script yourself
261
341
  * (e.g. via a framework's <Script> component for nonce/csp). */
262
342
  getConfig: () => Promise<AnalyticsConfig>;
343
+ /**
344
+ * Traffic report for the workspace — summary KPIs, bucketed
345
+ * timeseries for charting, and top pages, in one round-trip.
346
+ * This is a server-to-server read (it returns your site's
347
+ * traffic data); call it from your backend or build step, not
348
+ * from visitor-facing browser code.
349
+ *
350
+ * @param opts.range Window preset. Defaults to `'7d'`.
351
+ */
352
+ overview: (opts?: {
353
+ range?: AnalyticsOverviewRange;
354
+ }) => Promise<AnalyticsOverview>;
355
+ };
356
+ type LiveChatBootstrap = {
357
+ enabled: false;
358
+ } | {
359
+ enabled: true;
360
+ /** The workspace's SCOPED publishable key — safe to bake into
361
+ * public HTML; it can only start chat conversations. */
362
+ publishableKey: string;
363
+ greeting: string | null;
364
+ offlineMessage: string | null;
365
+ theme: Record<string, string> | null;
366
+ /** Widget bundle path relative to the API base URL. */
367
+ scriptPath: string;
368
+ };
369
+ type LiveChatInstallResult = {
370
+ installed: false;
371
+ reason: 'disabled' | 'ssr' | 'already-installed';
372
+ } | {
373
+ installed: true;
374
+ };
375
+ type LiveChatInstallOptions = {
376
+ /**
377
+ * Pre-known bootstrap. Same build-time/runtime trade-off as the
378
+ * analytics `install()`: pass the value your server half fetched
379
+ * (static-export sites bake it at build time), or omit — NOT
380
+ * recommended in browsers, because fetching here would require
381
+ * the broad key client-side. In practice: always pass `config`
382
+ * from your server half.
383
+ */
384
+ config: LiveChatBootstrap;
385
+ };
386
+ type LiveChatApi = {
387
+ /**
388
+ * Fetches the Live Chat bootstrap (publishable key + display
389
+ * config) with the broad workspace key. SERVER-SIDE ONLY — call
390
+ * from your backend, RSC, or build step, never from browser code.
391
+ */
392
+ getConfig: () => Promise<LiveChatBootstrap>;
393
+ /**
394
+ * Injects the chat widget into the page: appends a host
395
+ * `<div data-bf-live-chat>` carrying the publishable key and the
396
+ * widget `<script>` tag. Idempotent via a marker attribute.
397
+ * Browser-side half of the pair — receives the bootstrap your
398
+ * server half fetched with `getConfig()`.
399
+ *
400
+ * Returns what happened, mirroring `analytics.install()`:
401
+ * - `{ installed: true }` — widget just injected.
402
+ * - `{ installed: false, reason: 'disabled' }` — chat is off.
403
+ * - `{ installed: false, reason: 'ssr' }` — no `document`.
404
+ * - `{ installed: false, reason: 'already-installed' }`.
405
+ */
406
+ install: (opts: LiveChatInstallOptions) => Promise<LiveChatInstallResult>;
263
407
  };
264
408
  declare function createBrandfineClient(config: BrandfineClientConfig): BrandfineClient;
265
409
 
@@ -277,4 +421,4 @@ declare function createBrandfineClient(config: BrandfineClientConfig): Brandfine
277
421
  */
278
422
  declare const SDK_VERSION: "0.0.0";
279
423
 
280
- export { type AnalyticsConfig, type AnalyticsInstallResult, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type CreateSubmissionInput, type InstallOptions, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, type Submission, createBrandfineClient };
424
+ export { type AnalyticsConfig, type AnalyticsInstallResult, type AnalyticsOverview, type AnalyticsOverviewRange, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type CreateSubmissionInput, type InstallOptions, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, type Submission, createBrandfineClient };
package/dist/index.js CHANGED
@@ -20,6 +20,27 @@ var BrandfineApiError = class extends Error {
20
20
  }
21
21
  };
22
22
  var INSTALLED_MARKER = "data-brandfine-analytics";
23
+ var LIVE_CHAT_MARKER = "data-brandfine-live-chat";
24
+ var GTAG_MARKER = "data-brandfine-gtag";
25
+ function injectGoogleTag(measurementId) {
26
+ if (typeof document === "undefined") return;
27
+ const existing = document.querySelector(
28
+ `script[src*="googletagmanager.com/gtag/js"], script[${GTAG_MARKER}]`
29
+ );
30
+ if (existing) return;
31
+ const loader = document.createElement("script");
32
+ loader.async = true;
33
+ loader.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`;
34
+ loader.setAttribute(GTAG_MARKER, measurementId);
35
+ document.head.appendChild(loader);
36
+ const w = window;
37
+ w.dataLayer = w.dataLayer ?? [];
38
+ function gtag(..._args) {
39
+ w.dataLayer.push(arguments);
40
+ }
41
+ gtag("js", /* @__PURE__ */ new Date());
42
+ gtag("config", measurementId);
43
+ }
23
44
  var DEFAULT_USER_AGENT = "@brandfine/client";
24
45
  function createBrandfineClient(config) {
25
46
  if (!config.baseUrl)
@@ -109,11 +130,20 @@ function createBrandfineClient(config) {
109
130
  getConfig() {
110
131
  return get("/external/analytics-config");
111
132
  },
133
+ overview(opts = {}) {
134
+ const range = opts.range ?? "7d";
135
+ return get(
136
+ `/external/analytics/overview?range=${encodeURIComponent(range)}`
137
+ );
138
+ },
112
139
  async install(opts = {}) {
113
140
  if (typeof document === "undefined") {
114
141
  return { installed: false, reason: "ssr" };
115
142
  }
116
143
  const cfg = opts.config ?? await analytics.getConfig();
144
+ if (cfg.gaMeasurementId) {
145
+ injectGoogleTag(cfg.gaMeasurementId);
146
+ }
117
147
  if (!cfg.enabled) {
118
148
  return { installed: false, reason: "disabled" };
119
149
  }
@@ -132,6 +162,43 @@ function createBrandfineClient(config) {
132
162
  return { installed: true, websiteId: cfg.websiteId };
133
163
  }
134
164
  };
165
+ const liveChat = {
166
+ getConfig() {
167
+ return get("/external/live-chat/bootstrap");
168
+ },
169
+ async install(opts) {
170
+ if (typeof document === "undefined") {
171
+ return { installed: false, reason: "ssr" };
172
+ }
173
+ const cfg = opts.config;
174
+ if (!cfg.enabled) {
175
+ return { installed: false, reason: "disabled" };
176
+ }
177
+ const existing = document.querySelector(`[${LIVE_CHAT_MARKER}]`);
178
+ if (existing) {
179
+ return { installed: false, reason: "already-installed" };
180
+ }
181
+ const host = document.createElement("div");
182
+ host.setAttribute("data-bf-live-chat", "");
183
+ host.setAttribute("data-publishable-key", cfg.publishableKey);
184
+ host.setAttribute("data-base-url", baseUrl);
185
+ host.setAttribute(LIVE_CHAT_MARKER, "");
186
+ if (cfg.theme) {
187
+ for (const [key, value] of Object.entries(cfg.theme)) {
188
+ if (key.startsWith("--bf-chat-")) {
189
+ host.style.setProperty(key, value);
190
+ }
191
+ }
192
+ }
193
+ document.body.appendChild(host);
194
+ const script = document.createElement("script");
195
+ script.defer = true;
196
+ script.src = `${baseUrl}${cfg.scriptPath}`;
197
+ script.setAttribute(LIVE_CHAT_MARKER, "script");
198
+ document.head.appendChild(script);
199
+ return { installed: true };
200
+ }
201
+ };
135
202
  const submissions = {
136
203
  async create(input) {
137
204
  const url = `${baseUrl}/external/submissions`;
@@ -207,7 +274,8 @@ function createBrandfineClient(config) {
207
274
  navigations,
208
275
  analytics,
209
276
  submissions,
210
- appointments
277
+ appointments,
278
+ liveChat
211
279
  };
212
280
  }
213
281
  function toIso(d) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `createBrandfineClient` — the SDK's entry point.\n *\n * Returns a stateless, multi-instance-safe handle scoped to a\n * single `(baseUrl, apiKey)` pair. Pattern follows the Stripe /\n * Algolia / OpenAI SDKs — explicit construction with config,\n * namespaced methods (`bf.posts.list(...)`, `bf.workspace.get()`),\n * no module-level singletons.\n *\n * Why factory not module-level state: multi-tenant consumers\n * sometimes need two clients in the same process (e.g. main site\n * + admin preview). Module-level env reading makes that impossible\n * without monkey-patching.\n */\n\nimport type {\n BrandfineCategory,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n\nexport type BrandfineClientConfig = {\n /** Base URL of the Brandfine API. No trailing slash — the client\n * trims one if you pass it anyway. e.g. `https://api.brandfine.co` */\n baseUrl: string\n /** Workspace-scoped API key. Generated from the cms's Workspace\n * settings; identifies which workspace the client talks to. */\n apiKey: string\n /** Optional fetch override. Useful for tests (inject a stub),\n * for runtimes that need a custom implementation (edge workers\n * with non-standard fetch), or to add cross-cutting concerns\n * like tracing / retries. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch\n /** Optional User-Agent header. Falls back to a generic SDK tag. */\n userAgent?: string\n}\n\n/**\n * Structured error thrown by every request helper on non-2xx\n * responses. Carries the raw body so consumers can log it for\n * debugging without re-fetching.\n */\nexport class BrandfineApiError extends Error {\n override readonly name = 'BrandfineApiError'\n readonly status: number\n readonly statusText: string\n readonly body: string\n readonly url: string\n\n constructor(args: {\n status: number\n statusText: string\n body: string\n url: string\n }) {\n super(\n `[brandfine] ${args.status} ${args.statusText} on ${args.url} — ${args.body.slice(0, 200)}`,\n )\n this.status = args.status\n this.statusText = args.statusText\n this.body = args.body\n this.url = args.url\n }\n}\n\ntype RequestOptions = {\n /** When true and the response is 404, return `null` instead of\n * throwing. Used by endpoints where 404 is a meaningful empty\n * state (navigation by key, single post by slug). */\n nullable404?: boolean\n signal?: AbortSignal\n}\n\nexport type BrandfineClient = {\n /** Low-level GET. Reserved for endpoints we don't have a typed\n * helper for yet. Adds the X-Api-Key header automatically. */\n get: <T>(path: string, opts?: RequestOptions) => Promise<T>\n posts: PostsApi\n categories: CategoriesApi\n workspace: WorkspaceApi\n navigations: NavigationsApi\n analytics: AnalyticsApi\n submissions: SubmissionsApi\n appointments: AppointmentsApi\n}\n\ntype PostsApi = {\n /** Paginated list of published posts. Handles the cms's\n * pagination transparently — caller gets a flat array. */\n list: <TConfig = unknown>(\n opts?: ListPostsOptions,\n ) => Promise<BrandfinePost<TConfig>[]>\n /** Single post by per-locale URL slug, scoped to the active\n * locale on the workspace's content. Returns `null` for 404 so\n * callers can render their own \"not found\" page without try/catch. */\n getBySlug: <TConfig = unknown>(\n slug: string,\n ) => Promise<BrandfinePost<TConfig> | null>\n}\n\ntype CategoriesApi = {\n list: (opts?: ListCategoriesOptions) => Promise<BrandfineCategory[]>\n}\n\ntype WorkspaceApi = {\n get: <\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() => Promise<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>\n}\n\ntype NavigationsApi = {\n /** Navigation by its workspace-scoped `key` (e.g. `'header'`).\n * Returns `null` for 404 so consumers can fall back to a\n * hardcoded default without try/catch. `TConfig` narrows each\n * item's `customConfig` (default `unknown`). */\n get: <TConfig = unknown>(\n key: string,\n ) => Promise<BrandfineNavigation<TConfig> | null>\n}\n\nexport type CreateSubmissionInput = {\n /** Required. Display name of the submitter. */\n name: string\n /** Required. Validated server-side. */\n email: string\n /** Optional. Free-text up to 40 chars. */\n phone?: string\n /** Optional. Free-text up to 200 chars. */\n subject?: string\n /** Required. The message body — up to 10,000 chars. */\n message: string\n /** Optional. Where the submission came from — e.g. a route path\n * like `/contact`, or a marketing campaign label. Up to 500 chars. */\n source?: string\n /** Optional. Free-form JSON metadata the consumer attaches; the\n * cms surfaces it verbatim in the submissions admin view. */\n metadata?: Record<string, unknown>\n}\n\nexport type Submission = {\n id: string\n createdAt: string\n}\n\ntype SubmissionsApi = {\n /**\n * Posts a contact-form submission to `POST /external/submissions`\n * for this workspace. The cms surfaces the submission in the\n * Submissions inbox.\n *\n * Throws `BrandfineApiError` on validation failures (400) or\n * any other non-2xx — caller decides whether to surface that as\n * a user-visible error or a silent retry.\n */\n create: (input: CreateSubmissionInput) => Promise<Submission>\n}\n\n// ----------------------------------------------------------------\n// Appointments plugin SDK — pairs with the Appointments embed\n// widget. Consumers who want full control over the booking UI use\n// these methods directly; consumers who want the drop-in widget\n// use the `<script>` embed (which itself uses these methods under\n// the hood). The same `BrandfineClient` instance powers both.\n// ----------------------------------------------------------------\n\nexport type AppointmentSlot = {\n /** UTC ISO 8601 timestamp of the slot start. */\n start: string\n /** UTC ISO 8601 timestamp of the slot end. */\n end: string\n}\n\nexport type AppointmentAvailability = {\n /** False = plugin not activated, or activation row's `enabled`\n * flag is off. Widgets should render a \"not accepting bookings\"\n * state, not throw. */\n enabled: boolean\n /** Source-of-truth IANA timezone for the workspace's business\n * hours. Visitors see slots in their local TZ — use this for\n * the \"(workspace local: HH:MM)\" subtext. */\n timezone: string\n slotDurationMinutes: number\n leadTimeHours: number\n bookingWindowDays: number\n policyText: string | null\n slots: AppointmentSlot[]\n /** UTC ISO 8601. Useful for the widget's date range label. */\n windowStart: string\n windowEnd: string\n}\n\nexport type CreateAppointmentRequestInput = {\n visitorName: string\n visitorEmail: string\n visitorPhone?: string\n visitorMessage?: string\n /** UTC ISO 8601 of the requested slot start. Server re-validates\n * against business hours + busy ranges before accepting. */\n requestedAt: string\n /** Optional cookie-derived session id from the consumer site. */\n visitorSessionId?: string\n}\n\nexport type CreatedAppointmentRequest = {\n id: string\n createdAt: string\n requestedAt: string\n durationMinutes: number\n status: 'PENDING'\n /** Visitor's self-cancel token. Embed it in confirmation\n * emails / on-page UI so the visitor can cancel without an\n * account. One-time use; revoked once any party acts. */\n cancellationToken: string | null\n}\n\ntype AppointmentsApi = {\n /**\n * Available slots for the workspace's booking window.\n * `from` / `to` are optional clamps inside the workspace's\n * configured window — the server ignores ranges outside.\n */\n getAvailability: (opts?: {\n from?: Date | string\n to?: Date | string\n }) => Promise<AppointmentAvailability>\n /**\n * Submit a visitor's appointment request. Server-side validates\n * the slot is still bookable; if it isn't, throws\n * `BrandfineApiError` with status 404 / 409.\n *\n * The visitor's browser does not have any other appointment\n * actions in v1 — post-submission status changes (approve /\n * decline / reschedule) happen via email, driven by the\n * customer in the CMS.\n */\n createRequest: (\n input: CreateAppointmentRequestInput,\n ) => Promise<CreatedAppointmentRequest>\n}\n\nexport type AnalyticsConfig =\n | { enabled: false }\n | { enabled: true; websiteId: string; scriptUrl: string }\n\nexport type AnalyticsInstallResult =\n | { installed: false; reason: 'disabled' | 'ssr' | 'already-installed' }\n | { installed: true; websiteId: string }\n\nexport type InstallOptions = {\n /**\n * Pre-known config. When provided, `install()` skips the round-\n * trip to `/external/analytics-config` and injects the script\n * immediately. Use this when you've baked the values into your\n * build (env vars, CMS-side config dump, etc.) — typical for\n * static sites where the analytics state is decided at deploy\n * time, not per page load.\n *\n * Trade-off vs the default fetch path: if you disable analytics\n * in Brandfine, the tracker keeps loading until your next\n * deploy. That's usually the right trade for static sites\n * (which redeploy on every content change anyway) and the wrong\n * trade for dynamic sites where the api round-trip is cheap\n * relative to the rest of the page.\n *\n * Pass `{ enabled: false }` to force a no-op without touching\n * the api (e.g. to disable analytics for one environment without\n * changing Brandfine's state).\n */\n config?: AnalyticsConfig\n}\n\ntype AnalyticsApi = {\n /**\n * Injects the Brandfine analytics tracker into `document.head`\n * once. Safe to call on every page load — idempotent via a\n * marker attribute on the injected script tag.\n *\n * Two paths:\n * - `install()` — fetches the config from Brandfine, then\n * injects. Reflects enable/disable state on next page load.\n * - `install({ config })` — uses caller-provided config, skips\n * the fetch. Faster, no round-trip; ignores Brandfine state\n * changes until the consumer's next deploy.\n *\n * Returns details about what happened:\n * - `{ installed: true, websiteId }` — script was just injected.\n * - `{ installed: false, reason: 'disabled' }` — config says\n * analytics is off; no-op.\n * - `{ installed: false, reason: 'ssr' }` — no `document` in\n * scope (server-side). Call again on the client.\n * - `{ installed: false, reason: 'already-installed' }` — a\n * prior call (or another tab in the same SPA) already injected.\n *\n * Throws `BrandfineApiError` on non-2xx responses other than the\n * disabled case (which is a valid `{ enabled: false }` body).\n */\n install: (opts?: InstallOptions) => Promise<AnalyticsInstallResult>\n\n /** Lower-level helper — fetches the raw config without touching\n * the DOM. Useful when you want to inject the script yourself\n * (e.g. via a framework's <Script> component for nonce/csp). */\n getConfig: () => Promise<AnalyticsConfig>\n}\n\n/** Attribute we stamp on the injected <script> so `install()` is\n * idempotent across re-renders and SPA route changes. */\nconst INSTALLED_MARKER = 'data-brandfine-analytics'\n\nconst DEFAULT_USER_AGENT = '@brandfine/client'\n\nexport function createBrandfineClient(\n config: BrandfineClientConfig,\n): BrandfineClient {\n if (!config.baseUrl)\n throw new Error('createBrandfineClient: `baseUrl` is required')\n if (!config.apiKey)\n throw new Error('createBrandfineClient: `apiKey` is required')\n\n const baseUrl = config.baseUrl.replace(/\\/$/, '')\n const apiKey = config.apiKey\n // Resolve fetch lazily so consumers in environments without a\n // global fetch can polyfill before constructing the client.\n const fetchImpl: typeof fetch = config.fetch ?? globalThis.fetch\n const userAgent = config.userAgent ?? DEFAULT_USER_AGENT\n\n async function get<T>(path: string, opts: RequestOptions = {}): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'GET',\n headers: {\n 'X-Api-Key': apiKey,\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n signal: opts.signal,\n })\n if (res.status === 404 && opts.nullable404) {\n // Drain the body so the underlying socket can be reused —\n // fetch implementations that don't auto-drain (older Node)\n // can leak otherwise.\n await res.text().catch(() => '')\n return null as T\n }\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as T\n }\n\n const posts: PostsApi = {\n async list<TConfig = unknown>(opts: ListPostsOptions = {}) {\n const out: BrandfinePost<TConfig>[] = []\n let page = 1\n const typeQuery = opts.type ? `&type=${encodeURIComponent(opts.type)}` : ''\n const localeQuery = opts.locale\n ? `&locale=${encodeURIComponent(opts.locale)}`\n : ''\n // Default pagination at the cms's 50-per-page cap. `forceLimit`\n // opts past it for content types that would otherwise need\n // many round-trips.\n const sizeQuery = opts.forceLimit\n ? `&force_limit=${opts.forceLimit}`\n : '&limit=50'\n // Pathological safety brake — 200 pages × 50 = 10k posts. If\n // a workspace ever needs more, callers should hit the API\n // directly with their own pagination logic.\n const MAX_PAGES = 200\n while (page <= MAX_PAGES) {\n const data = await get<BrandfinePostListResponse<TConfig>>(\n `/external/posts?include=content${sizeQuery}&page=${page}${typeQuery}${localeQuery}`,\n )\n out.push(...data.items)\n if (!data.pageInfo.hasNext) break\n page += 1\n }\n return out\n },\n async getBySlug<TConfig = unknown>(slug: string) {\n return get<BrandfinePost<TConfig> | null>(\n `/external/posts/${encodeURIComponent(slug)}`,\n { nullable404: true },\n )\n },\n }\n\n const categories: CategoriesApi = {\n async list(opts: ListCategoriesOptions = {}) {\n const qs = opts.locale ? `?locale=${encodeURIComponent(opts.locale)}` : ''\n const data = await get<{ items: BrandfineCategory[] }>(\n `/external/categories${qs}`,\n )\n return data.items\n },\n }\n\n const workspace: WorkspaceApi = {\n get<\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() {\n return get<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>(\n '/external/workspace',\n )\n },\n }\n\n const navigations: NavigationsApi = {\n get<TConfig = unknown>(key: string) {\n return get<BrandfineNavigation<TConfig> | null>(\n `/external/navigations/${encodeURIComponent(key)}`,\n { nullable404: true },\n )\n },\n }\n\n const analytics: AnalyticsApi = {\n getConfig() {\n return get<AnalyticsConfig>('/external/analytics-config')\n },\n async install(opts: InstallOptions = {}) {\n // SSR safety: nothing to inject without a DOM. Consumers\n // call this from useEffect / onMount, but defensive anyway\n // (some frameworks still execute the file body on the server).\n if (typeof document === 'undefined') {\n return { installed: false, reason: 'ssr' as const }\n }\n\n // Use caller-provided config if present (build-time path),\n // otherwise fetch (runtime path).\n const cfg = opts.config ?? (await analytics.getConfig())\n if (!cfg.enabled) {\n return { installed: false, reason: 'disabled' as const }\n }\n\n // Idempotency: a prior call (StrictMode double-invoke, SPA\n // re-mount, second instance with the same workspace) may\n // have already injected. The marker attribute is the source\n // of truth — checking by script src would also miss the case\n // where two workspaces share the same scriptUrl.\n const existing = document.querySelector<HTMLScriptElement>(\n `script[${INSTALLED_MARKER}=\"${cfg.websiteId}\"]`,\n )\n if (existing) {\n return { installed: false, reason: 'already-installed' as const }\n }\n\n const script = document.createElement('script')\n script.defer = true\n script.src = cfg.scriptUrl\n script.setAttribute('data-website-id', cfg.websiteId)\n // The marker doubles as a sentinel + a debug aid (you can\n // grep the DOM for `data-brandfine-analytics` to confirm\n // an install).\n script.setAttribute(INSTALLED_MARKER, cfg.websiteId)\n document.head.appendChild(script)\n return { installed: true, websiteId: cfg.websiteId }\n },\n }\n\n const submissions: SubmissionsApi = {\n async create(input: CreateSubmissionInput) {\n const url = `${baseUrl}/external/submissions`\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n body: JSON.stringify(input),\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as Submission\n },\n }\n\n /**\n * Shared POST helper for the appointments namespace. The main\n * `get()` helper handles GETs; submissions has its own inline\n * POST because it predates this refactor. New plugin namespaces\n * (appointments first, others to follow) share this one so the\n * error-handling shape stays consistent.\n */\n async function post<T>(\n path: string,\n body: unknown,\n opts: RequestOptions = {},\n ): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n body: JSON.stringify(body),\n signal: opts.signal,\n })\n if (!res.ok) {\n const errBody = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body: errBody,\n url,\n })\n }\n if (res.status === 204) return undefined as T\n return (await res.json()) as T\n }\n\n const appointments: AppointmentsApi = {\n getAvailability(opts = {}) {\n const qs: string[] = []\n if (opts.from) qs.push(`from=${encodeURIComponent(toIso(opts.from))}`)\n if (opts.to) qs.push(`to=${encodeURIComponent(toIso(opts.to))}`)\n const suffix = qs.length ? `?${qs.join('&')}` : ''\n return get<AppointmentAvailability>(\n `/external/appointments/availability${suffix}`,\n )\n },\n createRequest(input) {\n return post<CreatedAppointmentRequest>(\n '/external/appointments/requests',\n input,\n )\n },\n }\n\n return {\n get,\n posts,\n categories,\n workspace,\n navigations,\n analytics,\n submissions,\n appointments,\n }\n}\n\n/** Accepts a Date or an already-ISO string and returns ISO. Saves\n * every caller from `.toISOString()`-ing manually. */\nfunction toIso(d: Date | string): string {\n return typeof d === 'string' ? d : d.toISOString()\n}\n","/**\n * @brandfine/client — root entry.\n *\n * The full SDK surface is exposed here for \"import everything from\n * one place\" usage. Tree-shaking + `sideEffects: false` mean\n * consumers don't pay a bundle cost for what they don't import.\n *\n * Heavier or framework-coupled pieces still live under subpath\n * exports (`@brandfine/client/cache`, `/resolvers`, `/webhook`) so\n * consumers with poor tree-shaking — or who only need one slice —\n * can scope their imports.\n */\n\nexport const SDK_VERSION = '0.0.0' as const\n\nexport {\n BrandfineApiError,\n createBrandfineClient,\n type AnalyticsConfig,\n type AnalyticsInstallResult,\n type BrandfineClient,\n type BrandfineClientConfig,\n type CreateSubmissionInput,\n type InstallOptions,\n type Submission,\n} from './client'\n\nexport {\n createCache,\n createKeyedCache,\n type Cache,\n type CacheOptions,\n type KeyedCache,\n type KeyedCacheOptions,\n} from './cache/index'\n\nexport {\n isLocale,\n localizePath,\n pickLocale,\n resolveNavigation,\n stripLocalePrefix,\n type HydratedNav,\n type HydratedNavItem,\n type LocaleOptions,\n type ResolveNavigationOptions,\n} from './resolvers/index'\n\nexport {\n createBrandfineWebhookHandler,\n parseWebhookPayload,\n verifyWebhookSecret,\n type BrandfineWebhookEvent,\n type BrandfineWebhookHandlerOptions,\n type BrandfineWebhookPayload,\n} from './webhook/index'\n\nexport type {\n BrandfineCategory,\n BrandfineNavItem,\n BrandfineNavItemType,\n BrandfineNavPost,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfinePostTranslation,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `createBrandfineClient` — the SDK's entry point.\n *\n * Returns a stateless, multi-instance-safe handle scoped to a\n * single `(baseUrl, apiKey)` pair. Pattern follows the Stripe /\n * Algolia / OpenAI SDKs — explicit construction with config,\n * namespaced methods (`bf.posts.list(...)`, `bf.workspace.get()`),\n * no module-level singletons.\n *\n * Why factory not module-level state: multi-tenant consumers\n * sometimes need two clients in the same process (e.g. main site\n * + admin preview). Module-level env reading makes that impossible\n * without monkey-patching.\n */\n\nimport type {\n BrandfineCategory,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n\nexport type BrandfineClientConfig = {\n /** Base URL of the Brandfine API. No trailing slash — the client\n * trims one if you pass it anyway. e.g. `https://api.brandfine.co` */\n baseUrl: string\n /** Workspace-scoped API key. Generated from the cms's Workspace\n * settings; identifies which workspace the client talks to. */\n apiKey: string\n /** Optional fetch override. Useful for tests (inject a stub),\n * for runtimes that need a custom implementation (edge workers\n * with non-standard fetch), or to add cross-cutting concerns\n * like tracing / retries. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch\n /** Optional User-Agent header. Falls back to a generic SDK tag. */\n userAgent?: string\n}\n\n/**\n * Structured error thrown by every request helper on non-2xx\n * responses. Carries the raw body so consumers can log it for\n * debugging without re-fetching.\n */\nexport class BrandfineApiError extends Error {\n override readonly name = 'BrandfineApiError'\n readonly status: number\n readonly statusText: string\n readonly body: string\n readonly url: string\n\n constructor(args: {\n status: number\n statusText: string\n body: string\n url: string\n }) {\n super(\n `[brandfine] ${args.status} ${args.statusText} on ${args.url} — ${args.body.slice(0, 200)}`,\n )\n this.status = args.status\n this.statusText = args.statusText\n this.body = args.body\n this.url = args.url\n }\n}\n\ntype RequestOptions = {\n /** When true and the response is 404, return `null` instead of\n * throwing. Used by endpoints where 404 is a meaningful empty\n * state (navigation by key, single post by slug). */\n nullable404?: boolean\n signal?: AbortSignal\n}\n\nexport type BrandfineClient = {\n /** Low-level GET. Reserved for endpoints we don't have a typed\n * helper for yet. Adds the X-Api-Key header automatically. */\n get: <T>(path: string, opts?: RequestOptions) => Promise<T>\n posts: PostsApi\n categories: CategoriesApi\n workspace: WorkspaceApi\n navigations: NavigationsApi\n analytics: AnalyticsApi\n submissions: SubmissionsApi\n appointments: AppointmentsApi\n liveChat: LiveChatApi\n}\n\ntype PostsApi = {\n /** Paginated list of published posts. Handles the cms's\n * pagination transparently — caller gets a flat array. */\n list: <TConfig = unknown>(\n opts?: ListPostsOptions,\n ) => Promise<BrandfinePost<TConfig>[]>\n /** Single post by per-locale URL slug, scoped to the active\n * locale on the workspace's content. Returns `null` for 404 so\n * callers can render their own \"not found\" page without try/catch. */\n getBySlug: <TConfig = unknown>(\n slug: string,\n ) => Promise<BrandfinePost<TConfig> | null>\n}\n\ntype CategoriesApi = {\n list: (opts?: ListCategoriesOptions) => Promise<BrandfineCategory[]>\n}\n\ntype WorkspaceApi = {\n get: <\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() => Promise<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>\n}\n\ntype NavigationsApi = {\n /** Navigation by its workspace-scoped `key` (e.g. `'header'`).\n * Returns `null` for 404 so consumers can fall back to a\n * hardcoded default without try/catch. `TConfig` narrows each\n * item's `customConfig` (default `unknown`). */\n get: <TConfig = unknown>(\n key: string,\n ) => Promise<BrandfineNavigation<TConfig> | null>\n}\n\nexport type CreateSubmissionInput = {\n /** Required. Display name of the submitter. */\n name: string\n /** Required. Validated server-side. */\n email: string\n /** Optional. Free-text up to 40 chars. */\n phone?: string\n /** Optional. Free-text up to 200 chars. */\n subject?: string\n /** Required. The message body — up to 10,000 chars. */\n message: string\n /** Optional. Where the submission came from — e.g. a route path\n * like `/contact`, or a marketing campaign label. Up to 500 chars. */\n source?: string\n /** Optional. Free-form JSON metadata the consumer attaches; the\n * cms surfaces it verbatim in the submissions admin view. */\n metadata?: Record<string, unknown>\n}\n\nexport type Submission = {\n id: string\n createdAt: string\n}\n\ntype SubmissionsApi = {\n /**\n * Posts a contact-form submission to `POST /external/submissions`\n * for this workspace. The cms surfaces the submission in the\n * Submissions inbox.\n *\n * Throws `BrandfineApiError` on validation failures (400) or\n * any other non-2xx — caller decides whether to surface that as\n * a user-visible error or a silent retry.\n */\n create: (input: CreateSubmissionInput) => Promise<Submission>\n}\n\n// ----------------------------------------------------------------\n// Appointments plugin SDK — pairs with the Appointments embed\n// widget. Consumers who want full control over the booking UI use\n// these methods directly; consumers who want the drop-in widget\n// use the `<script>` embed (which itself uses these methods under\n// the hood). The same `BrandfineClient` instance powers both.\n// ----------------------------------------------------------------\n\nexport type AppointmentSlot = {\n /** UTC ISO 8601 timestamp of the slot start. */\n start: string\n /** UTC ISO 8601 timestamp of the slot end. */\n end: string\n}\n\nexport type AppointmentAvailability = {\n /** False = plugin not activated, or activation row's `enabled`\n * flag is off. Widgets should render a \"not accepting bookings\"\n * state, not throw. */\n enabled: boolean\n /** Source-of-truth IANA timezone for the workspace's business\n * hours. Visitors see slots in their local TZ — use this for\n * the \"(workspace local: HH:MM)\" subtext. */\n timezone: string\n slotDurationMinutes: number\n leadTimeHours: number\n bookingWindowDays: number\n policyText: string | null\n slots: AppointmentSlot[]\n /** UTC ISO 8601. Useful for the widget's date range label. */\n windowStart: string\n windowEnd: string\n}\n\nexport type CreateAppointmentRequestInput = {\n visitorName: string\n visitorEmail: string\n visitorPhone?: string\n visitorMessage?: string\n /** UTC ISO 8601 of the requested slot start. Server re-validates\n * against business hours + busy ranges before accepting. */\n requestedAt: string\n /** Optional cookie-derived session id from the consumer site. */\n visitorSessionId?: string\n}\n\nexport type CreatedAppointmentRequest = {\n id: string\n createdAt: string\n requestedAt: string\n durationMinutes: number\n status: 'PENDING'\n /** Visitor's self-cancel token. Embed it in confirmation\n * emails / on-page UI so the visitor can cancel without an\n * account. One-time use; revoked once any party acts. */\n cancellationToken: string | null\n}\n\ntype AppointmentsApi = {\n /**\n * Available slots for the workspace's booking window.\n * `from` / `to` are optional clamps inside the workspace's\n * configured window — the server ignores ranges outside.\n */\n getAvailability: (opts?: {\n from?: Date | string\n to?: Date | string\n }) => Promise<AppointmentAvailability>\n /**\n * Submit a visitor's appointment request. Server-side validates\n * the slot is still bookable; if it isn't, throws\n * `BrandfineApiError` with status 404 / 409.\n *\n * The visitor's browser does not have any other appointment\n * actions in v1 — post-submission status changes (approve /\n * decline / reschedule) happen via email, driven by the\n * customer in the CMS.\n */\n createRequest: (\n input: CreateAppointmentRequestInput,\n ) => Promise<CreatedAppointmentRequest>\n}\n\nexport type AnalyticsConfig =\n | {\n enabled: false\n /** GA4 Measurement ID — present when the workspace's Google\n * Analytics property was provisioned through Brandfine AND\n * the customer opted into tag injection. `install()` loads\n * gtag for it. Note gtag sets cookies: consent banners are\n * your site's responsibility. */\n gaMeasurementId?: string\n }\n | {\n enabled: true\n websiteId: string\n scriptUrl: string\n gaMeasurementId?: string\n }\n\nexport type AnalyticsOverviewRange = '24h' | '7d' | '30d' | '90d'\n\n/**\n * Composed traffic report for the workspace — summary KPIs +\n * bucketed chart data + top pages in one payload. Mirrors\n * `GET /external/analytics/overview` (see the API's\n * `ExternalAnalyticsOverview` type); additive changes only.\n *\n * Three shapes to handle:\n * - `{ enabled: false }` — analytics never enabled for the\n * workspace. Show an enable CTA.\n * - `{ enabled: true, verified: false }` — tracker provisioned\n * but no pageview recorded yet. Show \"waiting for first visit\".\n * - full payload — render the dashboard.\n */\nexport type AnalyticsOverview =\n | { enabled: false }\n | { enabled: true; verified: false }\n | {\n enabled: true\n verified: true\n range: AnalyticsOverviewRange\n summary: {\n visitors: number\n /** Fractional change vs the prior window (0.12 = +12%). */\n visitorsChange: number\n pageviews: number\n pageviewsChange: number\n visits: number\n visitsChange: number\n /** 0..1 fraction. */\n bounceRate: number\n bounceRateChange: number\n avgVisitSeconds: number\n avgVisitSecondsChange: number\n /** Visitors active in the last ~5 minutes. */\n activeNow: number\n }\n /** Bucketed chart data, oldest → newest. Hourly buckets for\n * `24h`, daily otherwise. `t` is an ISO-8601 bucket start. */\n timeseries: Array<{ t: string; visitors: number; pageviews: number }>\n /** Top 10 paths by views in the window. */\n topPages: Array<{ path: string; views: number; visitors: number }>\n /** Top 10 referrer sources by visitors. Empty-string source\n * means direct traffic. */\n sources: Array<{ source: string; visitors: number }>\n /** Top 10 visitor countries (ISO 3166-1 alpha-2 codes —\n * map to display names on your side, e.g. via\n * `Intl.DisplayNames`). */\n countries: Array<{ country: string; visitors: number }>\n /** Visitors by device class (`desktop` / `mobile` /\n * `tablet` / …). */\n devices: Array<{ device: string; visitors: number }>\n }\n\nexport type AnalyticsInstallResult =\n | { installed: false; reason: 'disabled' | 'ssr' | 'already-installed' }\n | { installed: true; websiteId: string }\n\nexport type InstallOptions = {\n /**\n * Pre-known config. When provided, `install()` skips the round-\n * trip to `/external/analytics-config` and injects the script\n * immediately. Use this when you've baked the values into your\n * build (env vars, CMS-side config dump, etc.) — typical for\n * static sites where the analytics state is decided at deploy\n * time, not per page load.\n *\n * Trade-off vs the default fetch path: if you disable analytics\n * in Brandfine, the tracker keeps loading until your next\n * deploy. That's usually the right trade for static sites\n * (which redeploy on every content change anyway) and the wrong\n * trade for dynamic sites where the api round-trip is cheap\n * relative to the rest of the page.\n *\n * Pass `{ enabled: false }` to force a no-op without touching\n * the api (e.g. to disable analytics for one environment without\n * changing Brandfine's state).\n */\n config?: AnalyticsConfig\n}\n\ntype AnalyticsApi = {\n /**\n * Injects the Brandfine analytics tracker into `document.head`\n * once. Safe to call on every page load — idempotent via a\n * marker attribute on the injected script tag.\n *\n * Two paths:\n * - `install()` — fetches the config from Brandfine, then\n * injects. Reflects enable/disable state on next page load.\n * - `install({ config })` — uses caller-provided config, skips\n * the fetch. Faster, no round-trip; ignores Brandfine state\n * changes until the consumer's next deploy.\n *\n * Returns details about what happened:\n * - `{ installed: true, websiteId }` — script was just injected.\n * - `{ installed: false, reason: 'disabled' }` — config says\n * analytics is off; no-op.\n * - `{ installed: false, reason: 'ssr' }` — no `document` in\n * scope (server-side). Call again on the client.\n * - `{ installed: false, reason: 'already-installed' }` — a\n * prior call (or another tab in the same SPA) already injected.\n *\n * Throws `BrandfineApiError` on non-2xx responses other than the\n * disabled case (which is a valid `{ enabled: false }` body).\n */\n install: (opts?: InstallOptions) => Promise<AnalyticsInstallResult>\n\n /** Lower-level helper — fetches the raw config without touching\n * the DOM. Useful when you want to inject the script yourself\n * (e.g. via a framework's <Script> component for nonce/csp). */\n getConfig: () => Promise<AnalyticsConfig>\n\n /**\n * Traffic report for the workspace — summary KPIs, bucketed\n * timeseries for charting, and top pages, in one round-trip.\n * This is a server-to-server read (it returns your site's\n * traffic data); call it from your backend or build step, not\n * from visitor-facing browser code.\n *\n * @param opts.range Window preset. Defaults to `'7d'`.\n */\n overview: (opts?: {\n range?: AnalyticsOverviewRange\n }) => Promise<AnalyticsOverview>\n}\n\nexport type LiveChatBootstrap =\n | { enabled: false }\n | {\n enabled: true\n /** The workspace's SCOPED publishable key — safe to bake into\n * public HTML; it can only start chat conversations. */\n publishableKey: string\n greeting: string | null\n offlineMessage: string | null\n theme: Record<string, string> | null\n /** Widget bundle path relative to the API base URL. */\n scriptPath: string\n }\n\nexport type LiveChatInstallResult =\n | { installed: false; reason: 'disabled' | 'ssr' | 'already-installed' }\n | { installed: true }\n\nexport type LiveChatInstallOptions = {\n /**\n * Pre-known bootstrap. Same build-time/runtime trade-off as the\n * analytics `install()`: pass the value your server half fetched\n * (static-export sites bake it at build time), or omit — NOT\n * recommended in browsers, because fetching here would require\n * the broad key client-side. In practice: always pass `config`\n * from your server half.\n */\n config: LiveChatBootstrap\n}\n\ntype LiveChatApi = {\n /**\n * Fetches the Live Chat bootstrap (publishable key + display\n * config) with the broad workspace key. SERVER-SIDE ONLY — call\n * from your backend, RSC, or build step, never from browser code.\n */\n getConfig: () => Promise<LiveChatBootstrap>\n\n /**\n * Injects the chat widget into the page: appends a host\n * `<div data-bf-live-chat>` carrying the publishable key and the\n * widget `<script>` tag. Idempotent via a marker attribute.\n * Browser-side half of the pair — receives the bootstrap your\n * server half fetched with `getConfig()`.\n *\n * Returns what happened, mirroring `analytics.install()`:\n * - `{ installed: true }` — widget just injected.\n * - `{ installed: false, reason: 'disabled' }` — chat is off.\n * - `{ installed: false, reason: 'ssr' }` — no `document`.\n * - `{ installed: false, reason: 'already-installed' }`.\n */\n install: (opts: LiveChatInstallOptions) => Promise<LiveChatInstallResult>\n}\n\n/** Attribute we stamp on the injected <script> so `install()` is\n * idempotent across re-renders and SPA route changes. */\nconst INSTALLED_MARKER = 'data-brandfine-analytics'\n\n/** Same idempotency contract for the live-chat widget injection. */\nconst LIVE_CHAT_MARKER = 'data-brandfine-live-chat'\n\n/** Marker for the injected Google tag — same idempotency contract. */\nconst GTAG_MARKER = 'data-brandfine-gtag'\n\n/**\n * Inject the Google tag (gtag.js) for an auto-provisioned GA4\n * property. No-ops when ANY gtag script is already on the page —\n * a site that hand-installed Google Analytics must not get a\n * second config (double-counted sessions are worse than a missing\n * tag). Safe to call repeatedly; the marker makes it idempotent.\n */\nfunction injectGoogleTag(measurementId: string): void {\n if (typeof document === 'undefined') return\n const existing = document.querySelector(\n `script[src*=\"googletagmanager.com/gtag/js\"], script[${GTAG_MARKER}]`,\n )\n if (existing) return\n\n const loader = document.createElement('script')\n loader.async = true\n loader.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`\n loader.setAttribute(GTAG_MARKER, measurementId)\n document.head.appendChild(loader)\n\n const w = window as unknown as { dataLayer?: unknown[] }\n w.dataLayer = w.dataLayer ?? []\n // gtag() must push `arguments` (an Arguments object), not a\n // plain array — GA's snippet relies on it.\n function gtag(..._args: unknown[]) {\n // eslint-disable-next-line prefer-rest-params\n w.dataLayer!.push(arguments)\n }\n gtag('js', new Date())\n gtag('config', measurementId)\n}\n\nconst DEFAULT_USER_AGENT = '@brandfine/client'\n\nexport function createBrandfineClient(\n config: BrandfineClientConfig,\n): BrandfineClient {\n if (!config.baseUrl)\n throw new Error('createBrandfineClient: `baseUrl` is required')\n if (!config.apiKey)\n throw new Error('createBrandfineClient: `apiKey` is required')\n\n const baseUrl = config.baseUrl.replace(/\\/$/, '')\n const apiKey = config.apiKey\n // Resolve fetch lazily so consumers in environments without a\n // global fetch can polyfill before constructing the client.\n const fetchImpl: typeof fetch = config.fetch ?? globalThis.fetch\n const userAgent = config.userAgent ?? DEFAULT_USER_AGENT\n\n async function get<T>(path: string, opts: RequestOptions = {}): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'GET',\n headers: {\n 'X-Api-Key': apiKey,\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n signal: opts.signal,\n })\n if (res.status === 404 && opts.nullable404) {\n // Drain the body so the underlying socket can be reused —\n // fetch implementations that don't auto-drain (older Node)\n // can leak otherwise.\n await res.text().catch(() => '')\n return null as T\n }\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as T\n }\n\n const posts: PostsApi = {\n async list<TConfig = unknown>(opts: ListPostsOptions = {}) {\n const out: BrandfinePost<TConfig>[] = []\n let page = 1\n const typeQuery = opts.type ? `&type=${encodeURIComponent(opts.type)}` : ''\n const localeQuery = opts.locale\n ? `&locale=${encodeURIComponent(opts.locale)}`\n : ''\n // Default pagination at the cms's 50-per-page cap. `forceLimit`\n // opts past it for content types that would otherwise need\n // many round-trips.\n const sizeQuery = opts.forceLimit\n ? `&force_limit=${opts.forceLimit}`\n : '&limit=50'\n // Pathological safety brake — 200 pages × 50 = 10k posts. If\n // a workspace ever needs more, callers should hit the API\n // directly with their own pagination logic.\n const MAX_PAGES = 200\n while (page <= MAX_PAGES) {\n const data = await get<BrandfinePostListResponse<TConfig>>(\n `/external/posts?include=content${sizeQuery}&page=${page}${typeQuery}${localeQuery}`,\n )\n out.push(...data.items)\n if (!data.pageInfo.hasNext) break\n page += 1\n }\n return out\n },\n async getBySlug<TConfig = unknown>(slug: string) {\n return get<BrandfinePost<TConfig> | null>(\n `/external/posts/${encodeURIComponent(slug)}`,\n { nullable404: true },\n )\n },\n }\n\n const categories: CategoriesApi = {\n async list(opts: ListCategoriesOptions = {}) {\n const qs = opts.locale ? `?locale=${encodeURIComponent(opts.locale)}` : ''\n const data = await get<{ items: BrandfineCategory[] }>(\n `/external/categories${qs}`,\n )\n return data.items\n },\n }\n\n const workspace: WorkspaceApi = {\n get<\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() {\n return get<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>(\n '/external/workspace',\n )\n },\n }\n\n const navigations: NavigationsApi = {\n get<TConfig = unknown>(key: string) {\n return get<BrandfineNavigation<TConfig> | null>(\n `/external/navigations/${encodeURIComponent(key)}`,\n { nullable404: true },\n )\n },\n }\n\n const analytics: AnalyticsApi = {\n getConfig() {\n return get<AnalyticsConfig>('/external/analytics-config')\n },\n overview(opts = {}) {\n const range = opts.range ?? '7d'\n return get<AnalyticsOverview>(\n `/external/analytics/overview?range=${encodeURIComponent(range)}`,\n )\n },\n async install(opts: InstallOptions = {}) {\n // SSR safety: nothing to inject without a DOM. Consumers\n // call this from useEffect / onMount, but defensive anyway\n // (some frameworks still execute the file body on the server).\n if (typeof document === 'undefined') {\n return { installed: false, reason: 'ssr' as const }\n }\n\n // Use caller-provided config if present (build-time path),\n // otherwise fetch (runtime path).\n const cfg = opts.config ?? (await analytics.getConfig())\n\n // Google tag rides alongside the built-in tracker — injected\n // even when Brandfine analytics itself is off, because the\n // opt-in lives on the GA integration, not on the tracker.\n if (cfg.gaMeasurementId) {\n injectGoogleTag(cfg.gaMeasurementId)\n }\n\n if (!cfg.enabled) {\n return { installed: false, reason: 'disabled' as const }\n }\n\n // Idempotency: a prior call (StrictMode double-invoke, SPA\n // re-mount, second instance with the same workspace) may\n // have already injected. The marker attribute is the source\n // of truth — checking by script src would also miss the case\n // where two workspaces share the same scriptUrl.\n const existing = document.querySelector<HTMLScriptElement>(\n `script[${INSTALLED_MARKER}=\"${cfg.websiteId}\"]`,\n )\n if (existing) {\n return { installed: false, reason: 'already-installed' as const }\n }\n\n const script = document.createElement('script')\n script.defer = true\n script.src = cfg.scriptUrl\n script.setAttribute('data-website-id', cfg.websiteId)\n // The marker doubles as a sentinel + a debug aid (you can\n // grep the DOM for `data-brandfine-analytics` to confirm\n // an install).\n script.setAttribute(INSTALLED_MARKER, cfg.websiteId)\n document.head.appendChild(script)\n return { installed: true, websiteId: cfg.websiteId }\n },\n }\n\n const liveChat: LiveChatApi = {\n getConfig() {\n return get<LiveChatBootstrap>('/external/live-chat/bootstrap')\n },\n async install(opts: LiveChatInstallOptions) {\n if (typeof document === 'undefined') {\n return { installed: false, reason: 'ssr' as const }\n }\n\n const cfg = opts.config\n if (!cfg.enabled) {\n return { installed: false, reason: 'disabled' as const }\n }\n\n const existing = document.querySelector(`[${LIVE_CHAT_MARKER}]`)\n if (existing) {\n return { installed: false, reason: 'already-installed' as const }\n }\n\n // Host div the widget script mounts into. Theme vars ride as\n // inline custom properties — they inherit through the widget's\n // shadow boundary.\n const host = document.createElement('div')\n host.setAttribute('data-bf-live-chat', '')\n host.setAttribute('data-publishable-key', cfg.publishableKey)\n host.setAttribute('data-base-url', baseUrl)\n host.setAttribute(LIVE_CHAT_MARKER, '')\n if (cfg.theme) {\n for (const [key, value] of Object.entries(cfg.theme)) {\n if (key.startsWith('--bf-chat-')) {\n host.style.setProperty(key, value)\n }\n }\n }\n document.body.appendChild(host)\n\n const script = document.createElement('script')\n script.defer = true\n script.src = `${baseUrl}${cfg.scriptPath}`\n script.setAttribute(LIVE_CHAT_MARKER, 'script')\n document.head.appendChild(script)\n\n return { installed: true }\n },\n }\n\n const submissions: SubmissionsApi = {\n async create(input: CreateSubmissionInput) {\n const url = `${baseUrl}/external/submissions`\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n body: JSON.stringify(input),\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as Submission\n },\n }\n\n /**\n * Shared POST helper for the appointments namespace. The main\n * `get()` helper handles GETs; submissions has its own inline\n * POST because it predates this refactor. New plugin namespaces\n * (appointments first, others to follow) share this one so the\n * error-handling shape stays consistent.\n */\n async function post<T>(\n path: string,\n body: unknown,\n opts: RequestOptions = {},\n ): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n body: JSON.stringify(body),\n signal: opts.signal,\n })\n if (!res.ok) {\n const errBody = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body: errBody,\n url,\n })\n }\n if (res.status === 204) return undefined as T\n return (await res.json()) as T\n }\n\n const appointments: AppointmentsApi = {\n getAvailability(opts = {}) {\n const qs: string[] = []\n if (opts.from) qs.push(`from=${encodeURIComponent(toIso(opts.from))}`)\n if (opts.to) qs.push(`to=${encodeURIComponent(toIso(opts.to))}`)\n const suffix = qs.length ? `?${qs.join('&')}` : ''\n return get<AppointmentAvailability>(\n `/external/appointments/availability${suffix}`,\n )\n },\n createRequest(input) {\n return post<CreatedAppointmentRequest>(\n '/external/appointments/requests',\n input,\n )\n },\n }\n\n return {\n get,\n posts,\n categories,\n workspace,\n navigations,\n analytics,\n submissions,\n appointments,\n liveChat,\n }\n}\n\n/** Accepts a Date or an already-ISO string and returns ISO. Saves\n * every caller from `.toISOString()`-ing manually. */\nfunction toIso(d: Date | string): string {\n return typeof d === 'string' ? d : d.toISOString()\n}\n","/**\n * @brandfine/client — root entry.\n *\n * The full SDK surface is exposed here for \"import everything from\n * one place\" usage. Tree-shaking + `sideEffects: false` mean\n * consumers don't pay a bundle cost for what they don't import.\n *\n * Heavier or framework-coupled pieces still live under subpath\n * exports (`@brandfine/client/cache`, `/resolvers`, `/webhook`) so\n * consumers with poor tree-shaking — or who only need one slice —\n * can scope their imports.\n */\n\nexport const SDK_VERSION = '0.0.0' as const\n\nexport {\n BrandfineApiError,\n createBrandfineClient,\n type AnalyticsConfig,\n type AnalyticsInstallResult,\n type AnalyticsOverview,\n type AnalyticsOverviewRange,\n type BrandfineClient,\n type BrandfineClientConfig,\n type CreateSubmissionInput,\n type InstallOptions,\n type Submission,\n} from './client'\n\nexport {\n createCache,\n createKeyedCache,\n type Cache,\n type CacheOptions,\n type KeyedCache,\n type KeyedCacheOptions,\n} from './cache/index'\n\nexport {\n isLocale,\n localizePath,\n pickLocale,\n resolveNavigation,\n stripLocalePrefix,\n type HydratedNav,\n type HydratedNavItem,\n type LocaleOptions,\n type ResolveNavigationOptions,\n} from './resolvers/index'\n\nexport {\n createBrandfineWebhookHandler,\n parseWebhookPayload,\n verifyWebhookSecret,\n type BrandfineWebhookEvent,\n type BrandfineWebhookHandlerOptions,\n type BrandfineWebhookPayload,\n} from './webhook/index'\n\nexport type {\n BrandfineCategory,\n BrandfineNavItem,\n BrandfineNavItemType,\n BrandfineNavPost,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfinePostTranslation,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandfine/client",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Brandfine consumer SDK — typed HTTP client, server-side caches, locale + navigation resolvers, and webhook helpers for landing-page integrations.",
5
5
  "license": "MIT",
6
6
  "type": "module",