@brandfine/client 0.4.0 → 0.5.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 +28 -0
- package/README.md +65 -16
- package/dist/index.cjs +47 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -1
- package/dist/index.d.ts +36 -1
- package/dist/index.js +35 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @brandfine/client
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 43231d6: Add `bf.submissions` namespace for posting contact-form submissions
|
|
8
|
+
to the workspace from any consumer site.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
await bf.submissions.create({
|
|
12
|
+
name: "Yiğit",
|
|
13
|
+
email: "yigit@example.com",
|
|
14
|
+
message: "Hello from the contact form!",
|
|
15
|
+
// optional:
|
|
16
|
+
phone: "+90 …",
|
|
17
|
+
subject: "Demo request",
|
|
18
|
+
source: "/contact",
|
|
19
|
+
metadata: { utm_source: "twitter" },
|
|
20
|
+
});
|
|
21
|
+
// → { id, createdAt }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Wraps `POST /external/submissions` (already on the api). Validation
|
|
25
|
+
errors throw `BrandfineApiError` with the api's response body
|
|
26
|
+
attached so the caller can render field-level messages.
|
|
27
|
+
|
|
28
|
+
New `CreateSubmissionInput` and `Submission` types are exported
|
|
29
|
+
from the root entry.
|
|
30
|
+
|
|
3
31
|
## 0.4.0
|
|
4
32
|
|
|
5
33
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @brandfine/client
|
|
2
2
|
|
|
3
|
-
Typed HTTP client, server-side caches, locale + navigation resolvers,
|
|
3
|
+
Typed HTTP client, server-side caches, locale + navigation resolvers, webhook helpers, and analytics tracker installer for landing pages consuming the [Brandfine CMS](https://docs.brandfine.co).
|
|
4
4
|
|
|
5
5
|
> **Status:** early development (`0.x.y`). API may change in minor bumps before `1.0`.
|
|
6
6
|
|
|
@@ -15,26 +15,75 @@ Available on public npm with [provenance](https://docs.npmjs.com/generating-prov
|
|
|
15
15
|
## Subpath exports
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
createBrandfineClient,
|
|
20
|
+
type AnalyticsConfig,
|
|
21
|
+
type AnalyticsInstallResult,
|
|
22
|
+
} from '@brandfine/client'
|
|
23
|
+
import { createCache, createKeyedCache } from '@brandfine/client/cache'
|
|
20
24
|
import { resolveNavigation, localizePath } from '@brandfine/client/resolvers'
|
|
21
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
verifyWebhookSecret,
|
|
27
|
+
parseWebhookPayload,
|
|
28
|
+
} from '@brandfine/client/webhook'
|
|
22
29
|
```
|
|
23
30
|
|
|
24
31
|
Pick the import path that scopes to what you actually use — tree-shaking does the rest, but subpath imports keep consumer bundle analysis honest.
|
|
25
32
|
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
## What it does
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const bf = createBrandfineClient({
|
|
37
|
+
baseUrl: 'https://api.brandfine.co',
|
|
38
|
+
apiKey: process.env.BRANDFINE_API_KEY!,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Content reads
|
|
42
|
+
await bf.posts.list({ type: 'blog', locale: 'pt' })
|
|
43
|
+
await bf.posts.getBySlug('uk-eta-2026')
|
|
44
|
+
await bf.workspace.get()
|
|
45
|
+
await bf.categories.list()
|
|
46
|
+
await bf.navigations.get('header')
|
|
47
|
+
|
|
48
|
+
// Analytics — auto-inject the Brandfine tracker into <head>
|
|
49
|
+
await bf.analytics.install()
|
|
50
|
+
|
|
51
|
+
// Submissions — POST a contact-form submission
|
|
52
|
+
await bf.submissions.create({
|
|
53
|
+
name: 'Alex',
|
|
54
|
+
email: 'alex@example.com',
|
|
55
|
+
message: 'Hello!',
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The full surface lives under six namespaces — `posts`, `categories`, `workspace`, `navigations`, `analytics`, `submissions` — plus a `get(path)` escape hatch.
|
|
60
|
+
|
|
61
|
+
## Analytics
|
|
62
|
+
|
|
63
|
+
`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.
|
|
64
|
+
|
|
65
|
+
For static sites (Next.js `output: 'export'`, Astro), use the **build-time** variant to keep the API key out of the browser bundle and skip the runtime round-trip:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// Server-side at build/request time:
|
|
69
|
+
const config = await bf.analytics.getConfig()
|
|
70
|
+
// Pass `config` to a client component that calls:
|
|
71
|
+
bf.analytics.install({ config })
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Full walkthrough with framework recipes: [docs.brandfine.co/docs/sdk/analytics](https://docs.brandfine.co/docs/sdk/analytics).
|
|
75
|
+
|
|
76
|
+
## Caching
|
|
77
|
+
|
|
78
|
+
`createCache(opts)` and `createKeyedCache(opts)` are minimal SWR caches with TTL + background revalidation. They take any async fetcher — including `bf.posts.list` — and make the cached value the source of truth for hot paths. Pair with `verifyWebhookSecret` to invalidate on publish.
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
|
|
82
|
+
- [SDK quickstart](https://docs.brandfine.co/docs/sdk/quickstart) — minimal Astro integration end-to-end.
|
|
83
|
+
- [`createBrandfineClient`](https://docs.brandfine.co/docs/sdk/client) — full options + method reference.
|
|
84
|
+
- [Analytics install](https://docs.brandfine.co/docs/sdk/analytics) — runtime vs build-time, framework recipes.
|
|
85
|
+
- [Webhook handler](https://docs.brandfine.co/docs/sdk/webhooks) — verify + parse + dispatch.
|
|
86
|
+
- [REST API reference](https://docs.brandfine.co/docs/api/authentication) — for non-TypeScript consumers.
|
|
38
87
|
|
|
39
88
|
## License
|
|
40
89
|
|
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 {
|
|
@@ -134,7 +134,40 @@ function createBrandfineClient(config) {
|
|
|
134
134
|
return { installed: true, websiteId: cfg.websiteId };
|
|
135
135
|
}
|
|
136
136
|
};
|
|
137
|
-
|
|
137
|
+
const submissions = {
|
|
138
|
+
async create(input) {
|
|
139
|
+
const url = `${baseUrl}/external/submissions`;
|
|
140
|
+
const res = await fetchImpl(url, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: {
|
|
143
|
+
"X-Api-Key": apiKey,
|
|
144
|
+
"Content-Type": "application/json",
|
|
145
|
+
Accept: "application/json",
|
|
146
|
+
"User-Agent": userAgent
|
|
147
|
+
},
|
|
148
|
+
body: JSON.stringify(input)
|
|
149
|
+
});
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
const body = await res.text().catch(() => "");
|
|
152
|
+
throw new BrandfineApiError({
|
|
153
|
+
status: res.status,
|
|
154
|
+
statusText: res.statusText,
|
|
155
|
+
body,
|
|
156
|
+
url
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return await res.json();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
return {
|
|
163
|
+
get,
|
|
164
|
+
posts,
|
|
165
|
+
categories,
|
|
166
|
+
workspace,
|
|
167
|
+
navigations,
|
|
168
|
+
analytics,
|
|
169
|
+
submissions
|
|
170
|
+
};
|
|
138
171
|
}
|
|
139
172
|
|
|
140
173
|
// src/index.ts
|
|
@@ -148,6 +181,18 @@ Object.defineProperty(exports, "createKeyedCache", {
|
|
|
148
181
|
enumerable: true,
|
|
149
182
|
get: function () { return chunkXJFKL2HU_cjs.createKeyedCache; }
|
|
150
183
|
});
|
|
184
|
+
Object.defineProperty(exports, "createBrandfineWebhookHandler", {
|
|
185
|
+
enumerable: true,
|
|
186
|
+
get: function () { return chunkKHHMR2NX_cjs.createBrandfineWebhookHandler; }
|
|
187
|
+
});
|
|
188
|
+
Object.defineProperty(exports, "parseWebhookPayload", {
|
|
189
|
+
enumerable: true,
|
|
190
|
+
get: function () { return chunkKHHMR2NX_cjs.parseWebhookPayload; }
|
|
191
|
+
});
|
|
192
|
+
Object.defineProperty(exports, "verifyWebhookSecret", {
|
|
193
|
+
enumerable: true,
|
|
194
|
+
get: function () { return chunkKHHMR2NX_cjs.verifyWebhookSecret; }
|
|
195
|
+
});
|
|
151
196
|
Object.defineProperty(exports, "isLocale", {
|
|
152
197
|
enumerable: true,
|
|
153
198
|
get: function () { return chunkFCK7QJBC_cjs.isLocale; }
|
|
@@ -168,18 +213,6 @@ Object.defineProperty(exports, "stripLocalePrefix", {
|
|
|
168
213
|
enumerable: true,
|
|
169
214
|
get: function () { return chunkFCK7QJBC_cjs.stripLocalePrefix; }
|
|
170
215
|
});
|
|
171
|
-
Object.defineProperty(exports, "createBrandfineWebhookHandler", {
|
|
172
|
-
enumerable: true,
|
|
173
|
-
get: function () { return chunkKHHMR2NX_cjs.createBrandfineWebhookHandler; }
|
|
174
|
-
});
|
|
175
|
-
Object.defineProperty(exports, "parseWebhookPayload", {
|
|
176
|
-
enumerable: true,
|
|
177
|
-
get: function () { return chunkKHHMR2NX_cjs.parseWebhookPayload; }
|
|
178
|
-
});
|
|
179
|
-
Object.defineProperty(exports, "verifyWebhookSecret", {
|
|
180
|
-
enumerable: true,
|
|
181
|
-
get: function () { return chunkKHHMR2NX_cjs.verifyWebhookSecret; }
|
|
182
|
-
});
|
|
183
216
|
exports.BrandfineApiError = BrandfineApiError;
|
|
184
217
|
exports.SDK_VERSION = SDK_VERSION;
|
|
185
218
|
exports.createBrandfineClient = createBrandfineClient;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `createBrandfineClient` — the SDK's entry point.\n *\n * Returns a stateless, multi-instance-safe handle scoped to a\n * single `(baseUrl, apiKey)` pair. Pattern follows the Stripe /\n * Algolia / OpenAI SDKs — explicit construction with config,\n * namespaced methods (`bf.posts.list(...)`, `bf.workspace.get()`),\n * no module-level singletons.\n *\n * Why factory not module-level state: multi-tenant consumers\n * sometimes need two clients in the same process (e.g. main site\n * + admin preview). Module-level env reading makes that impossible\n * without monkey-patching.\n */\n\nimport type {\n BrandfineCategory,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n\nexport type BrandfineClientConfig = {\n /** Base URL of the Brandfine API. No trailing slash — the client\n * trims one if you pass it anyway. e.g. `https://api.brandfine.co` */\n baseUrl: string\n /** Workspace-scoped API key. Generated from the cms's Workspace\n * settings; identifies which workspace the client talks to. */\n apiKey: string\n /** Optional fetch override. Useful for tests (inject a stub),\n * for runtimes that need a custom implementation (edge workers\n * with non-standard fetch), or to add cross-cutting concerns\n * like tracing / retries. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch\n /** Optional User-Agent header. Falls back to a generic SDK tag. */\n userAgent?: string\n}\n\n/**\n * Structured error thrown by every request helper on non-2xx\n * responses. Carries the raw body so consumers can log it for\n * debugging without re-fetching.\n */\nexport class BrandfineApiError extends Error {\n override readonly name = 'BrandfineApiError'\n readonly status: number\n readonly statusText: string\n readonly body: string\n readonly url: string\n\n constructor(args: {\n status: number\n statusText: string\n body: string\n url: string\n }) {\n super(\n `[brandfine] ${args.status} ${args.statusText} on ${args.url} — ${args.body.slice(0, 200)}`,\n )\n this.status = args.status\n this.statusText = args.statusText\n this.body = args.body\n this.url = args.url\n }\n}\n\ntype RequestOptions = {\n /** When true and the response is 404, return `null` instead of\n * throwing. Used by endpoints where 404 is a meaningful empty\n * state (navigation by key, single post by slug). */\n nullable404?: boolean\n signal?: AbortSignal\n}\n\nexport type BrandfineClient = {\n /** Low-level GET. Reserved for endpoints we don't have a typed\n * helper for yet. Adds the X-Api-Key header automatically. */\n get: <T>(path: string, opts?: RequestOptions) => Promise<T>\n posts: PostsApi\n categories: CategoriesApi\n workspace: WorkspaceApi\n navigations: NavigationsApi\n analytics: AnalyticsApi\n}\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 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 return { get, posts, categories, workspace, navigations, analytics }\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 InstallOptions,\n} from './client'\n\nexport {\n createCache,\n createKeyedCache,\n type Cache,\n type CacheOptions,\n type KeyedCache,\n type KeyedCacheOptions,\n} from './cache/index'\n\nexport {\n isLocale,\n localizePath,\n pickLocale,\n resolveNavigation,\n stripLocalePrefix,\n type HydratedNav,\n type HydratedNavItem,\n type LocaleOptions,\n type ResolveNavigationOptions,\n} from './resolvers/index'\n\nexport {\n createBrandfineWebhookHandler,\n parseWebhookPayload,\n verifyWebhookSecret,\n type BrandfineWebhookEvent,\n type BrandfineWebhookHandlerOptions,\n type BrandfineWebhookPayload,\n} from './webhook/index'\n\nexport type {\n BrandfineCategory,\n BrandfineNavItem,\n BrandfineNavItemType,\n BrandfineNavPost,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfinePostTranslation,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `createBrandfineClient` — the SDK's entry point.\n *\n * Returns a stateless, multi-instance-safe handle scoped to a\n * single `(baseUrl, apiKey)` pair. Pattern follows the Stripe /\n * Algolia / OpenAI SDKs — explicit construction with config,\n * namespaced methods (`bf.posts.list(...)`, `bf.workspace.get()`),\n * no module-level singletons.\n *\n * Why factory not module-level state: multi-tenant consumers\n * sometimes need two clients in the same process (e.g. main site\n * + admin preview). Module-level env reading makes that impossible\n * without monkey-patching.\n */\n\nimport type {\n BrandfineCategory,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n\nexport type BrandfineClientConfig = {\n /** Base URL of the Brandfine API. No trailing slash — the client\n * trims one if you pass it anyway. e.g. `https://api.brandfine.co` */\n baseUrl: string\n /** Workspace-scoped API key. Generated from the cms's Workspace\n * settings; identifies which workspace the client talks to. */\n apiKey: string\n /** Optional fetch override. Useful for tests (inject a stub),\n * for runtimes that need a custom implementation (edge workers\n * with non-standard fetch), or to add cross-cutting concerns\n * like tracing / retries. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch\n /** Optional User-Agent header. Falls back to a generic SDK tag. */\n userAgent?: string\n}\n\n/**\n * Structured error thrown by every request helper on non-2xx\n * responses. Carries the raw body so consumers can log it for\n * debugging without re-fetching.\n */\nexport class BrandfineApiError extends Error {\n override readonly name = 'BrandfineApiError'\n readonly status: number\n readonly statusText: string\n readonly body: string\n readonly url: string\n\n constructor(args: {\n status: number\n statusText: string\n body: string\n url: string\n }) {\n super(\n `[brandfine] ${args.status} ${args.statusText} on ${args.url} — ${args.body.slice(0, 200)}`,\n )\n this.status = args.status\n this.statusText = args.statusText\n this.body = args.body\n this.url = args.url\n }\n}\n\ntype RequestOptions = {\n /** When true and the response is 404, return `null` instead of\n * throwing. Used by endpoints where 404 is a meaningful empty\n * state (navigation by key, single post by slug). */\n nullable404?: boolean\n signal?: AbortSignal\n}\n\nexport type BrandfineClient = {\n /** Low-level GET. Reserved for endpoints we don't have a typed\n * helper for yet. Adds the X-Api-Key header automatically. */\n get: <T>(path: string, opts?: RequestOptions) => Promise<T>\n posts: PostsApi\n categories: CategoriesApi\n workspace: WorkspaceApi\n navigations: NavigationsApi\n analytics: AnalyticsApi\n submissions: SubmissionsApi\n}\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\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 return {\n get,\n posts,\n categories,\n workspace,\n navigations,\n analytics,\n submissions,\n }\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"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -67,6 +67,7 @@ type BrandfineClient = {
|
|
|
67
67
|
workspace: WorkspaceApi;
|
|
68
68
|
navigations: NavigationsApi;
|
|
69
69
|
analytics: AnalyticsApi;
|
|
70
|
+
submissions: SubmissionsApi;
|
|
70
71
|
};
|
|
71
72
|
type PostsApi = {
|
|
72
73
|
/** Paginated list of published posts. Handles the cms's
|
|
@@ -90,6 +91,40 @@ type NavigationsApi = {
|
|
|
90
91
|
* item's `customConfig` (default `unknown`). */
|
|
91
92
|
get: <TConfig = unknown>(key: string) => Promise<BrandfineNavigation<TConfig> | null>;
|
|
92
93
|
};
|
|
94
|
+
type CreateSubmissionInput = {
|
|
95
|
+
/** Required. Display name of the submitter. */
|
|
96
|
+
name: string;
|
|
97
|
+
/** Required. Validated server-side. */
|
|
98
|
+
email: string;
|
|
99
|
+
/** Optional. Free-text up to 40 chars. */
|
|
100
|
+
phone?: string;
|
|
101
|
+
/** Optional. Free-text up to 200 chars. */
|
|
102
|
+
subject?: string;
|
|
103
|
+
/** Required. The message body — up to 10,000 chars. */
|
|
104
|
+
message: string;
|
|
105
|
+
/** Optional. Where the submission came from — e.g. a route path
|
|
106
|
+
* like `/contact`, or a marketing campaign label. Up to 500 chars. */
|
|
107
|
+
source?: string;
|
|
108
|
+
/** Optional. Free-form JSON metadata the consumer attaches; the
|
|
109
|
+
* cms surfaces it verbatim in the submissions admin view. */
|
|
110
|
+
metadata?: Record<string, unknown>;
|
|
111
|
+
};
|
|
112
|
+
type Submission = {
|
|
113
|
+
id: string;
|
|
114
|
+
createdAt: string;
|
|
115
|
+
};
|
|
116
|
+
type SubmissionsApi = {
|
|
117
|
+
/**
|
|
118
|
+
* Posts a contact-form submission to `POST /external/submissions`
|
|
119
|
+
* for this workspace. The cms surfaces the submission in the
|
|
120
|
+
* Submissions inbox.
|
|
121
|
+
*
|
|
122
|
+
* Throws `BrandfineApiError` on validation failures (400) or
|
|
123
|
+
* any other non-2xx — caller decides whether to surface that as
|
|
124
|
+
* a user-visible error or a silent retry.
|
|
125
|
+
*/
|
|
126
|
+
create: (input: CreateSubmissionInput) => Promise<Submission>;
|
|
127
|
+
};
|
|
93
128
|
type AnalyticsConfig = {
|
|
94
129
|
enabled: false;
|
|
95
130
|
} | {
|
|
@@ -173,4 +208,4 @@ declare function createBrandfineClient(config: BrandfineClientConfig): Brandfine
|
|
|
173
208
|
*/
|
|
174
209
|
declare const SDK_VERSION: "0.0.0";
|
|
175
210
|
|
|
176
|
-
export { type AnalyticsConfig, type AnalyticsInstallResult, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type InstallOptions, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, createBrandfineClient };
|
|
211
|
+
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -67,6 +67,7 @@ type BrandfineClient = {
|
|
|
67
67
|
workspace: WorkspaceApi;
|
|
68
68
|
navigations: NavigationsApi;
|
|
69
69
|
analytics: AnalyticsApi;
|
|
70
|
+
submissions: SubmissionsApi;
|
|
70
71
|
};
|
|
71
72
|
type PostsApi = {
|
|
72
73
|
/** Paginated list of published posts. Handles the cms's
|
|
@@ -90,6 +91,40 @@ type NavigationsApi = {
|
|
|
90
91
|
* item's `customConfig` (default `unknown`). */
|
|
91
92
|
get: <TConfig = unknown>(key: string) => Promise<BrandfineNavigation<TConfig> | null>;
|
|
92
93
|
};
|
|
94
|
+
type CreateSubmissionInput = {
|
|
95
|
+
/** Required. Display name of the submitter. */
|
|
96
|
+
name: string;
|
|
97
|
+
/** Required. Validated server-side. */
|
|
98
|
+
email: string;
|
|
99
|
+
/** Optional. Free-text up to 40 chars. */
|
|
100
|
+
phone?: string;
|
|
101
|
+
/** Optional. Free-text up to 200 chars. */
|
|
102
|
+
subject?: string;
|
|
103
|
+
/** Required. The message body — up to 10,000 chars. */
|
|
104
|
+
message: string;
|
|
105
|
+
/** Optional. Where the submission came from — e.g. a route path
|
|
106
|
+
* like `/contact`, or a marketing campaign label. Up to 500 chars. */
|
|
107
|
+
source?: string;
|
|
108
|
+
/** Optional. Free-form JSON metadata the consumer attaches; the
|
|
109
|
+
* cms surfaces it verbatim in the submissions admin view. */
|
|
110
|
+
metadata?: Record<string, unknown>;
|
|
111
|
+
};
|
|
112
|
+
type Submission = {
|
|
113
|
+
id: string;
|
|
114
|
+
createdAt: string;
|
|
115
|
+
};
|
|
116
|
+
type SubmissionsApi = {
|
|
117
|
+
/**
|
|
118
|
+
* Posts a contact-form submission to `POST /external/submissions`
|
|
119
|
+
* for this workspace. The cms surfaces the submission in the
|
|
120
|
+
* Submissions inbox.
|
|
121
|
+
*
|
|
122
|
+
* Throws `BrandfineApiError` on validation failures (400) or
|
|
123
|
+
* any other non-2xx — caller decides whether to surface that as
|
|
124
|
+
* a user-visible error or a silent retry.
|
|
125
|
+
*/
|
|
126
|
+
create: (input: CreateSubmissionInput) => Promise<Submission>;
|
|
127
|
+
};
|
|
93
128
|
type AnalyticsConfig = {
|
|
94
129
|
enabled: false;
|
|
95
130
|
} | {
|
|
@@ -173,4 +208,4 @@ declare function createBrandfineClient(config: BrandfineClientConfig): Brandfine
|
|
|
173
208
|
*/
|
|
174
209
|
declare const SDK_VERSION: "0.0.0";
|
|
175
210
|
|
|
176
|
-
export { type AnalyticsConfig, type AnalyticsInstallResult, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type InstallOptions, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, createBrandfineClient };
|
|
211
|
+
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 };
|
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 {
|
|
@@ -132,7 +132,40 @@ function createBrandfineClient(config) {
|
|
|
132
132
|
return { installed: true, websiteId: cfg.websiteId };
|
|
133
133
|
}
|
|
134
134
|
};
|
|
135
|
-
|
|
135
|
+
const submissions = {
|
|
136
|
+
async create(input) {
|
|
137
|
+
const url = `${baseUrl}/external/submissions`;
|
|
138
|
+
const res = await fetchImpl(url, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"X-Api-Key": apiKey,
|
|
142
|
+
"Content-Type": "application/json",
|
|
143
|
+
Accept: "application/json",
|
|
144
|
+
"User-Agent": userAgent
|
|
145
|
+
},
|
|
146
|
+
body: JSON.stringify(input)
|
|
147
|
+
});
|
|
148
|
+
if (!res.ok) {
|
|
149
|
+
const body = await res.text().catch(() => "");
|
|
150
|
+
throw new BrandfineApiError({
|
|
151
|
+
status: res.status,
|
|
152
|
+
statusText: res.statusText,
|
|
153
|
+
body,
|
|
154
|
+
url
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return await res.json();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
get,
|
|
162
|
+
posts,
|
|
163
|
+
categories,
|
|
164
|
+
workspace,
|
|
165
|
+
navigations,
|
|
166
|
+
analytics,
|
|
167
|
+
submissions
|
|
168
|
+
};
|
|
136
169
|
}
|
|
137
170
|
|
|
138
171
|
// src/index.ts
|
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}\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 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 return { get, posts, categories, workspace, navigations, analytics }\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 InstallOptions,\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;AAgKA,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;AAEA,EAAA,OAAO;AAAA,IACL,GAAA;AAAA,IACA,KAAA;AAAA,IACA,UAAA;AAAA,IACA,SAAA;AAAA,IACA,WAAA;AAAA,IACA,SAAA;AAAA,IACA;AAAA,GACF;AACF;;;ACxZO,IAAM,WAAA,GAAc","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}\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\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 return {\n get,\n posts,\n categories,\n workspace,\n navigations,\n analytics,\n submissions,\n }\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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brandfine/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|