@brandfine/client 0.6.1 → 0.8.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,73 @@
1
1
  # @brandfine/client
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 351b5ae: `bf.analytics.install()` now also loads the Google tag (gtag.js)
8
+ when the workspace's GA4 property was provisioned through
9
+ Brandfine and the customer opted into tag injection. The
10
+ `AnalyticsConfig` type gains an optional `gaMeasurementId` field
11
+ on both variants. Injection is idempotent and deliberately
12
+ no-ops when any gtag loader is already present on the page —
13
+ a hand-installed Google Analytics setup is never double-tagged.
14
+ Note: gtag sets cookies; consent banners remain the site's
15
+ responsibility.
16
+ - 2d9d037: Add `bf.analytics.overview()` — a composed traffic report for the
17
+ workspace (summary KPIs + bucketed timeseries + top pages + referrer
18
+ sources + visitor countries + device classes) served from the new
19
+ `GET /external/analytics/overview` endpoint. Accepts a
20
+ `range` preset (`'24h' | '7d' | '30d' | '90d'`, default `'7d'`) and
21
+ returns one of three shapes: `{ enabled: false }`, `{ enabled: true,
22
+ verified: false }`, or the full payload. New exported types:
23
+ `AnalyticsOverview`, `AnalyticsOverviewRange`. Intended for
24
+ server-side dashboard rendering — this is the same endpoint the
25
+ WordPress plugin's wp-admin insights panel consumes.
26
+
27
+ ## 0.7.0
28
+
29
+ ### Minor Changes
30
+
31
+ - c62ad80: **Narrows `bf.appointments` to the visitor's actual v1 surface.**
32
+
33
+ The visitor's only browser-side action with Brandfine is the
34
+ initial appointment request submission. Post-submission status
35
+ changes (approve / decline / reschedule) are driven by the
36
+ workspace's customer in the CMS and communicated to the visitor
37
+ by email. The cancel-by-token methods we shipped in 0.6.0
38
+ implied a visitor self-management surface that doesn't exist in
39
+ v1.
40
+
41
+ **Removed** (no published consumers — only a few hours since
42
+ 0.6.0 release):
43
+ - `bf.appointments.getByCancellationToken(token)` —
44
+ visitor-facing lookup, no first-party caller
45
+ - `bf.appointments.cancelByToken(token)` — visitor self-cancel,
46
+ no first-party caller
47
+ - `AppointmentRequestSummary` type — only used by the above
48
+
49
+ **Kept as-is**:
50
+ - `bf.appointments.getAvailability(opts?)` — visitor sees slots
51
+ - `bf.appointments.createRequest(input)` — visitor submits a request
52
+ - `AppointmentSlot`, `AppointmentAvailability`,
53
+ `CreateAppointmentRequestInput`, `CreatedAppointmentRequest`
54
+ types
55
+ - `cancellationToken` field on `CreatedAppointmentRequest` — the
56
+ underlying request row still carries the token server-side for
57
+ possible future reschedule-respond flows; surfaced on the wire
58
+ response for forward compatibility, but treat as opaque /
59
+ ignorable in v1
60
+
61
+ The corresponding `/external/appointments/requests/:token{,/cancel}`
62
+ REST endpoints still exist server-side (token-authed, no key
63
+ required) so a future reschedule-respond flow can re-use them.
64
+ They're documented as "Dormant in v1" in the REST API reference.
65
+
66
+ This is a breaking change in 0.x land per the package's stated
67
+ "API may change in minor bumps before 1.0" policy. The released
68
+ 0.6.0 has been live for ~hours, no consumers shipped against
69
+ the removed methods.
70
+
3
71
  ## 0.6.1
4
72
 
5
73
  ### Patch Changes
package/README.md CHANGED
@@ -66,6 +66,14 @@ await bf.appointments.createRequest({
66
66
 
67
67
  The full surface lives under seven namespaces — `posts`, `categories`, `workspace`, `navigations`, `analytics`, `submissions`, `appointments` — plus a `get(path)` escape hatch.
68
68
 
69
+ ## Server-side only
70
+
71
+ The workspace API key (`bfwk_*`) is **broad-scope** — it can read posts, navigations, submissions, analytics, and write submissions + appointments. **Don't put it in the browser bundle** even behind a `NEXT_PUBLIC_*` / `PUBLIC_*` / `VITE_*` prefix.
72
+
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
+
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).
76
+
69
77
  ## Analytics
70
78
 
71
79
  `bf.analytics.install()` fetches this workspace's tracker config from Brandfine and injects `<script defer src="…" data-website-id="…">` into `document.head` exactly once. Idempotent across StrictMode double-invokes, SPA route changes, and multi-instance scenarios via a marker attribute on the injected tag.
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 chunkFCK7QJBC_cjs = require('./chunk-FCK7QJBC.cjs');
5
4
  var chunkKHHMR2NX_cjs = require('./chunk-KHHMR2NX.cjs');
5
+ var chunkFCK7QJBC_cjs = require('./chunk-FCK7QJBC.cjs');
6
6
 
7
7
  // src/client.ts
8
8
  var BrandfineApiError = class extends Error {
@@ -22,6 +22,26 @@ var BrandfineApiError = class extends Error {
22
22
  }
23
23
  };
24
24
  var INSTALLED_MARKER = "data-brandfine-analytics";
25
+ var GTAG_MARKER = "data-brandfine-gtag";
26
+ function injectGoogleTag(measurementId) {
27
+ if (typeof document === "undefined") return;
28
+ const existing = document.querySelector(
29
+ `script[src*="googletagmanager.com/gtag/js"], script[${GTAG_MARKER}]`
30
+ );
31
+ if (existing) return;
32
+ const loader = document.createElement("script");
33
+ loader.async = true;
34
+ loader.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`;
35
+ loader.setAttribute(GTAG_MARKER, measurementId);
36
+ document.head.appendChild(loader);
37
+ const w = window;
38
+ w.dataLayer = w.dataLayer ?? [];
39
+ function gtag(..._args) {
40
+ w.dataLayer.push(arguments);
41
+ }
42
+ gtag("js", /* @__PURE__ */ new Date());
43
+ gtag("config", measurementId);
44
+ }
25
45
  var DEFAULT_USER_AGENT = "@brandfine/client";
26
46
  function createBrandfineClient(config) {
27
47
  if (!config.baseUrl)
@@ -111,11 +131,20 @@ function createBrandfineClient(config) {
111
131
  getConfig() {
112
132
  return get("/external/analytics-config");
113
133
  },
134
+ overview(opts = {}) {
135
+ const range = opts.range ?? "7d";
136
+ return get(
137
+ `/external/analytics/overview?range=${encodeURIComponent(range)}`
138
+ );
139
+ },
114
140
  async install(opts = {}) {
115
141
  if (typeof document === "undefined") {
116
142
  return { installed: false, reason: "ssr" };
117
143
  }
118
144
  const cfg = opts.config ?? await analytics.getConfig();
145
+ if (cfg.gaMeasurementId) {
146
+ injectGoogleTag(cfg.gaMeasurementId);
147
+ }
119
148
  if (!cfg.enabled) {
120
149
  return { installed: false, reason: "disabled" };
121
150
  }
@@ -199,17 +228,6 @@ function createBrandfineClient(config) {
199
228
  "/external/appointments/requests",
200
229
  input
201
230
  );
202
- },
203
- getByCancellationToken(token) {
204
- return get(
205
- `/external/appointments/requests/${encodeURIComponent(token)}`
206
- );
207
- },
208
- cancelByToken(token) {
209
- return post(
210
- `/external/appointments/requests/${encodeURIComponent(token)}/cancel`,
211
- {}
212
- );
213
231
  }
214
232
  };
215
233
  return {
@@ -238,6 +256,18 @@ Object.defineProperty(exports, "createKeyedCache", {
238
256
  enumerable: true,
239
257
  get: function () { return chunkXJFKL2HU_cjs.createKeyedCache; }
240
258
  });
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
+ });
241
271
  Object.defineProperty(exports, "isLocale", {
242
272
  enumerable: true,
243
273
  get: function () { return chunkFCK7QJBC_cjs.isLocale; }
@@ -258,18 +288,6 @@ Object.defineProperty(exports, "stripLocalePrefix", {
258
288
  enumerable: true,
259
289
  get: function () { return chunkFCK7QJBC_cjs.stripLocalePrefix; }
260
290
  });
261
- Object.defineProperty(exports, "createBrandfineWebhookHandler", {
262
- enumerable: true,
263
- get: function () { return chunkKHHMR2NX_cjs.createBrandfineWebhookHandler; }
264
- });
265
- Object.defineProperty(exports, "parseWebhookPayload", {
266
- enumerable: true,
267
- get: function () { return chunkKHHMR2NX_cjs.parseWebhookPayload; }
268
- });
269
- Object.defineProperty(exports, "verifyWebhookSecret", {
270
- enumerable: true,
271
- get: function () { return chunkKHHMR2NX_cjs.verifyWebhookSecret; }
272
- });
273
291
  exports.BrandfineApiError = BrandfineApiError;
274
292
  exports.SDK_VERSION = SDK_VERSION;
275
293
  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\nexport type AppointmentRequestSummary = {\n id: string\n visitorName: string\n visitorEmail: string\n requestedAt: string\n durationMinutes: number\n status: 'PENDING' | 'CONFIRMED' | 'REJECTED' | 'CANCELLED'\n workspace: { name: string; slug: string }\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. Returns the created\n * row + a one-time cancellation token. Server-side validates\n * the slot is still bookable; if it isn't, throws\n * `BrandfineApiError` with status 404 / 409.\n */\n createRequest: (\n input: CreateAppointmentRequestInput,\n ) => Promise<CreatedAppointmentRequest>\n /**\n * Look up an appointment by its cancellation token. Used by\n * the widget's `?bf-cancel=<token>` URL flow to render a\n * confirmation page before the visitor cancels.\n */\n getByCancellationToken: (\n token: string,\n ) => Promise<AppointmentRequestSummary>\n /**\n * Visitor self-cancellation. The token is revoked after the\n * call succeeds; subsequent calls return 400.\n */\n cancelByToken: (\n token: string,\n ) => Promise<{ id: string; status: 'CANCELLED' }>\n}\n\nexport type AnalyticsConfig =\n | { enabled: false }\n | { enabled: true; websiteId: string; scriptUrl: string }\n\nexport type AnalyticsInstallResult =\n | { installed: false; reason: 'disabled' | 'ssr' | 'already-installed' }\n | { installed: true; websiteId: string }\n\nexport type InstallOptions = {\n /**\n * Pre-known config. When provided, `install()` skips the round-\n * trip to `/external/analytics-config` and injects the script\n * immediately. Use this when you've baked the values into your\n * build (env vars, CMS-side config dump, etc.) — typical for\n * static sites where the analytics state is decided at deploy\n * time, not per page load.\n *\n * Trade-off vs the default fetch path: if you disable analytics\n * in Brandfine, the tracker keeps loading until your next\n * deploy. That's usually the right trade for static sites\n * (which redeploy on every content change anyway) and the wrong\n * trade for dynamic sites where the api round-trip is cheap\n * relative to the rest of the page.\n *\n * Pass `{ enabled: false }` to force a no-op without touching\n * the api (e.g. to disable analytics for one environment without\n * changing Brandfine's state).\n */\n config?: AnalyticsConfig\n}\n\ntype AnalyticsApi = {\n /**\n * Injects the Brandfine analytics tracker into `document.head`\n * once. Safe to call on every page load — idempotent via a\n * marker attribute on the injected script tag.\n *\n * Two paths:\n * - `install()` — fetches the config from Brandfine, then\n * injects. Reflects enable/disable state on next page load.\n * - `install({ config })` — uses caller-provided config, skips\n * the fetch. Faster, no round-trip; ignores Brandfine state\n * changes until the consumer's next deploy.\n *\n * Returns details about what happened:\n * - `{ installed: true, websiteId }` — script was just injected.\n * - `{ installed: false, reason: 'disabled' }` — config says\n * analytics is off; no-op.\n * - `{ installed: false, reason: 'ssr' }` — no `document` in\n * scope (server-side). Call again on the client.\n * - `{ installed: false, reason: 'already-installed' }` — a\n * prior call (or another tab in the same SPA) already injected.\n *\n * Throws `BrandfineApiError` on non-2xx responses other than the\n * disabled case (which is a valid `{ enabled: false }` body).\n */\n install: (opts?: InstallOptions) => Promise<AnalyticsInstallResult>\n\n /** Lower-level helper — fetches the raw config without touching\n * the DOM. Useful when you want to inject the script yourself\n * (e.g. via a framework's <Script> component for nonce/csp). */\n getConfig: () => Promise<AnalyticsConfig>\n}\n\n/** Attribute we stamp on the injected <script> so `install()` is\n * idempotent across re-renders and SPA route changes. */\nconst INSTALLED_MARKER = 'data-brandfine-analytics'\n\nconst DEFAULT_USER_AGENT = '@brandfine/client'\n\nexport function createBrandfineClient(\n config: BrandfineClientConfig,\n): BrandfineClient {\n if (!config.baseUrl)\n throw new Error('createBrandfineClient: `baseUrl` is required')\n if (!config.apiKey)\n throw new Error('createBrandfineClient: `apiKey` is required')\n\n const baseUrl = config.baseUrl.replace(/\\/$/, '')\n const apiKey = config.apiKey\n // Resolve fetch lazily so consumers in environments without a\n // global fetch can polyfill before constructing the client.\n const fetchImpl: typeof fetch = config.fetch ?? globalThis.fetch\n const userAgent = config.userAgent ?? DEFAULT_USER_AGENT\n\n async function get<T>(path: string, opts: RequestOptions = {}): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'GET',\n headers: {\n 'X-Api-Key': apiKey,\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n signal: opts.signal,\n })\n if (res.status === 404 && opts.nullable404) {\n // Drain the body so the underlying socket can be reused —\n // fetch implementations that don't auto-drain (older Node)\n // can leak otherwise.\n await res.text().catch(() => '')\n return null as T\n }\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as T\n }\n\n const posts: PostsApi = {\n async list<TConfig = unknown>(opts: ListPostsOptions = {}) {\n const out: BrandfinePost<TConfig>[] = []\n let page = 1\n const typeQuery = opts.type ? `&type=${encodeURIComponent(opts.type)}` : ''\n const localeQuery = opts.locale\n ? `&locale=${encodeURIComponent(opts.locale)}`\n : ''\n // Default pagination at the cms's 50-per-page cap. `forceLimit`\n // opts past it for content types that would otherwise need\n // many round-trips.\n const sizeQuery = opts.forceLimit\n ? `&force_limit=${opts.forceLimit}`\n : '&limit=50'\n // Pathological safety brake — 200 pages × 50 = 10k posts. If\n // a workspace ever needs more, callers should hit the API\n // directly with their own pagination logic.\n const MAX_PAGES = 200\n while (page <= MAX_PAGES) {\n const data = await get<BrandfinePostListResponse<TConfig>>(\n `/external/posts?include=content${sizeQuery}&page=${page}${typeQuery}${localeQuery}`,\n )\n out.push(...data.items)\n if (!data.pageInfo.hasNext) break\n page += 1\n }\n return out\n },\n async getBySlug<TConfig = unknown>(slug: string) {\n return get<BrandfinePost<TConfig> | null>(\n `/external/posts/${encodeURIComponent(slug)}`,\n { nullable404: true },\n )\n },\n }\n\n const categories: CategoriesApi = {\n async list(opts: ListCategoriesOptions = {}) {\n const qs = opts.locale ? `?locale=${encodeURIComponent(opts.locale)}` : ''\n const data = await get<{ items: BrandfineCategory[] }>(\n `/external/categories${qs}`,\n )\n return data.items\n },\n }\n\n const workspace: WorkspaceApi = {\n get<\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() {\n return get<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>(\n '/external/workspace',\n )\n },\n }\n\n const navigations: NavigationsApi = {\n get<TConfig = unknown>(key: string) {\n return get<BrandfineNavigation<TConfig> | null>(\n `/external/navigations/${encodeURIComponent(key)}`,\n { nullable404: true },\n )\n },\n }\n\n const analytics: AnalyticsApi = {\n getConfig() {\n return get<AnalyticsConfig>('/external/analytics-config')\n },\n async install(opts: InstallOptions = {}) {\n // SSR safety: nothing to inject without a DOM. Consumers\n // call this from useEffect / onMount, but defensive anyway\n // (some frameworks still execute the file body on the server).\n if (typeof document === 'undefined') {\n return { installed: false, reason: 'ssr' as const }\n }\n\n // Use caller-provided config if present (build-time path),\n // otherwise fetch (runtime path).\n const cfg = opts.config ?? (await analytics.getConfig())\n if (!cfg.enabled) {\n return { installed: false, reason: 'disabled' as const }\n }\n\n // Idempotency: a prior call (StrictMode double-invoke, SPA\n // re-mount, second instance with the same workspace) may\n // have already injected. The marker attribute is the source\n // of truth — checking by script src would also miss the case\n // where two workspaces share the same scriptUrl.\n const existing = document.querySelector<HTMLScriptElement>(\n `script[${INSTALLED_MARKER}=\"${cfg.websiteId}\"]`,\n )\n if (existing) {\n return { installed: false, reason: 'already-installed' as const }\n }\n\n const script = document.createElement('script')\n script.defer = true\n script.src = cfg.scriptUrl\n script.setAttribute('data-website-id', cfg.websiteId)\n // The marker doubles as a sentinel + a debug aid (you can\n // grep the DOM for `data-brandfine-analytics` to confirm\n // an install).\n script.setAttribute(INSTALLED_MARKER, cfg.websiteId)\n document.head.appendChild(script)\n return { installed: true, websiteId: cfg.websiteId }\n },\n }\n\n const submissions: SubmissionsApi = {\n async create(input: CreateSubmissionInput) {\n const url = `${baseUrl}/external/submissions`\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n body: JSON.stringify(input),\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as Submission\n },\n }\n\n /**\n * Shared POST helper for the appointments namespace. The main\n * `get()` helper handles GETs; submissions has its own inline\n * POST because it predates this refactor. New plugin namespaces\n * (appointments first, others to follow) share this one so the\n * error-handling shape stays consistent.\n */\n async function post<T>(\n path: string,\n body: unknown,\n opts: RequestOptions = {},\n ): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n body: JSON.stringify(body),\n signal: opts.signal,\n })\n if (!res.ok) {\n const errBody = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body: errBody,\n url,\n })\n }\n if (res.status === 204) return undefined as T\n return (await res.json()) as T\n }\n\n const appointments: AppointmentsApi = {\n getAvailability(opts = {}) {\n const qs: string[] = []\n if (opts.from) qs.push(`from=${encodeURIComponent(toIso(opts.from))}`)\n if (opts.to) qs.push(`to=${encodeURIComponent(toIso(opts.to))}`)\n const suffix = qs.length ? `?${qs.join('&')}` : ''\n return get<AppointmentAvailability>(\n `/external/appointments/availability${suffix}`,\n )\n },\n createRequest(input) {\n return post<CreatedAppointmentRequest>(\n '/external/appointments/requests',\n input,\n )\n },\n getByCancellationToken(token) {\n return get<AppointmentRequestSummary>(\n `/external/appointments/requests/${encodeURIComponent(token)}`,\n )\n },\n cancelByToken(token) {\n return post<{ id: string; status: 'CANCELLED' }>(\n `/external/appointments/requests/${encodeURIComponent(token)}/cancel`,\n {},\n )\n },\n }\n\n return {\n get,\n posts,\n categories,\n workspace,\n navigations,\n analytics,\n submissions,\n appointments,\n }\n}\n\n/** Accepts a Date or an already-ISO string and returns ISO. Saves\n * every caller from `.toISOString()`-ing manually. */\nfunction toIso(d: Date | string): string {\n return typeof d === 'string' ? d : d.toISOString()\n}\n","/**\n * @brandfine/client — root entry.\n *\n * The full SDK surface is exposed here for \"import everything from\n * one place\" usage. Tree-shaking + `sideEffects: false` mean\n * consumers don't pay a bundle cost for what they don't import.\n *\n * Heavier or framework-coupled pieces still live under subpath\n * exports (`@brandfine/client/cache`, `/resolvers`, `/webhook`) so\n * consumers with poor tree-shaking — or who only need one slice —\n * can scope their imports.\n */\n\nexport const SDK_VERSION = '0.0.0' as const\n\nexport {\n BrandfineApiError,\n createBrandfineClient,\n type AnalyticsConfig,\n type AnalyticsInstallResult,\n type BrandfineClient,\n type BrandfineClientConfig,\n type CreateSubmissionInput,\n type InstallOptions,\n type Submission,\n} from './client'\n\nexport {\n createCache,\n createKeyedCache,\n type Cache,\n type CacheOptions,\n type KeyedCache,\n type KeyedCacheOptions,\n} from './cache/index'\n\nexport {\n isLocale,\n localizePath,\n pickLocale,\n resolveNavigation,\n stripLocalePrefix,\n type HydratedNav,\n type HydratedNavItem,\n type LocaleOptions,\n type ResolveNavigationOptions,\n} from './resolvers/index'\n\nexport {\n createBrandfineWebhookHandler,\n parseWebhookPayload,\n verifyWebhookSecret,\n type BrandfineWebhookEvent,\n type BrandfineWebhookHandlerOptions,\n type BrandfineWebhookPayload,\n} from './webhook/index'\n\nexport type {\n BrandfineCategory,\n BrandfineNavItem,\n BrandfineNavItemType,\n BrandfineNavPost,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfinePostTranslation,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `createBrandfineClient` — the SDK's entry point.\n *\n * Returns a stateless, multi-instance-safe handle scoped to a\n * single `(baseUrl, apiKey)` pair. Pattern follows the Stripe /\n * Algolia / OpenAI SDKs — explicit construction with config,\n * namespaced methods (`bf.posts.list(...)`, `bf.workspace.get()`),\n * no module-level singletons.\n *\n * Why factory not module-level state: multi-tenant consumers\n * sometimes need two clients in the same process (e.g. main site\n * + admin preview). Module-level env reading makes that impossible\n * without monkey-patching.\n */\n\nimport type {\n BrandfineCategory,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n\nexport type BrandfineClientConfig = {\n /** Base URL of the Brandfine API. No trailing slash — the client\n * trims one if you pass it anyway. e.g. `https://api.brandfine.co` */\n baseUrl: string\n /** Workspace-scoped API key. Generated from the cms's Workspace\n * settings; identifies which workspace the client talks to. */\n apiKey: string\n /** Optional fetch override. Useful for tests (inject a stub),\n * for runtimes that need a custom implementation (edge workers\n * with non-standard fetch), or to add cross-cutting concerns\n * like tracing / retries. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch\n /** Optional User-Agent header. Falls back to a generic SDK tag. */\n userAgent?: string\n}\n\n/**\n * Structured error thrown by every request helper on non-2xx\n * responses. Carries the raw body so consumers can log it for\n * debugging without re-fetching.\n */\nexport class BrandfineApiError extends Error {\n override readonly name = 'BrandfineApiError'\n readonly status: number\n readonly statusText: string\n readonly body: string\n readonly url: string\n\n constructor(args: {\n status: number\n statusText: string\n body: string\n url: string\n }) {\n super(\n `[brandfine] ${args.status} ${args.statusText} on ${args.url} — ${args.body.slice(0, 200)}`,\n )\n this.status = args.status\n this.statusText = args.statusText\n this.body = args.body\n this.url = args.url\n }\n}\n\ntype RequestOptions = {\n /** When true and the response is 404, return `null` instead of\n * throwing. Used by endpoints where 404 is a meaningful empty\n * state (navigation by key, single post by slug). */\n nullable404?: boolean\n signal?: AbortSignal\n}\n\nexport type BrandfineClient = {\n /** Low-level GET. Reserved for endpoints we don't have a typed\n * helper for yet. Adds the X-Api-Key header automatically. */\n get: <T>(path: string, opts?: RequestOptions) => Promise<T>\n posts: PostsApi\n categories: CategoriesApi\n workspace: WorkspaceApi\n navigations: NavigationsApi\n analytics: AnalyticsApi\n submissions: SubmissionsApi\n appointments: AppointmentsApi\n}\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"]}
package/dist/index.d.cts CHANGED
@@ -172,18 +172,6 @@ type CreatedAppointmentRequest = {
172
172
  * account. One-time use; revoked once any party acts. */
173
173
  cancellationToken: string | null;
174
174
  };
175
- type AppointmentRequestSummary = {
176
- id: string;
177
- visitorName: string;
178
- visitorEmail: string;
179
- requestedAt: string;
180
- durationMinutes: number;
181
- status: 'PENDING' | 'CONFIRMED' | 'REJECTED' | 'CANCELLED';
182
- workspace: {
183
- name: string;
184
- slug: string;
185
- };
186
- };
187
175
  type AppointmentsApi = {
188
176
  /**
189
177
  * Available slots for the workspace's booking window.
@@ -195,33 +183,102 @@ type AppointmentsApi = {
195
183
  to?: Date | string;
196
184
  }) => Promise<AppointmentAvailability>;
197
185
  /**
198
- * Submit a visitor's appointment request. Returns the created
199
- * row + a one-time cancellation token. Server-side validates
186
+ * Submit a visitor's appointment request. Server-side validates
200
187
  * the slot is still bookable; if it isn't, throws
201
188
  * `BrandfineApiError` with status 404 / 409.
189
+ *
190
+ * The visitor's browser does not have any other appointment
191
+ * actions in v1 — post-submission status changes (approve /
192
+ * decline / reschedule) happen via email, driven by the
193
+ * customer in the CMS.
202
194
  */
203
195
  createRequest: (input: CreateAppointmentRequestInput) => Promise<CreatedAppointmentRequest>;
204
- /**
205
- * Look up an appointment by its cancellation token. Used by
206
- * the widget's `?bf-cancel=<token>` URL flow to render a
207
- * confirmation page before the visitor cancels.
208
- */
209
- getByCancellationToken: (token: string) => Promise<AppointmentRequestSummary>;
210
- /**
211
- * Visitor self-cancellation. The token is revoked after the
212
- * call succeeds; subsequent calls return 400.
213
- */
214
- cancelByToken: (token: string) => Promise<{
215
- id: string;
216
- status: 'CANCELLED';
217
- }>;
218
196
  };
219
197
  type AnalyticsConfig = {
220
198
  enabled: false;
199
+ /** GA4 Measurement ID — present when the workspace's Google
200
+ * Analytics property was provisioned through Brandfine AND
201
+ * the customer opted into tag injection. `install()` loads
202
+ * gtag for it. Note gtag sets cookies: consent banners are
203
+ * your site's responsibility. */
204
+ gaMeasurementId?: string;
221
205
  } | {
222
206
  enabled: true;
223
207
  websiteId: string;
224
208
  scriptUrl: string;
209
+ gaMeasurementId?: string;
210
+ };
211
+ type AnalyticsOverviewRange = '24h' | '7d' | '30d' | '90d';
212
+ /**
213
+ * Composed traffic report for the workspace — summary KPIs +
214
+ * bucketed chart data + top pages in one payload. Mirrors
215
+ * `GET /external/analytics/overview` (see the API's
216
+ * `ExternalAnalyticsOverview` type); additive changes only.
217
+ *
218
+ * Three shapes to handle:
219
+ * - `{ enabled: false }` — analytics never enabled for the
220
+ * workspace. Show an enable CTA.
221
+ * - `{ enabled: true, verified: false }` — tracker provisioned
222
+ * but no pageview recorded yet. Show "waiting for first visit".
223
+ * - full payload — render the dashboard.
224
+ */
225
+ type AnalyticsOverview = {
226
+ enabled: false;
227
+ } | {
228
+ enabled: true;
229
+ verified: false;
230
+ } | {
231
+ enabled: true;
232
+ verified: true;
233
+ range: AnalyticsOverviewRange;
234
+ summary: {
235
+ visitors: number;
236
+ /** Fractional change vs the prior window (0.12 = +12%). */
237
+ visitorsChange: number;
238
+ pageviews: number;
239
+ pageviewsChange: number;
240
+ visits: number;
241
+ visitsChange: number;
242
+ /** 0..1 fraction. */
243
+ bounceRate: number;
244
+ bounceRateChange: number;
245
+ avgVisitSeconds: number;
246
+ avgVisitSecondsChange: number;
247
+ /** Visitors active in the last ~5 minutes. */
248
+ activeNow: number;
249
+ };
250
+ /** Bucketed chart data, oldest → newest. Hourly buckets for
251
+ * `24h`, daily otherwise. `t` is an ISO-8601 bucket start. */
252
+ timeseries: Array<{
253
+ t: string;
254
+ visitors: number;
255
+ pageviews: number;
256
+ }>;
257
+ /** Top 10 paths by views in the window. */
258
+ topPages: Array<{
259
+ path: string;
260
+ views: number;
261
+ visitors: number;
262
+ }>;
263
+ /** Top 10 referrer sources by visitors. Empty-string source
264
+ * means direct traffic. */
265
+ sources: Array<{
266
+ source: string;
267
+ visitors: number;
268
+ }>;
269
+ /** Top 10 visitor countries (ISO 3166-1 alpha-2 codes —
270
+ * map to display names on your side, e.g. via
271
+ * `Intl.DisplayNames`). */
272
+ countries: Array<{
273
+ country: string;
274
+ visitors: number;
275
+ }>;
276
+ /** Visitors by device class (`desktop` / `mobile` /
277
+ * `tablet` / …). */
278
+ devices: Array<{
279
+ device: string;
280
+ visitors: number;
281
+ }>;
225
282
  };
226
283
  type AnalyticsInstallResult = {
227
284
  installed: false;
@@ -282,6 +339,18 @@ type AnalyticsApi = {
282
339
  * the DOM. Useful when you want to inject the script yourself
283
340
  * (e.g. via a framework's <Script> component for nonce/csp). */
284
341
  getConfig: () => Promise<AnalyticsConfig>;
342
+ /**
343
+ * Traffic report for the workspace — summary KPIs, bucketed
344
+ * timeseries for charting, and top pages, in one round-trip.
345
+ * This is a server-to-server read (it returns your site's
346
+ * traffic data); call it from your backend or build step, not
347
+ * from visitor-facing browser code.
348
+ *
349
+ * @param opts.range Window preset. Defaults to `'7d'`.
350
+ */
351
+ overview: (opts?: {
352
+ range?: AnalyticsOverviewRange;
353
+ }) => Promise<AnalyticsOverview>;
285
354
  };
286
355
  declare function createBrandfineClient(config: BrandfineClientConfig): BrandfineClient;
287
356
 
@@ -299,4 +368,4 @@ declare function createBrandfineClient(config: BrandfineClientConfig): Brandfine
299
368
  */
300
369
  declare const SDK_VERSION: "0.0.0";
301
370
 
302
- export { type AnalyticsConfig, type AnalyticsInstallResult, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type CreateSubmissionInput, type InstallOptions, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, type Submission, createBrandfineClient };
371
+ export { type AnalyticsConfig, type AnalyticsInstallResult, type AnalyticsOverview, type AnalyticsOverviewRange, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type CreateSubmissionInput, type InstallOptions, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, type Submission, createBrandfineClient };
package/dist/index.d.ts CHANGED
@@ -172,18 +172,6 @@ type CreatedAppointmentRequest = {
172
172
  * account. One-time use; revoked once any party acts. */
173
173
  cancellationToken: string | null;
174
174
  };
175
- type AppointmentRequestSummary = {
176
- id: string;
177
- visitorName: string;
178
- visitorEmail: string;
179
- requestedAt: string;
180
- durationMinutes: number;
181
- status: 'PENDING' | 'CONFIRMED' | 'REJECTED' | 'CANCELLED';
182
- workspace: {
183
- name: string;
184
- slug: string;
185
- };
186
- };
187
175
  type AppointmentsApi = {
188
176
  /**
189
177
  * Available slots for the workspace's booking window.
@@ -195,33 +183,102 @@ type AppointmentsApi = {
195
183
  to?: Date | string;
196
184
  }) => Promise<AppointmentAvailability>;
197
185
  /**
198
- * Submit a visitor's appointment request. Returns the created
199
- * row + a one-time cancellation token. Server-side validates
186
+ * Submit a visitor's appointment request. Server-side validates
200
187
  * the slot is still bookable; if it isn't, throws
201
188
  * `BrandfineApiError` with status 404 / 409.
189
+ *
190
+ * The visitor's browser does not have any other appointment
191
+ * actions in v1 — post-submission status changes (approve /
192
+ * decline / reschedule) happen via email, driven by the
193
+ * customer in the CMS.
202
194
  */
203
195
  createRequest: (input: CreateAppointmentRequestInput) => Promise<CreatedAppointmentRequest>;
204
- /**
205
- * Look up an appointment by its cancellation token. Used by
206
- * the widget's `?bf-cancel=<token>` URL flow to render a
207
- * confirmation page before the visitor cancels.
208
- */
209
- getByCancellationToken: (token: string) => Promise<AppointmentRequestSummary>;
210
- /**
211
- * Visitor self-cancellation. The token is revoked after the
212
- * call succeeds; subsequent calls return 400.
213
- */
214
- cancelByToken: (token: string) => Promise<{
215
- id: string;
216
- status: 'CANCELLED';
217
- }>;
218
196
  };
219
197
  type AnalyticsConfig = {
220
198
  enabled: false;
199
+ /** GA4 Measurement ID — present when the workspace's Google
200
+ * Analytics property was provisioned through Brandfine AND
201
+ * the customer opted into tag injection. `install()` loads
202
+ * gtag for it. Note gtag sets cookies: consent banners are
203
+ * your site's responsibility. */
204
+ gaMeasurementId?: string;
221
205
  } | {
222
206
  enabled: true;
223
207
  websiteId: string;
224
208
  scriptUrl: string;
209
+ gaMeasurementId?: string;
210
+ };
211
+ type AnalyticsOverviewRange = '24h' | '7d' | '30d' | '90d';
212
+ /**
213
+ * Composed traffic report for the workspace — summary KPIs +
214
+ * bucketed chart data + top pages in one payload. Mirrors
215
+ * `GET /external/analytics/overview` (see the API's
216
+ * `ExternalAnalyticsOverview` type); additive changes only.
217
+ *
218
+ * Three shapes to handle:
219
+ * - `{ enabled: false }` — analytics never enabled for the
220
+ * workspace. Show an enable CTA.
221
+ * - `{ enabled: true, verified: false }` — tracker provisioned
222
+ * but no pageview recorded yet. Show "waiting for first visit".
223
+ * - full payload — render the dashboard.
224
+ */
225
+ type AnalyticsOverview = {
226
+ enabled: false;
227
+ } | {
228
+ enabled: true;
229
+ verified: false;
230
+ } | {
231
+ enabled: true;
232
+ verified: true;
233
+ range: AnalyticsOverviewRange;
234
+ summary: {
235
+ visitors: number;
236
+ /** Fractional change vs the prior window (0.12 = +12%). */
237
+ visitorsChange: number;
238
+ pageviews: number;
239
+ pageviewsChange: number;
240
+ visits: number;
241
+ visitsChange: number;
242
+ /** 0..1 fraction. */
243
+ bounceRate: number;
244
+ bounceRateChange: number;
245
+ avgVisitSeconds: number;
246
+ avgVisitSecondsChange: number;
247
+ /** Visitors active in the last ~5 minutes. */
248
+ activeNow: number;
249
+ };
250
+ /** Bucketed chart data, oldest → newest. Hourly buckets for
251
+ * `24h`, daily otherwise. `t` is an ISO-8601 bucket start. */
252
+ timeseries: Array<{
253
+ t: string;
254
+ visitors: number;
255
+ pageviews: number;
256
+ }>;
257
+ /** Top 10 paths by views in the window. */
258
+ topPages: Array<{
259
+ path: string;
260
+ views: number;
261
+ visitors: number;
262
+ }>;
263
+ /** Top 10 referrer sources by visitors. Empty-string source
264
+ * means direct traffic. */
265
+ sources: Array<{
266
+ source: string;
267
+ visitors: number;
268
+ }>;
269
+ /** Top 10 visitor countries (ISO 3166-1 alpha-2 codes —
270
+ * map to display names on your side, e.g. via
271
+ * `Intl.DisplayNames`). */
272
+ countries: Array<{
273
+ country: string;
274
+ visitors: number;
275
+ }>;
276
+ /** Visitors by device class (`desktop` / `mobile` /
277
+ * `tablet` / …). */
278
+ devices: Array<{
279
+ device: string;
280
+ visitors: number;
281
+ }>;
225
282
  };
226
283
  type AnalyticsInstallResult = {
227
284
  installed: false;
@@ -282,6 +339,18 @@ type AnalyticsApi = {
282
339
  * the DOM. Useful when you want to inject the script yourself
283
340
  * (e.g. via a framework's <Script> component for nonce/csp). */
284
341
  getConfig: () => Promise<AnalyticsConfig>;
342
+ /**
343
+ * Traffic report for the workspace — summary KPIs, bucketed
344
+ * timeseries for charting, and top pages, in one round-trip.
345
+ * This is a server-to-server read (it returns your site's
346
+ * traffic data); call it from your backend or build step, not
347
+ * from visitor-facing browser code.
348
+ *
349
+ * @param opts.range Window preset. Defaults to `'7d'`.
350
+ */
351
+ overview: (opts?: {
352
+ range?: AnalyticsOverviewRange;
353
+ }) => Promise<AnalyticsOverview>;
285
354
  };
286
355
  declare function createBrandfineClient(config: BrandfineClientConfig): BrandfineClient;
287
356
 
@@ -299,4 +368,4 @@ declare function createBrandfineClient(config: BrandfineClientConfig): Brandfine
299
368
  */
300
369
  declare const SDK_VERSION: "0.0.0";
301
370
 
302
- export { type AnalyticsConfig, type AnalyticsInstallResult, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type CreateSubmissionInput, type InstallOptions, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, type Submission, createBrandfineClient };
371
+ export { type AnalyticsConfig, type AnalyticsInstallResult, type AnalyticsOverview, type AnalyticsOverviewRange, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type CreateSubmissionInput, type InstallOptions, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, type Submission, createBrandfineClient };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { createCache, createKeyedCache } from './chunk-DHQHUIFO.js';
2
- export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix } from './chunk-U6VJX7PP.js';
3
2
  export { createBrandfineWebhookHandler, parseWebhookPayload, verifyWebhookSecret } from './chunk-QQLAYITF.js';
3
+ export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix } from './chunk-U6VJX7PP.js';
4
4
 
5
5
  // src/client.ts
6
6
  var BrandfineApiError = class extends Error {
@@ -20,6 +20,26 @@ var BrandfineApiError = class extends Error {
20
20
  }
21
21
  };
22
22
  var INSTALLED_MARKER = "data-brandfine-analytics";
23
+ var GTAG_MARKER = "data-brandfine-gtag";
24
+ function injectGoogleTag(measurementId) {
25
+ if (typeof document === "undefined") return;
26
+ const existing = document.querySelector(
27
+ `script[src*="googletagmanager.com/gtag/js"], script[${GTAG_MARKER}]`
28
+ );
29
+ if (existing) return;
30
+ const loader = document.createElement("script");
31
+ loader.async = true;
32
+ loader.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`;
33
+ loader.setAttribute(GTAG_MARKER, measurementId);
34
+ document.head.appendChild(loader);
35
+ const w = window;
36
+ w.dataLayer = w.dataLayer ?? [];
37
+ function gtag(..._args) {
38
+ w.dataLayer.push(arguments);
39
+ }
40
+ gtag("js", /* @__PURE__ */ new Date());
41
+ gtag("config", measurementId);
42
+ }
23
43
  var DEFAULT_USER_AGENT = "@brandfine/client";
24
44
  function createBrandfineClient(config) {
25
45
  if (!config.baseUrl)
@@ -109,11 +129,20 @@ function createBrandfineClient(config) {
109
129
  getConfig() {
110
130
  return get("/external/analytics-config");
111
131
  },
132
+ overview(opts = {}) {
133
+ const range = opts.range ?? "7d";
134
+ return get(
135
+ `/external/analytics/overview?range=${encodeURIComponent(range)}`
136
+ );
137
+ },
112
138
  async install(opts = {}) {
113
139
  if (typeof document === "undefined") {
114
140
  return { installed: false, reason: "ssr" };
115
141
  }
116
142
  const cfg = opts.config ?? await analytics.getConfig();
143
+ if (cfg.gaMeasurementId) {
144
+ injectGoogleTag(cfg.gaMeasurementId);
145
+ }
117
146
  if (!cfg.enabled) {
118
147
  return { installed: false, reason: "disabled" };
119
148
  }
@@ -197,17 +226,6 @@ function createBrandfineClient(config) {
197
226
  "/external/appointments/requests",
198
227
  input
199
228
  );
200
- },
201
- getByCancellationToken(token) {
202
- return get(
203
- `/external/appointments/requests/${encodeURIComponent(token)}`
204
- );
205
- },
206
- cancelByToken(token) {
207
- return post(
208
- `/external/appointments/requests/${encodeURIComponent(token)}/cancel`,
209
- {}
210
- );
211
229
  }
212
230
  };
213
231
  return {
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\nexport type AppointmentRequestSummary = {\n id: string\n visitorName: string\n visitorEmail: string\n requestedAt: string\n durationMinutes: number\n status: 'PENDING' | 'CONFIRMED' | 'REJECTED' | 'CANCELLED'\n workspace: { name: string; slug: string }\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. Returns the created\n * row + a one-time cancellation token. Server-side validates\n * the slot is still bookable; if it isn't, throws\n * `BrandfineApiError` with status 404 / 409.\n */\n createRequest: (\n input: CreateAppointmentRequestInput,\n ) => Promise<CreatedAppointmentRequest>\n /**\n * Look up an appointment by its cancellation token. Used by\n * the widget's `?bf-cancel=<token>` URL flow to render a\n * confirmation page before the visitor cancels.\n */\n getByCancellationToken: (\n token: string,\n ) => Promise<AppointmentRequestSummary>\n /**\n * Visitor self-cancellation. The token is revoked after the\n * call succeeds; subsequent calls return 400.\n */\n cancelByToken: (\n token: string,\n ) => Promise<{ id: string; status: 'CANCELLED' }>\n}\n\nexport type AnalyticsConfig =\n | { enabled: false }\n | { enabled: true; websiteId: string; scriptUrl: string }\n\nexport type AnalyticsInstallResult =\n | { installed: false; reason: 'disabled' | 'ssr' | 'already-installed' }\n | { installed: true; websiteId: string }\n\nexport type InstallOptions = {\n /**\n * Pre-known config. When provided, `install()` skips the round-\n * trip to `/external/analytics-config` and injects the script\n * immediately. Use this when you've baked the values into your\n * build (env vars, CMS-side config dump, etc.) — typical for\n * static sites where the analytics state is decided at deploy\n * time, not per page load.\n *\n * Trade-off vs the default fetch path: if you disable analytics\n * in Brandfine, the tracker keeps loading until your next\n * deploy. That's usually the right trade for static sites\n * (which redeploy on every content change anyway) and the wrong\n * trade for dynamic sites where the api round-trip is cheap\n * relative to the rest of the page.\n *\n * Pass `{ enabled: false }` to force a no-op without touching\n * the api (e.g. to disable analytics for one environment without\n * changing Brandfine's state).\n */\n config?: AnalyticsConfig\n}\n\ntype AnalyticsApi = {\n /**\n * Injects the Brandfine analytics tracker into `document.head`\n * once. Safe to call on every page load — idempotent via a\n * marker attribute on the injected script tag.\n *\n * Two paths:\n * - `install()` — fetches the config from Brandfine, then\n * injects. Reflects enable/disable state on next page load.\n * - `install({ config })` — uses caller-provided config, skips\n * the fetch. Faster, no round-trip; ignores Brandfine state\n * changes until the consumer's next deploy.\n *\n * Returns details about what happened:\n * - `{ installed: true, websiteId }` — script was just injected.\n * - `{ installed: false, reason: 'disabled' }` — config says\n * analytics is off; no-op.\n * - `{ installed: false, reason: 'ssr' }` — no `document` in\n * scope (server-side). Call again on the client.\n * - `{ installed: false, reason: 'already-installed' }` — a\n * prior call (or another tab in the same SPA) already injected.\n *\n * Throws `BrandfineApiError` on non-2xx responses other than the\n * disabled case (which is a valid `{ enabled: false }` body).\n */\n install: (opts?: InstallOptions) => Promise<AnalyticsInstallResult>\n\n /** Lower-level helper — fetches the raw config without touching\n * the DOM. Useful when you want to inject the script yourself\n * (e.g. via a framework's <Script> component for nonce/csp). */\n getConfig: () => Promise<AnalyticsConfig>\n}\n\n/** Attribute we stamp on the injected <script> so `install()` is\n * idempotent across re-renders and SPA route changes. */\nconst INSTALLED_MARKER = 'data-brandfine-analytics'\n\nconst DEFAULT_USER_AGENT = '@brandfine/client'\n\nexport function createBrandfineClient(\n config: BrandfineClientConfig,\n): BrandfineClient {\n if (!config.baseUrl)\n throw new Error('createBrandfineClient: `baseUrl` is required')\n if (!config.apiKey)\n throw new Error('createBrandfineClient: `apiKey` is required')\n\n const baseUrl = config.baseUrl.replace(/\\/$/, '')\n const apiKey = config.apiKey\n // Resolve fetch lazily so consumers in environments without a\n // global fetch can polyfill before constructing the client.\n const fetchImpl: typeof fetch = config.fetch ?? globalThis.fetch\n const userAgent = config.userAgent ?? DEFAULT_USER_AGENT\n\n async function get<T>(path: string, opts: RequestOptions = {}): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'GET',\n headers: {\n 'X-Api-Key': apiKey,\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n signal: opts.signal,\n })\n if (res.status === 404 && opts.nullable404) {\n // Drain the body so the underlying socket can be reused —\n // fetch implementations that don't auto-drain (older Node)\n // can leak otherwise.\n await res.text().catch(() => '')\n return null as T\n }\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as T\n }\n\n const posts: PostsApi = {\n async list<TConfig = unknown>(opts: ListPostsOptions = {}) {\n const out: BrandfinePost<TConfig>[] = []\n let page = 1\n const typeQuery = opts.type ? `&type=${encodeURIComponent(opts.type)}` : ''\n const localeQuery = opts.locale\n ? `&locale=${encodeURIComponent(opts.locale)}`\n : ''\n // Default pagination at the cms's 50-per-page cap. `forceLimit`\n // opts past it for content types that would otherwise need\n // many round-trips.\n const sizeQuery = opts.forceLimit\n ? `&force_limit=${opts.forceLimit}`\n : '&limit=50'\n // Pathological safety brake — 200 pages × 50 = 10k posts. If\n // a workspace ever needs more, callers should hit the API\n // directly with their own pagination logic.\n const MAX_PAGES = 200\n while (page <= MAX_PAGES) {\n const data = await get<BrandfinePostListResponse<TConfig>>(\n `/external/posts?include=content${sizeQuery}&page=${page}${typeQuery}${localeQuery}`,\n )\n out.push(...data.items)\n if (!data.pageInfo.hasNext) break\n page += 1\n }\n return out\n },\n async getBySlug<TConfig = unknown>(slug: string) {\n return get<BrandfinePost<TConfig> | null>(\n `/external/posts/${encodeURIComponent(slug)}`,\n { nullable404: true },\n )\n },\n }\n\n const categories: CategoriesApi = {\n async list(opts: ListCategoriesOptions = {}) {\n const qs = opts.locale ? `?locale=${encodeURIComponent(opts.locale)}` : ''\n const data = await get<{ items: BrandfineCategory[] }>(\n `/external/categories${qs}`,\n )\n return data.items\n },\n }\n\n const workspace: WorkspaceApi = {\n get<\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() {\n return get<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>(\n '/external/workspace',\n )\n },\n }\n\n const navigations: NavigationsApi = {\n get<TConfig = unknown>(key: string) {\n return get<BrandfineNavigation<TConfig> | null>(\n `/external/navigations/${encodeURIComponent(key)}`,\n { nullable404: true },\n )\n },\n }\n\n const analytics: AnalyticsApi = {\n getConfig() {\n return get<AnalyticsConfig>('/external/analytics-config')\n },\n async install(opts: InstallOptions = {}) {\n // SSR safety: nothing to inject without a DOM. Consumers\n // call this from useEffect / onMount, but defensive anyway\n // (some frameworks still execute the file body on the server).\n if (typeof document === 'undefined') {\n return { installed: false, reason: 'ssr' as const }\n }\n\n // Use caller-provided config if present (build-time path),\n // otherwise fetch (runtime path).\n const cfg = opts.config ?? (await analytics.getConfig())\n if (!cfg.enabled) {\n return { installed: false, reason: 'disabled' as const }\n }\n\n // Idempotency: a prior call (StrictMode double-invoke, SPA\n // re-mount, second instance with the same workspace) may\n // have already injected. The marker attribute is the source\n // of truth — checking by script src would also miss the case\n // where two workspaces share the same scriptUrl.\n const existing = document.querySelector<HTMLScriptElement>(\n `script[${INSTALLED_MARKER}=\"${cfg.websiteId}\"]`,\n )\n if (existing) {\n return { installed: false, reason: 'already-installed' as const }\n }\n\n const script = document.createElement('script')\n script.defer = true\n script.src = cfg.scriptUrl\n script.setAttribute('data-website-id', cfg.websiteId)\n // The marker doubles as a sentinel + a debug aid (you can\n // grep the DOM for `data-brandfine-analytics` to confirm\n // an install).\n script.setAttribute(INSTALLED_MARKER, cfg.websiteId)\n document.head.appendChild(script)\n return { installed: true, websiteId: cfg.websiteId }\n },\n }\n\n const submissions: SubmissionsApi = {\n async create(input: CreateSubmissionInput) {\n const url = `${baseUrl}/external/submissions`\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n body: JSON.stringify(input),\n })\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as Submission\n },\n }\n\n /**\n * Shared POST helper for the appointments namespace. The main\n * `get()` helper handles GETs; submissions has its own inline\n * POST because it predates this refactor. New plugin namespaces\n * (appointments first, others to follow) share this one so the\n * error-handling shape stays consistent.\n */\n async function post<T>(\n path: string,\n body: unknown,\n opts: RequestOptions = {},\n ): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n body: JSON.stringify(body),\n signal: opts.signal,\n })\n if (!res.ok) {\n const errBody = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body: errBody,\n url,\n })\n }\n if (res.status === 204) return undefined as T\n return (await res.json()) as T\n }\n\n const appointments: AppointmentsApi = {\n getAvailability(opts = {}) {\n const qs: string[] = []\n if (opts.from) qs.push(`from=${encodeURIComponent(toIso(opts.from))}`)\n if (opts.to) qs.push(`to=${encodeURIComponent(toIso(opts.to))}`)\n const suffix = qs.length ? `?${qs.join('&')}` : ''\n return get<AppointmentAvailability>(\n `/external/appointments/availability${suffix}`,\n )\n },\n createRequest(input) {\n return post<CreatedAppointmentRequest>(\n '/external/appointments/requests',\n input,\n )\n },\n getByCancellationToken(token) {\n return get<AppointmentRequestSummary>(\n `/external/appointments/requests/${encodeURIComponent(token)}`,\n )\n },\n cancelByToken(token) {\n return post<{ id: string; status: 'CANCELLED' }>(\n `/external/appointments/requests/${encodeURIComponent(token)}/cancel`,\n {},\n )\n },\n }\n\n return {\n get,\n posts,\n categories,\n workspace,\n navigations,\n analytics,\n submissions,\n appointments,\n }\n}\n\n/** Accepts a Date or an already-ISO string and returns ISO. Saves\n * every caller from `.toISOString()`-ing manually. */\nfunction toIso(d: Date | string): string {\n return typeof d === 'string' ? d : d.toISOString()\n}\n","/**\n * @brandfine/client — root entry.\n *\n * The full SDK surface is exposed here for \"import everything from\n * one place\" usage. Tree-shaking + `sideEffects: false` mean\n * consumers don't pay a bundle cost for what they don't import.\n *\n * Heavier or framework-coupled pieces still live under subpath\n * exports (`@brandfine/client/cache`, `/resolvers`, `/webhook`) so\n * consumers with poor tree-shaking — or who only need one slice —\n * can scope their imports.\n */\n\nexport const SDK_VERSION = '0.0.0' as const\n\nexport {\n BrandfineApiError,\n createBrandfineClient,\n type AnalyticsConfig,\n type AnalyticsInstallResult,\n type BrandfineClient,\n type BrandfineClientConfig,\n type CreateSubmissionInput,\n type InstallOptions,\n type Submission,\n} from './client'\n\nexport {\n createCache,\n createKeyedCache,\n type Cache,\n type CacheOptions,\n type KeyedCache,\n type KeyedCacheOptions,\n} from './cache/index'\n\nexport {\n isLocale,\n localizePath,\n pickLocale,\n resolveNavigation,\n stripLocalePrefix,\n type HydratedNav,\n type HydratedNavItem,\n type LocaleOptions,\n type ResolveNavigationOptions,\n} from './resolvers/index'\n\nexport {\n createBrandfineWebhookHandler,\n parseWebhookPayload,\n verifyWebhookSecret,\n type BrandfineWebhookEvent,\n type BrandfineWebhookHandlerOptions,\n type BrandfineWebhookPayload,\n} from './webhook/index'\n\nexport type {\n BrandfineCategory,\n BrandfineNavItem,\n BrandfineNavItemType,\n BrandfineNavPost,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfinePostTranslation,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `createBrandfineClient` — the SDK's entry point.\n *\n * Returns a stateless, multi-instance-safe handle scoped to a\n * single `(baseUrl, apiKey)` pair. Pattern follows the Stripe /\n * Algolia / OpenAI SDKs — explicit construction with config,\n * namespaced methods (`bf.posts.list(...)`, `bf.workspace.get()`),\n * no module-level singletons.\n *\n * Why factory not module-level state: multi-tenant consumers\n * sometimes need two clients in the same process (e.g. main site\n * + admin preview). Module-level env reading makes that impossible\n * without monkey-patching.\n */\n\nimport type {\n BrandfineCategory,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n\nexport type BrandfineClientConfig = {\n /** Base URL of the Brandfine API. No trailing slash — the client\n * trims one if you pass it anyway. e.g. `https://api.brandfine.co` */\n baseUrl: string\n /** Workspace-scoped API key. Generated from the cms's Workspace\n * settings; identifies which workspace the client talks to. */\n apiKey: string\n /** Optional fetch override. Useful for tests (inject a stub),\n * for runtimes that need a custom implementation (edge workers\n * with non-standard fetch), or to add cross-cutting concerns\n * like tracing / retries. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch\n /** Optional User-Agent header. Falls back to a generic SDK tag. */\n userAgent?: string\n}\n\n/**\n * Structured error thrown by every request helper on non-2xx\n * responses. Carries the raw body so consumers can log it for\n * debugging without re-fetching.\n */\nexport class BrandfineApiError extends Error {\n override readonly name = 'BrandfineApiError'\n readonly status: number\n readonly statusText: string\n readonly body: string\n readonly url: string\n\n constructor(args: {\n status: number\n statusText: string\n body: string\n url: string\n }) {\n super(\n `[brandfine] ${args.status} ${args.statusText} on ${args.url} — ${args.body.slice(0, 200)}`,\n )\n this.status = args.status\n this.statusText = args.statusText\n this.body = args.body\n this.url = args.url\n }\n}\n\ntype RequestOptions = {\n /** When true and the response is 404, return `null` instead of\n * throwing. Used by endpoints where 404 is a meaningful empty\n * state (navigation by key, single post by slug). */\n nullable404?: boolean\n signal?: AbortSignal\n}\n\nexport type BrandfineClient = {\n /** Low-level GET. Reserved for endpoints we don't have a typed\n * helper for yet. Adds the X-Api-Key header automatically. */\n get: <T>(path: string, opts?: RequestOptions) => Promise<T>\n posts: PostsApi\n categories: CategoriesApi\n workspace: WorkspaceApi\n navigations: NavigationsApi\n analytics: AnalyticsApi\n submissions: SubmissionsApi\n appointments: AppointmentsApi\n}\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandfine/client",
3
- "version": "0.6.1",
3
+ "version": "0.8.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",