@brandfine/client 0.8.0 → 0.10.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,26 @@
1
1
  # @brandfine/client
2
2
 
3
+ ## 0.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e22934a: Live Chat verified visitor identity:
8
+ - `liveChat.identityToken(externalId, { secret? })` — server-only HMAC-SHA256 helper (WebCrypto; throws in browsers and when the secret is missing — also reads `BRANDFINE_LIVE_CHAT_IDENTITY_SECRET`).
9
+ - `liveChat.install({ config, visitor })` — optional signed `visitor` (`externalId`, `name?`, `email?`, `attributes?`, `identityToken`) serialized onto the widget host; the API verifies the signature and labels the conversation as that person, resuming their open thread across devices. Invalid or absent signatures downgrade to anonymous — never a failed chat.
10
+ - New exported type `LiveChatVisitor`.
11
+
12
+ Everything is additive; existing callers are unaffected.
13
+
14
+ ## 0.9.0
15
+
16
+ ### Minor Changes
17
+
18
+ - 28bbfe7: Add the `bf.liveChat` namespace for the Live Chat plugin:
19
+ - `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.
20
+ - `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`).
21
+
22
+ New exported types: `LiveChatBootstrap`, `LiveChatInstallResult`, `LiveChatInstallOptions`.
23
+
3
24
  ## 0.8.0
4
25
 
5
26
  ### Minor Changes
package/README.md CHANGED
@@ -72,7 +72,7 @@ The workspace API key (`bfwk_*`) is **broad-scope** — it can read posts, navig
72
72
 
73
73
  Every recipe in our docs ([SDK reference](https://docs.brandfine.co/docs/sdk/client)) keeps the SDK call server-side. Your frontend posts to your own backend; your backend hits Brandfine.
74
74
 
75
- **Static-export site without a server runtime?** See [Static sites](https://docs.brandfine.co/docs/concepts/static-sites) for your options — add an adapter, run a tiny proxy, or wait for scoped publishable keys (`bfpk_*`, on the roadmap).
75
+ **Static-export site without a server runtime?** See [Static sites](https://docs.brandfine.co/docs/concepts/static-sites) for your options — add an adapter or run a tiny proxy. Live Chat is the exception: it uses a **scoped publishable key** (`brandfine_pk_*`) that is safe in public HTML because it can only start chat conversations.
76
76
 
77
77
  ## Analytics
78
78
 
@@ -89,6 +89,57 @@ bf.analytics.install({ config })
89
89
 
90
90
  Full walkthrough with framework recipes: [docs.brandfine.co/docs/sdk/analytics](https://docs.brandfine.co/docs/sdk/analytics).
91
91
 
92
+ ## Live Chat
93
+
94
+ Same server-half / client-half shape as analytics. The server
95
+ fetches the bootstrap with the broad key; the client injects the
96
+ widget with only the **publishable key** — no secrets reach the
97
+ browser:
98
+
99
+ ```ts
100
+ // Server (RSC / build step):
101
+ const config = await bf.liveChat.getConfig()
102
+ // Client component:
103
+ bf.liveChat.install({ config })
104
+ ```
105
+
106
+ ### Verified visitors
107
+
108
+ On signed-in pages, tell the inbox **who** is chatting. Sign the
109
+ identity on your server with the workspace's identity secret (CMS:
110
+ Plugins → Live Chat → Manage settings → Integrate), then pass it to
111
+ `install()`:
112
+
113
+ ```ts
114
+ // Server — never in a browser:
115
+ const identityToken = await bf.liveChat.identityToken(user.id, {
116
+ secret: process.env.BRANDFINE_LIVE_CHAT_IDENTITY_SECRET,
117
+ })
118
+ const visitor = {
119
+ externalId: user.id,
120
+ name: user.name,
121
+ email: user.email,
122
+ attributes: { plan: user.plan },
123
+ identityToken,
124
+ }
125
+
126
+ // Client:
127
+ bf.liveChat.install({ config, visitor })
128
+ ```
129
+
130
+ Verified conversations show the visitor's name with a ✓ marker in
131
+ the Brandfine inbox and continue across devices/sessions (same
132
+ `externalId` = same person). An invalid or missing token silently
133
+ downgrades to anonymous chat — never a blocked visitor.
134
+
135
+ > **Security:** never ship the identity secret to a browser and
136
+ > never compute the HMAC client-side — either would let anyone
137
+ > impersonate any visitor. `identityToken()` enforces this: it
138
+ > throws in browser contexts and when the secret is missing. Rotate
139
+ > the secret in the CMS if it ever leaks.
140
+
141
+ Full reference: [docs.brandfine.co/docs/sdk/live-chat](https://docs.brandfine.co/docs/sdk/live-chat).
142
+
92
143
  ## Caching
93
144
 
94
145
  `createCache(opts)` and `createKeyedCache(opts)` are minimal SWR caches with TTL + background revalidation. They take any async fetcher — including `bf.posts.list` — and make the cached value the source of truth for hot paths. Pair with `verifyWebhookSecret` to invalidate on publish.
@@ -98,6 +149,7 @@ Full walkthrough with framework recipes: [docs.brandfine.co/docs/sdk/analytics](
98
149
  - [SDK quickstart](https://docs.brandfine.co/docs/sdk/quickstart) — minimal Astro integration end-to-end.
99
150
  - [`createBrandfineClient`](https://docs.brandfine.co/docs/sdk/client) — full options + method reference.
100
151
  - [Analytics install](https://docs.brandfine.co/docs/sdk/analytics) — runtime vs build-time, framework recipes.
152
+ - [Live Chat](https://docs.brandfine.co/docs/sdk/live-chat) — widget install + verified visitor identity.
101
153
  - [Submissions](https://docs.brandfine.co/docs/sdk/submissions) — POST a contact-form submission.
102
154
  - [Appointments](https://docs.brandfine.co/docs/sdk/appointments) — booking availability + visitor requests for workspaces running the Appointments plugin (server-side only).
103
155
  - [Webhook handler](https://docs.brandfine.co/docs/sdk/webhooks) — verify + parse + dispatch.
package/dist/index.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var chunkXJFKL2HU_cjs = require('./chunk-XJFKL2HU.cjs');
4
- var chunkKHHMR2NX_cjs = require('./chunk-KHHMR2NX.cjs');
5
4
  var chunkFCK7QJBC_cjs = require('./chunk-FCK7QJBC.cjs');
5
+ var chunkKHHMR2NX_cjs = require('./chunk-KHHMR2NX.cjs');
6
6
 
7
7
  // src/client.ts
8
8
  var BrandfineApiError = class extends Error {
@@ -22,6 +22,7 @@ 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";
25
26
  var GTAG_MARKER = "data-brandfine-gtag";
26
27
  function injectGoogleTag(measurementId) {
27
28
  if (typeof document === "undefined") return;
@@ -163,6 +164,76 @@ function createBrandfineClient(config) {
163
164
  return { installed: true, websiteId: cfg.websiteId };
164
165
  }
165
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 (opts.visitor?.externalId && opts.visitor.identityToken) {
189
+ host.setAttribute("data-visitor", JSON.stringify(opts.visitor));
190
+ }
191
+ if (cfg.theme) {
192
+ for (const [key, value] of Object.entries(cfg.theme)) {
193
+ if (key.startsWith("--bf-chat-")) {
194
+ host.style.setProperty(key, value);
195
+ }
196
+ }
197
+ }
198
+ document.body.appendChild(host);
199
+ const script = document.createElement("script");
200
+ script.defer = true;
201
+ script.src = `${baseUrl}${cfg.scriptPath}`;
202
+ script.setAttribute(LIVE_CHAT_MARKER, "script");
203
+ document.head.appendChild(script);
204
+ return { installed: true };
205
+ },
206
+ async identityToken(externalId, opts = {}) {
207
+ if (typeof document !== "undefined" || typeof window !== "undefined") {
208
+ throw new Error(
209
+ "liveChat.identityToken() is server-only \u2014 never compute identity tokens in a browser. Sign the visitor on your server and pass the result to install({ visitor })."
210
+ );
211
+ }
212
+ const secret = opts.secret ?? (typeof process !== "undefined" ? process.env.BRANDFINE_LIVE_CHAT_IDENTITY_SECRET : void 0);
213
+ if (!secret) {
214
+ throw new Error(
215
+ "liveChat.identityToken(): identity secret missing. Pass { secret } or set BRANDFINE_LIVE_CHAT_IDENTITY_SECRET. Generate one in the CMS: Plugins \u2192 Live Chat \u2192 Integrate."
216
+ );
217
+ }
218
+ if (!externalId) {
219
+ throw new Error("liveChat.identityToken(): externalId is required.");
220
+ }
221
+ const enc = new TextEncoder();
222
+ const key = await globalThis.crypto.subtle.importKey(
223
+ "raw",
224
+ enc.encode(secret),
225
+ { name: "HMAC", hash: "SHA-256" },
226
+ false,
227
+ ["sign"]
228
+ );
229
+ const sig = await globalThis.crypto.subtle.sign(
230
+ "HMAC",
231
+ key,
232
+ enc.encode(externalId)
233
+ );
234
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
235
+ }
236
+ };
166
237
  const submissions = {
167
238
  async create(input) {
168
239
  const url = `${baseUrl}/external/submissions`;
@@ -238,7 +309,8 @@ function createBrandfineClient(config) {
238
309
  navigations,
239
310
  analytics,
240
311
  submissions,
241
- appointments
312
+ appointments,
313
+ liveChat
242
314
  };
243
315
  }
244
316
  function toIso(d) {
@@ -256,18 +328,6 @@ Object.defineProperty(exports, "createKeyedCache", {
256
328
  enumerable: true,
257
329
  get: function () { return chunkXJFKL2HU_cjs.createKeyedCache; }
258
330
  });
259
- Object.defineProperty(exports, "createBrandfineWebhookHandler", {
260
- enumerable: true,
261
- get: function () { return chunkKHHMR2NX_cjs.createBrandfineWebhookHandler; }
262
- });
263
- Object.defineProperty(exports, "parseWebhookPayload", {
264
- enumerable: true,
265
- get: function () { return chunkKHHMR2NX_cjs.parseWebhookPayload; }
266
- });
267
- Object.defineProperty(exports, "verifyWebhookSecret", {
268
- enumerable: true,
269
- get: function () { return chunkKHHMR2NX_cjs.verifyWebhookSecret; }
270
- });
271
331
  Object.defineProperty(exports, "isLocale", {
272
332
  enumerable: true,
273
333
  get: function () { return chunkFCK7QJBC_cjs.isLocale; }
@@ -288,6 +348,18 @@ Object.defineProperty(exports, "stripLocalePrefix", {
288
348
  enumerable: true,
289
349
  get: function () { return chunkFCK7QJBC_cjs.stripLocalePrefix; }
290
350
  });
351
+ Object.defineProperty(exports, "createBrandfineWebhookHandler", {
352
+ enumerable: true,
353
+ get: function () { return chunkKHHMR2NX_cjs.createBrandfineWebhookHandler; }
354
+ });
355
+ Object.defineProperty(exports, "parseWebhookPayload", {
356
+ enumerable: true,
357
+ get: function () { return chunkKHHMR2NX_cjs.parseWebhookPayload; }
358
+ });
359
+ Object.defineProperty(exports, "verifyWebhookSecret", {
360
+ enumerable: true,
361
+ get: function () { return chunkKHHMR2NX_cjs.verifyWebhookSecret; }
362
+ });
291
363
  exports.BrandfineApiError = BrandfineApiError;
292
364
  exports.SDK_VERSION = SDK_VERSION;
293
365
  exports.createBrandfineClient = createBrandfineClient;
@@ -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 | {\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\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/** 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 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 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"]}
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\n/**\n * A signed visitor identity for Live Chat. Build it SERVER-SIDE:\n * compute `identityToken` with `liveChat.identityToken()` (or your\n * own HMAC_SHA256(identitySecret, externalId), hex) and pass the\n * whole object to `install()` — the widget forwards it verbatim and\n * the Brandfine API verifies the signature. An invalid or missing\n * token silently downgrades the conversation to anonymous.\n */\nexport type LiveChatVisitor = {\n /** Your app's stable id for this person (user id, lead reference…).\n * Conversations sharing an externalId are the same person across\n * devices and sessions. Max 128 chars. */\n externalId: string\n /** Display name shown in the Brandfine inbox. Max 120 chars. */\n name?: string\n /** Max 200 chars. */\n email?: string\n /** Small display-only key→string map (≤10 keys, values ≤100 chars). */\n attributes?: Record<string, string>\n /** hex HMAC_SHA256(identitySecret, externalId) — REQUIRED, computed\n * on your server. Never derive this in a browser. */\n identityToken: string\n}\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 * Already-signed visitor identity (see LiveChatVisitor). Optional —\n * omit for anonymous chat. The object must arrive from your server\n * with `identityToken` precomputed; `install()` only serializes it\n * onto the widget host, it performs no crypto.\n */\n visitor?: LiveChatVisitor\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 * Computes the visitor identity token:\n * hex(HMAC_SHA256(identitySecret, externalId)). SERVER-ONLY — it\n * throws in a browser context and throws when no secret is\n * provided (explicitly or via BRANDFINE_LIVE_CHAT_IDENTITY_SECRET),\n * rather than ever emitting an unsigned/mis-signed payload.\n *\n * Get the secret from the CMS: Plugins → Live Chat → Manage\n * settings → Integrate → Identity secret. Keep it in server env;\n * shipping it to a browser lets anyone impersonate any visitor.\n */\n identityToken: (\n externalId: string,\n opts?: { secret?: string },\n ) => Promise<string>\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 // Signed visitor identity → data-visitor. Serialization only;\n // the signature was computed server-side (identityToken()).\n if (opts.visitor?.externalId && opts.visitor.identityToken) {\n host.setAttribute('data-visitor', JSON.stringify(opts.visitor))\n }\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 async identityToken(externalId, opts = {}) {\n // Refuse to run where the secret could be exfiltrated. A page\n // that needs a token must get it from ITS server, not compute\n // one next to the DOM.\n if (typeof document !== 'undefined' || typeof window !== 'undefined') {\n throw new Error(\n 'liveChat.identityToken() is server-only — never compute identity ' +\n 'tokens in a browser. Sign the visitor on your server and pass ' +\n 'the result to install({ visitor }).',\n )\n }\n const secret =\n opts.secret ??\n (typeof process !== 'undefined'\n ? process.env.BRANDFINE_LIVE_CHAT_IDENTITY_SECRET\n : undefined)\n if (!secret) {\n throw new Error(\n 'liveChat.identityToken(): identity secret missing. Pass ' +\n '{ secret } or set BRANDFINE_LIVE_CHAT_IDENTITY_SECRET. ' +\n 'Generate one in the CMS: Plugins → Live Chat → Integrate.',\n )\n }\n if (!externalId) {\n throw new Error('liveChat.identityToken(): externalId is required.')\n }\n // WebCrypto (Node ≥20 global) — avoids a node:crypto import\n // that would break browser bundling of this dual-target module.\n const enc = new TextEncoder()\n const key = await globalThis.crypto.subtle.importKey(\n 'raw',\n enc.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n )\n const sig = await globalThis.crypto.subtle.sign(\n 'HMAC',\n key,\n enc.encode(externalId),\n )\n return Array.from(new Uint8Array(sig))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\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
@@ -352,6 +353,102 @@ type AnalyticsApi = {
352
353
  range?: AnalyticsOverviewRange;
353
354
  }) => Promise<AnalyticsOverview>;
354
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
+ /**
376
+ * A signed visitor identity for Live Chat. Build it SERVER-SIDE:
377
+ * compute `identityToken` with `liveChat.identityToken()` (or your
378
+ * own HMAC_SHA256(identitySecret, externalId), hex) and pass the
379
+ * whole object to `install()` — the widget forwards it verbatim and
380
+ * the Brandfine API verifies the signature. An invalid or missing
381
+ * token silently downgrades the conversation to anonymous.
382
+ */
383
+ type LiveChatVisitor = {
384
+ /** Your app's stable id for this person (user id, lead reference…).
385
+ * Conversations sharing an externalId are the same person across
386
+ * devices and sessions. Max 128 chars. */
387
+ externalId: string;
388
+ /** Display name shown in the Brandfine inbox. Max 120 chars. */
389
+ name?: string;
390
+ /** Max 200 chars. */
391
+ email?: string;
392
+ /** Small display-only key→string map (≤10 keys, values ≤100 chars). */
393
+ attributes?: Record<string, string>;
394
+ /** hex HMAC_SHA256(identitySecret, externalId) — REQUIRED, computed
395
+ * on your server. Never derive this in a browser. */
396
+ identityToken: string;
397
+ };
398
+ type LiveChatInstallOptions = {
399
+ /**
400
+ * Pre-known bootstrap. Same build-time/runtime trade-off as the
401
+ * analytics `install()`: pass the value your server half fetched
402
+ * (static-export sites bake it at build time), or omit — NOT
403
+ * recommended in browsers, because fetching here would require
404
+ * the broad key client-side. In practice: always pass `config`
405
+ * from your server half.
406
+ */
407
+ config: LiveChatBootstrap;
408
+ /**
409
+ * Already-signed visitor identity (see LiveChatVisitor). Optional —
410
+ * omit for anonymous chat. The object must arrive from your server
411
+ * with `identityToken` precomputed; `install()` only serializes it
412
+ * onto the widget host, it performs no crypto.
413
+ */
414
+ visitor?: LiveChatVisitor;
415
+ };
416
+ type LiveChatApi = {
417
+ /**
418
+ * Fetches the Live Chat bootstrap (publishable key + display
419
+ * config) with the broad workspace key. SERVER-SIDE ONLY — call
420
+ * from your backend, RSC, or build step, never from browser code.
421
+ */
422
+ getConfig: () => Promise<LiveChatBootstrap>;
423
+ /**
424
+ * Injects the chat widget into the page: appends a host
425
+ * `<div data-bf-live-chat>` carrying the publishable key and the
426
+ * widget `<script>` tag. Idempotent via a marker attribute.
427
+ * Browser-side half of the pair — receives the bootstrap your
428
+ * server half fetched with `getConfig()`.
429
+ *
430
+ * Returns what happened, mirroring `analytics.install()`:
431
+ * - `{ installed: true }` — widget just injected.
432
+ * - `{ installed: false, reason: 'disabled' }` — chat is off.
433
+ * - `{ installed: false, reason: 'ssr' }` — no `document`.
434
+ * - `{ installed: false, reason: 'already-installed' }`.
435
+ */
436
+ install: (opts: LiveChatInstallOptions) => Promise<LiveChatInstallResult>;
437
+ /**
438
+ * Computes the visitor identity token:
439
+ * hex(HMAC_SHA256(identitySecret, externalId)). SERVER-ONLY — it
440
+ * throws in a browser context and throws when no secret is
441
+ * provided (explicitly or via BRANDFINE_LIVE_CHAT_IDENTITY_SECRET),
442
+ * rather than ever emitting an unsigned/mis-signed payload.
443
+ *
444
+ * Get the secret from the CMS: Plugins → Live Chat → Manage
445
+ * settings → Integrate → Identity secret. Keep it in server env;
446
+ * shipping it to a browser lets anyone impersonate any visitor.
447
+ */
448
+ identityToken: (externalId: string, opts?: {
449
+ secret?: string;
450
+ }) => Promise<string>;
451
+ };
355
452
  declare function createBrandfineClient(config: BrandfineClientConfig): BrandfineClient;
356
453
 
357
454
  /**
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
@@ -352,6 +353,102 @@ type AnalyticsApi = {
352
353
  range?: AnalyticsOverviewRange;
353
354
  }) => Promise<AnalyticsOverview>;
354
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
+ /**
376
+ * A signed visitor identity for Live Chat. Build it SERVER-SIDE:
377
+ * compute `identityToken` with `liveChat.identityToken()` (or your
378
+ * own HMAC_SHA256(identitySecret, externalId), hex) and pass the
379
+ * whole object to `install()` — the widget forwards it verbatim and
380
+ * the Brandfine API verifies the signature. An invalid or missing
381
+ * token silently downgrades the conversation to anonymous.
382
+ */
383
+ type LiveChatVisitor = {
384
+ /** Your app's stable id for this person (user id, lead reference…).
385
+ * Conversations sharing an externalId are the same person across
386
+ * devices and sessions. Max 128 chars. */
387
+ externalId: string;
388
+ /** Display name shown in the Brandfine inbox. Max 120 chars. */
389
+ name?: string;
390
+ /** Max 200 chars. */
391
+ email?: string;
392
+ /** Small display-only key→string map (≤10 keys, values ≤100 chars). */
393
+ attributes?: Record<string, string>;
394
+ /** hex HMAC_SHA256(identitySecret, externalId) — REQUIRED, computed
395
+ * on your server. Never derive this in a browser. */
396
+ identityToken: string;
397
+ };
398
+ type LiveChatInstallOptions = {
399
+ /**
400
+ * Pre-known bootstrap. Same build-time/runtime trade-off as the
401
+ * analytics `install()`: pass the value your server half fetched
402
+ * (static-export sites bake it at build time), or omit — NOT
403
+ * recommended in browsers, because fetching here would require
404
+ * the broad key client-side. In practice: always pass `config`
405
+ * from your server half.
406
+ */
407
+ config: LiveChatBootstrap;
408
+ /**
409
+ * Already-signed visitor identity (see LiveChatVisitor). Optional —
410
+ * omit for anonymous chat. The object must arrive from your server
411
+ * with `identityToken` precomputed; `install()` only serializes it
412
+ * onto the widget host, it performs no crypto.
413
+ */
414
+ visitor?: LiveChatVisitor;
415
+ };
416
+ type LiveChatApi = {
417
+ /**
418
+ * Fetches the Live Chat bootstrap (publishable key + display
419
+ * config) with the broad workspace key. SERVER-SIDE ONLY — call
420
+ * from your backend, RSC, or build step, never from browser code.
421
+ */
422
+ getConfig: () => Promise<LiveChatBootstrap>;
423
+ /**
424
+ * Injects the chat widget into the page: appends a host
425
+ * `<div data-bf-live-chat>` carrying the publishable key and the
426
+ * widget `<script>` tag. Idempotent via a marker attribute.
427
+ * Browser-side half of the pair — receives the bootstrap your
428
+ * server half fetched with `getConfig()`.
429
+ *
430
+ * Returns what happened, mirroring `analytics.install()`:
431
+ * - `{ installed: true }` — widget just injected.
432
+ * - `{ installed: false, reason: 'disabled' }` — chat is off.
433
+ * - `{ installed: false, reason: 'ssr' }` — no `document`.
434
+ * - `{ installed: false, reason: 'already-installed' }`.
435
+ */
436
+ install: (opts: LiveChatInstallOptions) => Promise<LiveChatInstallResult>;
437
+ /**
438
+ * Computes the visitor identity token:
439
+ * hex(HMAC_SHA256(identitySecret, externalId)). SERVER-ONLY — it
440
+ * throws in a browser context and throws when no secret is
441
+ * provided (explicitly or via BRANDFINE_LIVE_CHAT_IDENTITY_SECRET),
442
+ * rather than ever emitting an unsigned/mis-signed payload.
443
+ *
444
+ * Get the secret from the CMS: Plugins → Live Chat → Manage
445
+ * settings → Integrate → Identity secret. Keep it in server env;
446
+ * shipping it to a browser lets anyone impersonate any visitor.
447
+ */
448
+ identityToken: (externalId: string, opts?: {
449
+ secret?: string;
450
+ }) => Promise<string>;
451
+ };
355
452
  declare function createBrandfineClient(config: BrandfineClientConfig): BrandfineClient;
356
453
 
357
454
  /**
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { createCache, createKeyedCache } from './chunk-DHQHUIFO.js';
2
- export { createBrandfineWebhookHandler, parseWebhookPayload, verifyWebhookSecret } from './chunk-QQLAYITF.js';
3
2
  export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix } from './chunk-U6VJX7PP.js';
3
+ export { createBrandfineWebhookHandler, parseWebhookPayload, verifyWebhookSecret } from './chunk-QQLAYITF.js';
4
4
 
5
5
  // src/client.ts
6
6
  var BrandfineApiError = class extends Error {
@@ -20,6 +20,7 @@ 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";
23
24
  var GTAG_MARKER = "data-brandfine-gtag";
24
25
  function injectGoogleTag(measurementId) {
25
26
  if (typeof document === "undefined") return;
@@ -161,6 +162,76 @@ function createBrandfineClient(config) {
161
162
  return { installed: true, websiteId: cfg.websiteId };
162
163
  }
163
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 (opts.visitor?.externalId && opts.visitor.identityToken) {
187
+ host.setAttribute("data-visitor", JSON.stringify(opts.visitor));
188
+ }
189
+ if (cfg.theme) {
190
+ for (const [key, value] of Object.entries(cfg.theme)) {
191
+ if (key.startsWith("--bf-chat-")) {
192
+ host.style.setProperty(key, value);
193
+ }
194
+ }
195
+ }
196
+ document.body.appendChild(host);
197
+ const script = document.createElement("script");
198
+ script.defer = true;
199
+ script.src = `${baseUrl}${cfg.scriptPath}`;
200
+ script.setAttribute(LIVE_CHAT_MARKER, "script");
201
+ document.head.appendChild(script);
202
+ return { installed: true };
203
+ },
204
+ async identityToken(externalId, opts = {}) {
205
+ if (typeof document !== "undefined" || typeof window !== "undefined") {
206
+ throw new Error(
207
+ "liveChat.identityToken() is server-only \u2014 never compute identity tokens in a browser. Sign the visitor on your server and pass the result to install({ visitor })."
208
+ );
209
+ }
210
+ const secret = opts.secret ?? (typeof process !== "undefined" ? process.env.BRANDFINE_LIVE_CHAT_IDENTITY_SECRET : void 0);
211
+ if (!secret) {
212
+ throw new Error(
213
+ "liveChat.identityToken(): identity secret missing. Pass { secret } or set BRANDFINE_LIVE_CHAT_IDENTITY_SECRET. Generate one in the CMS: Plugins \u2192 Live Chat \u2192 Integrate."
214
+ );
215
+ }
216
+ if (!externalId) {
217
+ throw new Error("liveChat.identityToken(): externalId is required.");
218
+ }
219
+ const enc = new TextEncoder();
220
+ const key = await globalThis.crypto.subtle.importKey(
221
+ "raw",
222
+ enc.encode(secret),
223
+ { name: "HMAC", hash: "SHA-256" },
224
+ false,
225
+ ["sign"]
226
+ );
227
+ const sig = await globalThis.crypto.subtle.sign(
228
+ "HMAC",
229
+ key,
230
+ enc.encode(externalId)
231
+ );
232
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
233
+ }
234
+ };
164
235
  const submissions = {
165
236
  async create(input) {
166
237
  const url = `${baseUrl}/external/submissions`;
@@ -236,7 +307,8 @@ function createBrandfineClient(config) {
236
307
  navigations,
237
308
  analytics,
238
309
  submissions,
239
- appointments
310
+ appointments,
311
+ liveChat
240
312
  };
241
313
  }
242
314
  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 | {\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\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/** 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 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 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"]}
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\n/**\n * A signed visitor identity for Live Chat. Build it SERVER-SIDE:\n * compute `identityToken` with `liveChat.identityToken()` (or your\n * own HMAC_SHA256(identitySecret, externalId), hex) and pass the\n * whole object to `install()` — the widget forwards it verbatim and\n * the Brandfine API verifies the signature. An invalid or missing\n * token silently downgrades the conversation to anonymous.\n */\nexport type LiveChatVisitor = {\n /** Your app's stable id for this person (user id, lead reference…).\n * Conversations sharing an externalId are the same person across\n * devices and sessions. Max 128 chars. */\n externalId: string\n /** Display name shown in the Brandfine inbox. Max 120 chars. */\n name?: string\n /** Max 200 chars. */\n email?: string\n /** Small display-only key→string map (≤10 keys, values ≤100 chars). */\n attributes?: Record<string, string>\n /** hex HMAC_SHA256(identitySecret, externalId) — REQUIRED, computed\n * on your server. Never derive this in a browser. */\n identityToken: string\n}\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 * Already-signed visitor identity (see LiveChatVisitor). Optional —\n * omit for anonymous chat. The object must arrive from your server\n * with `identityToken` precomputed; `install()` only serializes it\n * onto the widget host, it performs no crypto.\n */\n visitor?: LiveChatVisitor\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 * Computes the visitor identity token:\n * hex(HMAC_SHA256(identitySecret, externalId)). SERVER-ONLY — it\n * throws in a browser context and throws when no secret is\n * provided (explicitly or via BRANDFINE_LIVE_CHAT_IDENTITY_SECRET),\n * rather than ever emitting an unsigned/mis-signed payload.\n *\n * Get the secret from the CMS: Plugins → Live Chat → Manage\n * settings → Integrate → Identity secret. Keep it in server env;\n * shipping it to a browser lets anyone impersonate any visitor.\n */\n identityToken: (\n externalId: string,\n opts?: { secret?: string },\n ) => Promise<string>\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 // Signed visitor identity → data-visitor. Serialization only;\n // the signature was computed server-side (identityToken()).\n if (opts.visitor?.externalId && opts.visitor.identityToken) {\n host.setAttribute('data-visitor', JSON.stringify(opts.visitor))\n }\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 async identityToken(externalId, opts = {}) {\n // Refuse to run where the secret could be exfiltrated. A page\n // that needs a token must get it from ITS server, not compute\n // one next to the DOM.\n if (typeof document !== 'undefined' || typeof window !== 'undefined') {\n throw new Error(\n 'liveChat.identityToken() is server-only — never compute identity ' +\n 'tokens in a browser. Sign the visitor on your server and pass ' +\n 'the result to install({ visitor }).',\n )\n }\n const secret =\n opts.secret ??\n (typeof process !== 'undefined'\n ? process.env.BRANDFINE_LIVE_CHAT_IDENTITY_SECRET\n : undefined)\n if (!secret) {\n throw new Error(\n 'liveChat.identityToken(): identity secret missing. Pass ' +\n '{ secret } or set BRANDFINE_LIVE_CHAT_IDENTITY_SECRET. ' +\n 'Generate one in the CMS: Plugins → Live Chat → Integrate.',\n )\n }\n if (!externalId) {\n throw new Error('liveChat.identityToken(): externalId is required.')\n }\n // WebCrypto (Node ≥20 global) — avoids a node:crypto import\n // that would break browser bundling of this dual-target module.\n const enc = new TextEncoder()\n const key = await globalThis.crypto.subtle.importKey(\n 'raw',\n enc.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n )\n const sig = await globalThis.crypto.subtle.sign(\n 'HMAC',\n key,\n enc.encode(externalId),\n )\n return Array.from(new Uint8Array(sig))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\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.8.0",
3
+ "version": "0.10.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",