@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 +68 -0
- package/README.md +8 -0
- package/dist/index.cjs +42 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +98 -29
- package/dist/index.d.ts +98 -29
- package/dist/index.js +30 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/index.ts"],"names":[],"mappings":";;;;;;;AA8CO,IAAM,iBAAA,GAAN,cAAgC,KAAA,CAAM;AAAA,EACzB,IAAA,GAAO,mBAAA;AAAA,EAChB,MAAA;AAAA,EACA,UAAA;AAAA,EACA,IAAA;AAAA,EACA,GAAA;AAAA,EAET,YAAY,IAAA,EAKT;AACD,IAAA,KAAA;AAAA,MACE,CAAA,YAAA,EAAe,IAAA,CAAK,MAAM,CAAA,CAAA,EAAI,KAAK,UAAU,CAAA,IAAA,EAAO,IAAA,CAAK,GAAG,WAAM,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,GAAG,CAAC,CAAA;AAAA,KAC3F;AACA,IAAA,IAAA,CAAK,SAAS,IAAA,CAAK,MAAA;AACnB,IAAA,IAAA,CAAK,aAAa,IAAA,CAAK,UAAA;AACvB,IAAA,IAAA,CAAK,OAAO,IAAA,CAAK,IAAA;AACjB,IAAA,IAAA,CAAK,MAAM,IAAA,CAAK,GAAA;AAAA,EAClB;AACF;AAyQA,IAAM,gBAAA,GAAmB,0BAAA;AAEzB,IAAM,kBAAA,GAAqB,mBAAA;AAEpB,SAAS,sBACd,MAAA,EACiB;AACjB,EAAA,IAAI,CAAC,MAAA,CAAO,OAAA;AACV,IAAA,MAAM,IAAI,MAAM,8CAA8C,CAAA;AAChE,EAAA,IAAI,CAAC,MAAA,CAAO,MAAA;AACV,IAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAE/D,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,OAAA,CAAQ,OAAO,EAAE,CAAA;AAChD,EAAA,MAAM,SAAS,MAAA,CAAO,MAAA;AAGtB,EAAA,MAAM,SAAA,GAA0B,MAAA,CAAO,KAAA,IAAS,UAAA,CAAW,KAAA;AAC3D,EAAA,MAAM,SAAA,GAAY,OAAO,SAAA,IAAa,kBAAA;AAEtC,EAAA,eAAe,GAAA,CAAO,IAAA,EAAc,IAAA,GAAuB,EAAC,EAAe;AACzE,IAAA,MAAM,GAAA,GAAM,CAAA,EAAG,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA;AAC7B,IAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,GAAA,EAAK;AAAA,MAC/B,MAAA,EAAQ,KAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,WAAA,EAAa,MAAA;AAAA,QACb,MAAA,EAAQ,kBAAA;AAAA,QACR,YAAA,EAAc;AAAA,OAChB;AAAA,MACA,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AACD,IAAA,IAAI,GAAA,CAAI,MAAA,KAAW,GAAA,IAAO,IAAA,CAAK,WAAA,EAAa;AAI1C,MAAA,MAAM,GAAA,CAAI,IAAA,EAAK,CAAE,KAAA,CAAM,MAAM,EAAE,CAAA;AAC/B,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,MAAA,MAAM,OAAO,MAAM,GAAA,CAAI,MAAK,CAAE,KAAA,CAAM,MAAM,EAAE,CAAA;AAC5C,MAAA,MAAM,IAAI,iBAAA,CAAkB;AAAA,QAC1B,QAAQ,GAAA,CAAI,MAAA;AAAA,QACZ,YAAY,GAAA,CAAI,UAAA;AAAA,QAChB,IAAA;AAAA,QACA;AAAA,OACD,CAAA;AAAA,IACH;AACA,IAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,EACzB;AAEA,EAAA,MAAM,KAAA,GAAkB;AAAA,IACtB,MAAM,IAAA,CAAwB,IAAA,GAAyB,EAAC,EAAG;AACzD,MAAA,MAAM,MAAgC,EAAC;AACvC,MAAA,IAAI,IAAA,GAAO,CAAA;AACX,MAAA,MAAM,SAAA,GAAY,KAAK,IAAA,GAAO,CAAA,MAAA,EAAS,mBAAmB,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA,GAAK,EAAA;AACzE,MAAA,MAAM,WAAA,GAAc,KAAK,MAAA,GACrB,CAAA,QAAA,EAAW,mBAAmB,IAAA,CAAK,MAAM,CAAC,CAAA,CAAA,GAC1C,EAAA;AAIJ,MAAA,MAAM,YAAY,IAAA,CAAK,UAAA,GACnB,CAAA,aAAA,EAAgB,IAAA,CAAK,UAAU,CAAA,CAAA,GAC/B,WAAA;AAIJ,MAAA,MAAM,SAAA,GAAY,GAAA;AAClB,MAAA,OAAO,QAAQ,SAAA,EAAW;AACxB,QAAA,MAAM,OAAO,MAAM,GAAA;AAAA,UACjB,kCAAkC,SAAS,CAAA,MAAA,EAAS,IAAI,CAAA,EAAG,SAAS,GAAG,WAAW,CAAA;AAAA,SACpF;AACA,QAAA,GAAA,CAAI,IAAA,CAAK,GAAG,IAAA,CAAK,KAAK,CAAA;AACtB,QAAA,IAAI,CAAC,IAAA,CAAK,QAAA,CAAS,OAAA,EAAS;AAC5B,QAAA,IAAA,IAAQ,CAAA;AAAA,MACV;AACA,MAAA,OAAO,GAAA;AAAA,IACT,CAAA;AAAA,IACA,MAAM,UAA6B,IAAA,EAAc;AAC/C,MAAA,OAAO,GAAA;AAAA,QACL,CAAA,gBAAA,EAAmB,kBAAA,CAAmB,IAAI,CAAC,CAAA,CAAA;AAAA,QAC3C,EAAE,aAAa,IAAA;AAAK,OACtB;AAAA,IACF;AAAA,GACF;AAEA,EAAA,MAAM,UAAA,GAA4B;AAAA,IAChC,MAAM,IAAA,CAAK,IAAA,GAA8B,EAAC,EAAG;AAC3C,MAAA,MAAM,EAAA,GAAK,KAAK,MAAA,GAAS,CAAA,QAAA,EAAW,mBAAmB,IAAA,CAAK,MAAM,CAAC,CAAA,CAAA,GAAK,EAAA;AACxE,MAAA,MAAM,OAAO,MAAM,GAAA;AAAA,QACjB,uBAAuB,EAAE,CAAA;AAAA,OAC3B;AACA,MAAA,OAAO,IAAA,CAAK,KAAA;AAAA,IACd;AAAA,GACF;AAEA,EAAA,MAAM,SAAA,GAA0B;AAAA,IAC9B,GAAA,GAGI;AACF,MAAA,OAAO,GAAA;AAAA,QACL;AAAA,OACF;AAAA,IACF;AAAA,GACF;AAEA,EAAA,MAAM,WAAA,GAA8B;AAAA,IAClC,IAAuB,GAAA,EAAa;AAClC,MAAA,OAAO,GAAA;AAAA,QACL,CAAA,sBAAA,EAAyB,kBAAA,CAAmB,GAAG,CAAC,CAAA,CAAA;AAAA,QAChD,EAAE,aAAa,IAAA;AAAK,OACtB;AAAA,IACF;AAAA,GACF;AAEA,EAAA,MAAM,SAAA,GAA0B;AAAA,IAC9B,SAAA,GAAY;AACV,MAAA,OAAO,IAAqB,4BAA4B,CAAA;AAAA,IAC1D,CAAA;AAAA,IACA,MAAM,OAAA,CAAQ,IAAA,GAAuB,EAAC,EAAG;AAIvC,MAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACnC,QAAA,OAAO,EAAE,SAAA,EAAW,KAAA,EAAO,MAAA,EAAQ,KAAA,EAAe;AAAA,MACpD;AAIA,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,MAAA,IAAW,MAAM,UAAU,SAAA,EAAU;AACtD,MAAA,IAAI,CAAC,IAAI,OAAA,EAAS;AAChB,QAAA,OAAO,EAAE,SAAA,EAAW,KAAA,EAAO,MAAA,EAAQ,UAAA,EAAoB;AAAA,MACzD;AAOA,MAAA,MAAM,WAAW,QAAA,CAAS,aAAA;AAAA,QACxB,CAAA,OAAA,EAAU,gBAAgB,CAAA,EAAA,EAAK,GAAA,CAAI,SAAS,CAAA,EAAA;AAAA,OAC9C;AACA,MAAA,IAAI,QAAA,EAAU;AACZ,QAAA,OAAO,EAAE,SAAA,EAAW,KAAA,EAAO,MAAA,EAAQ,mBAAA,EAA6B;AAAA,MAClE;AAEA,MAAA,MAAM,MAAA,GAAS,QAAA,CAAS,aAAA,CAAc,QAAQ,CAAA;AAC9C,MAAA,MAAA,CAAO,KAAA,GAAQ,IAAA;AACf,MAAA,MAAA,CAAO,MAAM,GAAA,CAAI,SAAA;AACjB,MAAA,MAAA,CAAO,YAAA,CAAa,iBAAA,EAAmB,GAAA,CAAI,SAAS,CAAA;AAIpD,MAAA,MAAA,CAAO,YAAA,CAAa,gBAAA,EAAkB,GAAA,CAAI,SAAS,CAAA;AACnD,MAAA,QAAA,CAAS,IAAA,CAAK,YAAY,MAAM,CAAA;AAChC,MAAA,OAAO,EAAE,SAAA,EAAW,IAAA,EAAM,SAAA,EAAW,IAAI,SAAA,EAAU;AAAA,IACrD;AAAA,GACF;AAEA,EAAA,MAAM,WAAA,GAA8B;AAAA,IAClC,MAAM,OAAO,KAAA,EAA8B;AACzC,MAAA,MAAM,GAAA,GAAM,GAAG,OAAO,CAAA,qBAAA,CAAA;AACtB,MAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,GAAA,EAAK;AAAA,QAC/B,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS;AAAA,UACP,WAAA,EAAa,MAAA;AAAA,UACb,cAAA,EAAgB,kBAAA;AAAA,UAChB,MAAA,EAAQ,kBAAA;AAAA,UACR,YAAA,EAAc;AAAA,SAChB;AAAA,QACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,KAAK;AAAA,OAC3B,CAAA;AACD,MAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,QAAA,MAAM,OAAO,MAAM,GAAA,CAAI,MAAK,CAAE,KAAA,CAAM,MAAM,EAAE,CAAA;AAC5C,QAAA,MAAM,IAAI,iBAAA,CAAkB;AAAA,UAC1B,QAAQ,GAAA,CAAI,MAAA;AAAA,UACZ,YAAY,GAAA,CAAI,UAAA;AAAA,UAChB,IAAA;AAAA,UACA;AAAA,SACD,CAAA;AAAA,MACH;AACA,MAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,IACzB;AAAA,GACF;AASA,EAAA,eAAe,IAAA,CACb,IAAA,EACA,IAAA,EACA,IAAA,GAAuB,EAAC,EACZ;AACZ,IAAA,MAAM,GAAA,GAAM,CAAA,EAAG,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA;AAC7B,IAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,GAAA,EAAK;AAAA,MAC/B,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,WAAA,EAAa,MAAA;AAAA,QACb,cAAA,EAAgB,kBAAA;AAAA,QAChB,MAAA,EAAQ,kBAAA;AAAA,QACR,YAAA,EAAc;AAAA,OAChB;AAAA,MACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA;AAAA,MACzB,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AACD,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,MAAA,MAAM,UAAU,MAAM,GAAA,CAAI,MAAK,CAAE,KAAA,CAAM,MAAM,EAAE,CAAA;AAC/C,MAAA,MAAM,IAAI,iBAAA,CAAkB;AAAA,QAC1B,QAAQ,GAAA,CAAI,MAAA;AAAA,QACZ,YAAY,GAAA,CAAI,UAAA;AAAA,QAChB,IAAA,EAAM,OAAA;AAAA,QACN;AAAA,OACD,CAAA;AAAA,IACH;AACA,IAAA,IAAI,GAAA,CAAI,MAAA,KAAW,GAAA,EAAK,OAAO,MAAA;AAC/B,IAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,EACzB;AAEA,EAAA,MAAM,YAAA,GAAgC;AAAA,IACpC,eAAA,CAAgB,IAAA,GAAO,EAAC,EAAG;AACzB,MAAA,MAAM,KAAe,EAAC;AACtB,MAAA,IAAI,IAAA,CAAK,IAAA,EAAM,EAAA,CAAG,IAAA,CAAK,CAAA,KAAA,EAAQ,kBAAA,CAAmB,KAAA,CAAM,IAAA,CAAK,IAAI,CAAC,CAAC,CAAA,CAAE,CAAA;AACrE,MAAA,IAAI,IAAA,CAAK,EAAA,EAAI,EAAA,CAAG,IAAA,CAAK,CAAA,GAAA,EAAM,kBAAA,CAAmB,KAAA,CAAM,IAAA,CAAK,EAAE,CAAC,CAAC,CAAA,CAAE,CAAA;AAC/D,MAAA,MAAM,MAAA,GAAS,GAAG,MAAA,GAAS,CAAA,CAAA,EAAI,GAAG,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA,GAAK,EAAA;AAChD,MAAA,OAAO,GAAA;AAAA,QACL,sCAAsC,MAAM,CAAA;AAAA,OAC9C;AAAA,IACF,CAAA;AAAA,IACA,cAAc,KAAA,EAAO;AACnB,MAAA,OAAO,IAAA;AAAA,QACL,iCAAA;AAAA,QACA;AAAA,OACF;AAAA,IACF,CAAA;AAAA,IACA,uBAAuB,KAAA,EAAO;AAC5B,MAAA,OAAO,GAAA;AAAA,QACL,CAAA,gCAAA,EAAmC,kBAAA,CAAmB,KAAK,CAAC,CAAA;AAAA,OAC9D;AAAA,IACF,CAAA;AAAA,IACA,cAAc,KAAA,EAAO;AACnB,MAAA,OAAO,IAAA;AAAA,QACL,CAAA,gCAAA,EAAmC,kBAAA,CAAmB,KAAK,CAAC,CAAA,OAAA,CAAA;AAAA,QAC5D;AAAC,OACH;AAAA,IACF;AAAA,GACF;AAEA,EAAA,OAAO;AAAA,IACL,GAAA;AAAA,IACA,KAAA;AAAA,IACA,UAAA;AAAA,IACA,SAAA;AAAA,IACA,WAAA;AAAA,IACA,SAAA;AAAA,IACA,WAAA;AAAA,IACA;AAAA,GACF;AACF;AAIA,SAAS,MAAM,CAAA,EAA0B;AACvC,EAAA,OAAO,OAAO,CAAA,KAAM,QAAA,GAAW,CAAA,GAAI,EAAE,WAAA,EAAY;AACnD;;;AC1kBO,IAAM,WAAA,GAAc","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":";;;;;;;AA8CO,IAAM,iBAAA,GAAN,cAAgC,KAAA,CAAM;AAAA,EACzB,IAAA,GAAO,mBAAA;AAAA,EAChB,MAAA;AAAA,EACA,UAAA;AAAA,EACA,IAAA;AAAA,EACA,GAAA;AAAA,EAET,YAAY,IAAA,EAKT;AACD,IAAA,KAAA;AAAA,MACE,CAAA,YAAA,EAAe,IAAA,CAAK,MAAM,CAAA,CAAA,EAAI,KAAK,UAAU,CAAA,IAAA,EAAO,IAAA,CAAK,GAAG,WAAM,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,GAAG,CAAC,CAAA;AAAA,KAC3F;AACA,IAAA,IAAA,CAAK,SAAS,IAAA,CAAK,MAAA;AACnB,IAAA,IAAA,CAAK,aAAa,IAAA,CAAK,UAAA;AACvB,IAAA,IAAA,CAAK,OAAO,IAAA,CAAK,IAAA;AACjB,IAAA,IAAA,CAAK,MAAM,IAAA,CAAK,GAAA;AAAA,EAClB;AACF;AAqUA,IAAM,gBAAA,GAAmB,0BAAA;AAGzB,IAAM,WAAA,GAAc,qBAAA;AASpB,SAAS,gBAAgB,aAAA,EAA6B;AACpD,EAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACrC,EAAA,MAAM,WAAW,QAAA,CAAS,aAAA;AAAA,IACxB,uDAAuD,WAAW,CAAA,CAAA;AAAA,GACpE;AACA,EAAA,IAAI,QAAA,EAAU;AAEd,EAAA,MAAM,MAAA,GAAS,QAAA,CAAS,aAAA,CAAc,QAAQ,CAAA;AAC9C,EAAA,MAAA,CAAO,KAAA,GAAQ,IAAA;AACf,EAAA,MAAA,CAAO,GAAA,GAAM,CAAA,4CAAA,EAA+C,kBAAA,CAAmB,aAAa,CAAC,CAAA,CAAA;AAC7F,EAAA,MAAA,CAAO,YAAA,CAAa,aAAa,aAAa,CAAA;AAC9C,EAAA,QAAA,CAAS,IAAA,CAAK,YAAY,MAAM,CAAA;AAEhC,EAAA,MAAM,CAAA,GAAI,MAAA;AACV,EAAA,CAAA,CAAE,SAAA,GAAY,CAAA,CAAE,SAAA,IAAa,EAAC;AAG9B,EAAA,SAAS,QAAQ,KAAA,EAAkB;AAEjC,IAAA,CAAA,CAAE,SAAA,CAAW,KAAK,SAAS,CAAA;AAAA,EAC7B;AACA,EAAA,IAAA,CAAK,IAAA,kBAAM,IAAI,IAAA,EAAM,CAAA;AACrB,EAAA,IAAA,CAAK,UAAU,aAAa,CAAA;AAC9B;AAEA,IAAM,kBAAA,GAAqB,mBAAA;AAEpB,SAAS,sBACd,MAAA,EACiB;AACjB,EAAA,IAAI,CAAC,MAAA,CAAO,OAAA;AACV,IAAA,MAAM,IAAI,MAAM,8CAA8C,CAAA;AAChE,EAAA,IAAI,CAAC,MAAA,CAAO,MAAA;AACV,IAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAE/D,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,OAAA,CAAQ,OAAO,EAAE,CAAA;AAChD,EAAA,MAAM,SAAS,MAAA,CAAO,MAAA;AAGtB,EAAA,MAAM,SAAA,GAA0B,MAAA,CAAO,KAAA,IAAS,UAAA,CAAW,KAAA;AAC3D,EAAA,MAAM,SAAA,GAAY,OAAO,SAAA,IAAa,kBAAA;AAEtC,EAAA,eAAe,GAAA,CAAO,IAAA,EAAc,IAAA,GAAuB,EAAC,EAAe;AACzE,IAAA,MAAM,GAAA,GAAM,CAAA,EAAG,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA;AAC7B,IAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,GAAA,EAAK;AAAA,MAC/B,MAAA,EAAQ,KAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,WAAA,EAAa,MAAA;AAAA,QACb,MAAA,EAAQ,kBAAA;AAAA,QACR,YAAA,EAAc;AAAA,OAChB;AAAA,MACA,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AACD,IAAA,IAAI,GAAA,CAAI,MAAA,KAAW,GAAA,IAAO,IAAA,CAAK,WAAA,EAAa;AAI1C,MAAA,MAAM,GAAA,CAAI,IAAA,EAAK,CAAE,KAAA,CAAM,MAAM,EAAE,CAAA;AAC/B,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,MAAA,MAAM,OAAO,MAAM,GAAA,CAAI,MAAK,CAAE,KAAA,CAAM,MAAM,EAAE,CAAA;AAC5C,MAAA,MAAM,IAAI,iBAAA,CAAkB;AAAA,QAC1B,QAAQ,GAAA,CAAI,MAAA;AAAA,QACZ,YAAY,GAAA,CAAI,UAAA;AAAA,QAChB,IAAA;AAAA,QACA;AAAA,OACD,CAAA;AAAA,IACH;AACA,IAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,EACzB;AAEA,EAAA,MAAM,KAAA,GAAkB;AAAA,IACtB,MAAM,IAAA,CAAwB,IAAA,GAAyB,EAAC,EAAG;AACzD,MAAA,MAAM,MAAgC,EAAC;AACvC,MAAA,IAAI,IAAA,GAAO,CAAA;AACX,MAAA,MAAM,SAAA,GAAY,KAAK,IAAA,GAAO,CAAA,MAAA,EAAS,mBAAmB,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA,GAAK,EAAA;AACzE,MAAA,MAAM,WAAA,GAAc,KAAK,MAAA,GACrB,CAAA,QAAA,EAAW,mBAAmB,IAAA,CAAK,MAAM,CAAC,CAAA,CAAA,GAC1C,EAAA;AAIJ,MAAA,MAAM,YAAY,IAAA,CAAK,UAAA,GACnB,CAAA,aAAA,EAAgB,IAAA,CAAK,UAAU,CAAA,CAAA,GAC/B,WAAA;AAIJ,MAAA,MAAM,SAAA,GAAY,GAAA;AAClB,MAAA,OAAO,QAAQ,SAAA,EAAW;AACxB,QAAA,MAAM,OAAO,MAAM,GAAA;AAAA,UACjB,kCAAkC,SAAS,CAAA,MAAA,EAAS,IAAI,CAAA,EAAG,SAAS,GAAG,WAAW,CAAA;AAAA,SACpF;AACA,QAAA,GAAA,CAAI,IAAA,CAAK,GAAG,IAAA,CAAK,KAAK,CAAA;AACtB,QAAA,IAAI,CAAC,IAAA,CAAK,QAAA,CAAS,OAAA,EAAS;AAC5B,QAAA,IAAA,IAAQ,CAAA;AAAA,MACV;AACA,MAAA,OAAO,GAAA;AAAA,IACT,CAAA;AAAA,IACA,MAAM,UAA6B,IAAA,EAAc;AAC/C,MAAA,OAAO,GAAA;AAAA,QACL,CAAA,gBAAA,EAAmB,kBAAA,CAAmB,IAAI,CAAC,CAAA,CAAA;AAAA,QAC3C,EAAE,aAAa,IAAA;AAAK,OACtB;AAAA,IACF;AAAA,GACF;AAEA,EAAA,MAAM,UAAA,GAA4B;AAAA,IAChC,MAAM,IAAA,CAAK,IAAA,GAA8B,EAAC,EAAG;AAC3C,MAAA,MAAM,EAAA,GAAK,KAAK,MAAA,GAAS,CAAA,QAAA,EAAW,mBAAmB,IAAA,CAAK,MAAM,CAAC,CAAA,CAAA,GAAK,EAAA;AACxE,MAAA,MAAM,OAAO,MAAM,GAAA;AAAA,QACjB,uBAAuB,EAAE,CAAA;AAAA,OAC3B;AACA,MAAA,OAAO,IAAA,CAAK,KAAA;AAAA,IACd;AAAA,GACF;AAEA,EAAA,MAAM,SAAA,GAA0B;AAAA,IAC9B,GAAA,GAGI;AACF,MAAA,OAAO,GAAA;AAAA,QACL;AAAA,OACF;AAAA,IACF;AAAA,GACF;AAEA,EAAA,MAAM,WAAA,GAA8B;AAAA,IAClC,IAAuB,GAAA,EAAa;AAClC,MAAA,OAAO,GAAA;AAAA,QACL,CAAA,sBAAA,EAAyB,kBAAA,CAAmB,GAAG,CAAC,CAAA,CAAA;AAAA,QAChD,EAAE,aAAa,IAAA;AAAK,OACtB;AAAA,IACF;AAAA,GACF;AAEA,EAAA,MAAM,SAAA,GAA0B;AAAA,IAC9B,SAAA,GAAY;AACV,MAAA,OAAO,IAAqB,4BAA4B,CAAA;AAAA,IAC1D,CAAA;AAAA,IACA,QAAA,CAAS,IAAA,GAAO,EAAC,EAAG;AAClB,MAAA,MAAM,KAAA,GAAQ,KAAK,KAAA,IAAS,IAAA;AAC5B,MAAA,OAAO,GAAA;AAAA,QACL,CAAA,mCAAA,EAAsC,kBAAA,CAAmB,KAAK,CAAC,CAAA;AAAA,OACjE;AAAA,IACF,CAAA;AAAA,IACA,MAAM,OAAA,CAAQ,IAAA,GAAuB,EAAC,EAAG;AAIvC,MAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACnC,QAAA,OAAO,EAAE,SAAA,EAAW,KAAA,EAAO,MAAA,EAAQ,KAAA,EAAe;AAAA,MACpD;AAIA,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,MAAA,IAAW,MAAM,UAAU,SAAA,EAAU;AAKtD,MAAA,IAAI,IAAI,eAAA,EAAiB;AACvB,QAAA,eAAA,CAAgB,IAAI,eAAe,CAAA;AAAA,MACrC;AAEA,MAAA,IAAI,CAAC,IAAI,OAAA,EAAS;AAChB,QAAA,OAAO,EAAE,SAAA,EAAW,KAAA,EAAO,MAAA,EAAQ,UAAA,EAAoB;AAAA,MACzD;AAOA,MAAA,MAAM,WAAW,QAAA,CAAS,aAAA;AAAA,QACxB,CAAA,OAAA,EAAU,gBAAgB,CAAA,EAAA,EAAK,GAAA,CAAI,SAAS,CAAA,EAAA;AAAA,OAC9C;AACA,MAAA,IAAI,QAAA,EAAU;AACZ,QAAA,OAAO,EAAE,SAAA,EAAW,KAAA,EAAO,MAAA,EAAQ,mBAAA,EAA6B;AAAA,MAClE;AAEA,MAAA,MAAM,MAAA,GAAS,QAAA,CAAS,aAAA,CAAc,QAAQ,CAAA;AAC9C,MAAA,MAAA,CAAO,KAAA,GAAQ,IAAA;AACf,MAAA,MAAA,CAAO,MAAM,GAAA,CAAI,SAAA;AACjB,MAAA,MAAA,CAAO,YAAA,CAAa,iBAAA,EAAmB,GAAA,CAAI,SAAS,CAAA;AAIpD,MAAA,MAAA,CAAO,YAAA,CAAa,gBAAA,EAAkB,GAAA,CAAI,SAAS,CAAA;AACnD,MAAA,QAAA,CAAS,IAAA,CAAK,YAAY,MAAM,CAAA;AAChC,MAAA,OAAO,EAAE,SAAA,EAAW,IAAA,EAAM,SAAA,EAAW,IAAI,SAAA,EAAU;AAAA,IACrD;AAAA,GACF;AAEA,EAAA,MAAM,WAAA,GAA8B;AAAA,IAClC,MAAM,OAAO,KAAA,EAA8B;AACzC,MAAA,MAAM,GAAA,GAAM,GAAG,OAAO,CAAA,qBAAA,CAAA;AACtB,MAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,GAAA,EAAK;AAAA,QAC/B,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS;AAAA,UACP,WAAA,EAAa,MAAA;AAAA,UACb,cAAA,EAAgB,kBAAA;AAAA,UAChB,MAAA,EAAQ,kBAAA;AAAA,UACR,YAAA,EAAc;AAAA,SAChB;AAAA,QACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,KAAK;AAAA,OAC3B,CAAA;AACD,MAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,QAAA,MAAM,OAAO,MAAM,GAAA,CAAI,MAAK,CAAE,KAAA,CAAM,MAAM,EAAE,CAAA;AAC5C,QAAA,MAAM,IAAI,iBAAA,CAAkB;AAAA,UAC1B,QAAQ,GAAA,CAAI,MAAA;AAAA,UACZ,YAAY,GAAA,CAAI,UAAA;AAAA,UAChB,IAAA;AAAA,UACA;AAAA,SACD,CAAA;AAAA,MACH;AACA,MAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,IACzB;AAAA,GACF;AASA,EAAA,eAAe,IAAA,CACb,IAAA,EACA,IAAA,EACA,IAAA,GAAuB,EAAC,EACZ;AACZ,IAAA,MAAM,GAAA,GAAM,CAAA,EAAG,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA;AAC7B,IAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,GAAA,EAAK;AAAA,MAC/B,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,WAAA,EAAa,MAAA;AAAA,QACb,cAAA,EAAgB,kBAAA;AAAA,QAChB,MAAA,EAAQ,kBAAA;AAAA,QACR,YAAA,EAAc;AAAA,OAChB;AAAA,MACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA;AAAA,MACzB,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AACD,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,MAAA,MAAM,UAAU,MAAM,GAAA,CAAI,MAAK,CAAE,KAAA,CAAM,MAAM,EAAE,CAAA;AAC/C,MAAA,MAAM,IAAI,iBAAA,CAAkB;AAAA,QAC1B,QAAQ,GAAA,CAAI,MAAA;AAAA,QACZ,YAAY,GAAA,CAAI,UAAA;AAAA,QAChB,IAAA,EAAM,OAAA;AAAA,QACN;AAAA,OACD,CAAA;AAAA,IACH;AACA,IAAA,IAAI,GAAA,CAAI,MAAA,KAAW,GAAA,EAAK,OAAO,MAAA;AAC/B,IAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,EACzB;AAEA,EAAA,MAAM,YAAA,GAAgC;AAAA,IACpC,eAAA,CAAgB,IAAA,GAAO,EAAC,EAAG;AACzB,MAAA,MAAM,KAAe,EAAC;AACtB,MAAA,IAAI,IAAA,CAAK,IAAA,EAAM,EAAA,CAAG,IAAA,CAAK,CAAA,KAAA,EAAQ,kBAAA,CAAmB,KAAA,CAAM,IAAA,CAAK,IAAI,CAAC,CAAC,CAAA,CAAE,CAAA;AACrE,MAAA,IAAI,IAAA,CAAK,EAAA,EAAI,EAAA,CAAG,IAAA,CAAK,CAAA,GAAA,EAAM,kBAAA,CAAmB,KAAA,CAAM,IAAA,CAAK,EAAE,CAAC,CAAC,CAAA,CAAE,CAAA;AAC/D,MAAA,MAAM,MAAA,GAAS,GAAG,MAAA,GAAS,CAAA,CAAA,EAAI,GAAG,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA,GAAK,EAAA;AAChD,MAAA,OAAO,GAAA;AAAA,QACL,sCAAsC,MAAM,CAAA;AAAA,OAC9C;AAAA,IACF,CAAA;AAAA,IACA,cAAc,KAAA,EAAO;AACnB,MAAA,OAAO,IAAA;AAAA,QACL,iCAAA;AAAA,QACA;AAAA,OACF;AAAA,IACF;AAAA,GACF;AAEA,EAAA,OAAO;AAAA,IACL,GAAA;AAAA,IACA,KAAA;AAAA,IACA,UAAA;AAAA,IACA,SAAA;AAAA,IACA,WAAA;AAAA,IACA,SAAA;AAAA,IACA,WAAA;AAAA,IACA;AAAA,GACF;AACF;AAIA,SAAS,MAAM,CAAA,EAA0B;AACvC,EAAA,OAAO,OAAO,CAAA,KAAM,QAAA,GAAW,CAAA,GAAI,EAAE,WAAA,EAAY;AACnD;;;AC5qBO,IAAM,WAAA,GAAc","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.
|
|
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.
|
|
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.
|
|
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",
|