@botlr/sdk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @botlr/sdk
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e3ff8e7: Retry failed reads. GET requests that fail with a network error or a 408, 429 or 5xx status are retried twice with exponential backoff and `Retry-After` support before `BotlrError` is thrown. Configure or disable with the new `retry` client option, also accepted by `getBotlrForRoute` and `createBotlrFromEnv`. Writes are never retried.
8
+
3
9
  ## 0.2.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -23,6 +23,27 @@ Every request carries `Authorization: Bearer <apiKey>`, `x-botlr-app-id`,
23
23
  `x-botlr-locale` and `x-botlr-preview`. Non-2xx responses throw `BotlrError`
24
24
  with `status` and the parsed body in `details`.
25
25
 
26
+ ## Retries
27
+
28
+ Reads (GET) that fail with a network error or a 408, 429 or 5xx status are
29
+ retried twice with exponential backoff (300 ms, then 600 ms, ±25% jitter,
30
+ capped at 5 s) before the error is thrown. A `Retry-After` header wins over
31
+ the backoff. Writes such as form submissions and `sections.push` are never
32
+ retried, so nothing is submitted twice.
33
+
34
+ ```ts
35
+ const botlr = createBotlrClient({
36
+ ...config,
37
+ retry: {
38
+ retries: 3,
39
+ delay: 500,
40
+ onRetry: ({ attempt, delay, url, error }) => console.warn(`retry ${attempt} in ${delay}ms`, url, error),
41
+ },
42
+ });
43
+
44
+ createBotlrClient({ ...config, retry: false }); // one attempt only
45
+ ```
46
+
26
47
  ## Section source bridge
27
48
 
28
49
  `botlr.sections` talks to the section source bridge (`/api/sections/...`):
package/dist/index.cjs CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  BotlrConflictError: () => BotlrConflictError,
24
24
  BotlrError: () => BotlrError,
25
+ DEFAULT_RETRY_STATUSES: () => DEFAULT_RETRY_STATUSES,
25
26
  MANIFEST_STATE_KEYS: () => MANIFEST_STATE_KEYS,
26
27
  SECTION_FRAMEWORK_RE: () => SECTION_FRAMEWORK_RE,
27
28
  SECTION_PATH_RE: () => SECTION_PATH_RE,
@@ -64,6 +65,44 @@ var BotlrConflictError = class extends BotlrError {
64
65
  }
65
66
  };
66
67
 
68
+ // src/retry.ts
69
+ var DEFAULT_RETRY_STATUSES = [408, 429, 500, 502, 503, 504];
70
+ function resolveRetryConfig(retry) {
71
+ if (retry === false) {
72
+ return { retries: 0, delay: 0, maxDelay: 0, statuses: [], onRetry: void 0 };
73
+ }
74
+ return {
75
+ retries: Math.max(0, retry?.retries ?? 2),
76
+ delay: Math.max(0, retry?.delay ?? 300),
77
+ maxDelay: Math.max(0, retry?.maxDelay ?? 5e3),
78
+ statuses: retry?.statuses ?? DEFAULT_RETRY_STATUSES,
79
+ onRetry: retry?.onRetry
80
+ };
81
+ }
82
+ function isRetryableMethod(method) {
83
+ const m = (method ?? "GET").toUpperCase();
84
+ return m === "GET" || m === "HEAD";
85
+ }
86
+ function parseRetryAfter(value) {
87
+ if (!value) return void 0;
88
+ const seconds = Number(value);
89
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
90
+ const date = Date.parse(value);
91
+ if (Number.isFinite(date)) return Math.max(0, date - Date.now());
92
+ return void 0;
93
+ }
94
+ function retryDelayMs(retry, attempt, retryAfter) {
95
+ const fromHeader = parseRetryAfter(retryAfter);
96
+ if (fromHeader !== void 0) return Math.min(fromHeader, retry.maxDelay);
97
+ const base = Math.min(retry.maxDelay, retry.delay * 2 ** attempt);
98
+ return Math.round(base * (0.75 + Math.random() * 0.5));
99
+ }
100
+ async function waitBeforeRetry(retry, attempt, error, url, retryAfter) {
101
+ const delay = retryDelayMs(retry, attempt, retryAfter);
102
+ retry.onRetry?.({ attempt: attempt + 1, delay, url, error });
103
+ if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
104
+ }
105
+
67
106
  // src/client.ts
68
107
  function toQueryString(query) {
69
108
  const params = new URLSearchParams();
@@ -131,35 +170,42 @@ function createBotlrClient(config) {
131
170
  locale: config.locale,
132
171
  preview: config.preview
133
172
  };
173
+ const retry = resolveRetryConfig(config.retry);
134
174
  async function request(path, options = {}) {
135
175
  const { query, context: contextOverride, headers: extraHeaders, ...init } = options;
136
176
  const context = mergeContext(baseContext, contextOverride);
137
177
  const url = `${baseUrl}${path}${toQueryString(query ?? {})}`;
138
- const response = await fetcher(url, {
139
- ...init,
140
- headers: {
141
- Accept: "application/json",
142
- "Content-Type": "application/json",
143
- ...config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
144
- ...contextToHeaders(context),
145
- ...extraHeaders ?? {}
178
+ const headers = {
179
+ Accept: "application/json",
180
+ "Content-Type": "application/json",
181
+ ...config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
182
+ ...contextToHeaders(context),
183
+ ...extraHeaders ?? {}
184
+ };
185
+ const retryable = retry.retries > 0 && isRetryableMethod(init.method);
186
+ for (let attempt = 0; ; attempt++) {
187
+ const canRetry = retryable && attempt < retry.retries && !init.signal?.aborted;
188
+ let response;
189
+ try {
190
+ response = await fetcher(url, { ...init, headers });
191
+ } catch (error2) {
192
+ if (!canRetry) throw error2;
193
+ await waitBeforeRetry(retry, attempt, error2, url);
194
+ continue;
146
195
  }
147
- });
148
- if (!response.ok) {
149
- const details = await errorDetails(response);
150
- if (response.status === 409) {
151
- throw new BotlrConflictError(
152
- conflictMessage(details) ?? `Botlr request failed with status 409`,
153
- conflictLatest(details),
154
- details
155
- );
196
+ if (response.ok) {
197
+ if (response.status === 204) return void 0;
198
+ return await response.json();
156
199
  }
157
- throw new BotlrError(`Botlr request failed with status ${response.status}`, response.status, details);
158
- }
159
- if (response.status === 204) {
160
- return void 0;
200
+ const details = await errorDetails(response);
201
+ const error = response.status === 409 ? new BotlrConflictError(
202
+ conflictMessage(details) ?? `Botlr request failed with status 409`,
203
+ conflictLatest(details),
204
+ details
205
+ ) : new BotlrError(`Botlr request failed with status ${response.status}`, response.status, details);
206
+ if (!canRetry || !retry.statuses.includes(response.status)) throw error;
207
+ await waitBeforeRetry(retry, attempt, error, url, response.headers.get("retry-after"));
161
208
  }
162
- return await response.json();
163
209
  }
164
210
  const sections = {
165
211
  /** Every section the app can pull for `framework`, with the latest version per scope. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/client.ts","../src/sections.ts","../src/assets.ts"],"sourcesContent":["export * from \"./client\";\nexport * from \"./types\";\nexport * from \"./errors\";\nexport * from \"./sections\";\nexport * from \"./assets\";\n","/**\n * Thrown for every non-2xx response. `status` is the HTTP status and\n * `details` the parsed JSON body (or the raw text when the body is not JSON).\n */\nexport class BotlrError extends Error {\n readonly status: number | undefined;\n readonly details: unknown;\n\n constructor(message: string, status?: number, details?: unknown) {\n super(message);\n this.name = \"BotlrError\";\n this.status = status;\n this.details = details;\n }\n}\n\n/** The latest version of a section source, as reported on a push conflict. */\nexport type ConflictLatest = {\n version: number;\n contentHash: string;\n};\n\n/**\n * Thrown by `sections.push` on 409: `baseVersion` was not the latest version,\n * or a concurrent push won. `latest` is null when the server has no version\n * at all (the client sent a `baseVersion` above 0 for a section with no source).\n */\nexport class BotlrConflictError extends BotlrError {\n readonly latest: ConflictLatest | null;\n\n constructor(message: string, latest: ConflictLatest | null, details?: unknown) {\n super(message, 409, details);\n this.name = \"BotlrConflictError\";\n this.latest = latest;\n }\n}\n","import type {\n BotlrClientConfig,\n BotlrContext,\n BotlrEntry,\n BotlrQuery,\n BotlrQueryValue,\n FormResponse,\n GetEntriesParams,\n GetEntryBySlugParams,\n GetPageListParams,\n GetPageSlugListParams,\n GetSingletonParams,\n ListResponse,\n MenuResponse,\n PageListResponse,\n PageResponse,\n PageSlugListResponse,\n RedirectsResponse,\n ResolveResponse,\n} from \"./types\";\nimport type {\n SectionSourceListParams,\n SectionSourceListResponse,\n SectionSourcePullParams,\n SectionSourcePullResponse,\n SectionSourcePushBody,\n SectionSourcePushResponse,\n} from \"./sections\";\nimport { BotlrConflictError, BotlrError } from \"./errors\";\n\nexport type RequestOptions = Omit<RequestInit, \"headers\"> & {\n query?: BotlrQuery;\n context?: Partial<BotlrContext>;\n headers?: Record<string, string>;\n};\n\nexport function toQueryString(query: BotlrQuery): string {\n const params = new URLSearchParams();\n\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined || value === null) continue;\n\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null) {\n params.append(key, String(item));\n }\n }\n } else {\n params.set(key, String(value));\n }\n }\n\n const qs = params.toString();\n return qs ? `?${qs}` : \"\";\n}\n\nfunction mergeContext(\n base?: Partial<BotlrContext>,\n override?: Partial<BotlrContext>,\n): Partial<BotlrContext> | undefined {\n if (!base && !override) return undefined;\n return { ...base, ...override };\n}\n\nexport function contextToHeaders(context?: Partial<BotlrContext>): Record<string, string> {\n if (!context) return {};\n\n return {\n ...(context.appId ? { \"x-botlr-app-id\": context.appId } : {}),\n ...(context.locale ? { \"x-botlr-locale\": context.locale } : {}),\n ...(context.preview !== undefined ? { \"x-botlr-preview\": String(context.preview) } : {}),\n };\n}\n\nasync function errorDetails(response: Response): Promise<unknown> {\n const text = await response.text().catch(() => undefined);\n if (text === undefined || text === \"\") return undefined;\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return text;\n }\n}\n\nfunction conflictLatest(details: unknown): { version: number; contentHash: string } | null {\n if (!details || typeof details !== \"object\") return null;\n const detail = (details as { detail?: unknown }).detail;\n if (!detail || typeof detail !== \"object\") return null;\n const latest = (detail as { latest?: unknown }).latest;\n if (!latest || typeof latest !== \"object\") return null;\n const { version, contentHash } = latest as { version?: unknown; contentHash?: unknown };\n if (typeof version !== \"number\" || typeof contentHash !== \"string\") return null;\n return { version, contentHash };\n}\n\nfunction conflictMessage(details: unknown): string | undefined {\n if (!details || typeof details !== \"object\") return undefined;\n const detail = (details as { detail?: unknown }).detail;\n if (!detail || typeof detail !== \"object\") return undefined;\n const message = (detail as { message?: unknown }).message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nexport type BotlrClient = ReturnType<typeof createBotlrClient>;\n\n/**\n * A Botlr client. Every call sends the site token as `Authorization: Bearer`\n * plus `x-botlr-app-id`, `x-botlr-locale` and `x-botlr-preview` from the\n * config; `sections.push` sends the personal token instead.\n */\nexport function createBotlrClient(config: BotlrClientConfig) {\n const fetcher = config.fetch ?? globalThis.fetch;\n if (typeof fetcher !== \"function\") {\n throw new BotlrError(\"No fetch implementation: pass `fetch` in the client config\");\n }\n const baseUrl = config.baseUrl.replace(/\\/+$/, \"\");\n\n const baseContext: BotlrContext = {\n appId: config.appId,\n locale: config.locale,\n preview: config.preview,\n };\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const { query, context: contextOverride, headers: extraHeaders, ...init } = options;\n const context = mergeContext(baseContext, contextOverride);\n const url = `${baseUrl}${path}${toQueryString(query ?? {})}`;\n\n const response = await fetcher(url, {\n ...init,\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}),\n ...contextToHeaders(context),\n ...(extraHeaders ?? {}),\n },\n });\n\n if (!response.ok) {\n const details = await errorDetails(response);\n if (response.status === 409) {\n throw new BotlrConflictError(\n conflictMessage(details) ?? `Botlr request failed with status 409`,\n conflictLatest(details),\n details,\n );\n }\n throw new BotlrError(`Botlr request failed with status ${response.status}`, response.status, details);\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n return (await response.json()) as T;\n }\n\n const sections = {\n /** Every section the app can pull for `framework`, with the latest version per scope. */\n list(params: SectionSourceListParams): Promise<SectionSourceListResponse> {\n return request<SectionSourceListResponse>(\"/api/sections/source\", {\n method: \"GET\",\n query: { framework: params.framework, appInstanceId: baseContext.appId },\n });\n },\n\n /** The section's source: this app's row first, the shared row second. */\n pull(name: string, params: SectionSourcePullParams): Promise<SectionSourcePullResponse> {\n return request<SectionSourcePullResponse>(`/api/sections/${encodeURIComponent(name)}/source`, {\n method: \"GET\",\n query: {\n framework: params.framework,\n appInstanceId: baseContext.appId,\n version: params.version,\n scope: params.scope,\n },\n });\n },\n\n /**\n * Push a new version with the personal access token (`write:sections`).\n * Resolves with `created: true` on 201 and `created: false` when nothing\n * changed; throws `BotlrConflictError` when `baseVersion` is not the latest.\n */\n push(name: string, body: SectionSourcePushBody): Promise<SectionSourcePushResponse> {\n if (!config.personalToken) {\n return Promise.reject(\n new BotlrError(\"sections.push needs `personalToken` (a personal access token with write:sections)\"),\n );\n }\n const { appInstanceId, ...rest } = body;\n return request<SectionSourcePushResponse>(`/api/sections/${encodeURIComponent(name)}/source`, {\n method: \"PUT\",\n headers: { Authorization: `Bearer ${config.personalToken}` },\n body: JSON.stringify({ appInstanceId: appInstanceId ?? baseContext.appId, ...rest }),\n });\n },\n };\n\n return {\n request,\n\n /** The resolved config, for wrappers that need `baseUrl` or `appId`. */\n config: { ...config, baseUrl } as Readonly<BotlrClientConfig>,\n\n withContext(contextOverride: Partial<BotlrContext>) {\n return createBotlrClient({ ...config, ...contextOverride });\n },\n\n async getEntryBySlug<TFields>(params: GetEntryBySlugParams): Promise<BotlrEntry<TFields> | null> {\n const result = await request<{ item: BotlrEntry<TFields> | null }>(\"/entries/by-slug\", {\n method: \"GET\",\n query: { contentType: params.contentType, slug: params.slug, locale: params.locale },\n context: { locale: params.locale },\n });\n return result.item;\n },\n\n async getEntries<TFields>(\n params: GetEntriesParams,\n ): Promise<{ items: BotlrEntry<TFields>[]; total: number }> {\n const query: Record<string, BotlrQueryValue> = {\n contentType: params.contentType,\n locale: params.locale,\n limit: params.limit,\n offset: params.offset,\n sort: params.sort,\n };\n for (const [key, value] of Object.entries(params.filters ?? {})) {\n query[`filter.${key}`] = value;\n }\n return request<{ items: BotlrEntry<TFields>[]; total: number }>(\"/entries\", {\n method: \"GET\",\n query,\n context: { locale: params.locale },\n });\n },\n\n async getSingleton<TFields>(params: GetSingletonParams): Promise<BotlrEntry<TFields> | null> {\n const result = await request<{ item: BotlrEntry<TFields> | null }>(\"/singletons\", {\n method: \"GET\",\n query: { key: params.key, locale: params.locale },\n context: { locale: params.locale },\n });\n return result.item;\n },\n\n getPageBySlug(slug: string, locale?: string) {\n return request<PageResponse>(`/api/pages/${slug}`, {\n query: {\n appInstanceId: baseContext.appId,\n locale: locale || baseContext.locale,\n mode: baseContext.preview ? \"draft\" : \"published\",\n },\n });\n },\n\n resolveUrl(slug: string, locale?: string) {\n return request<ResolveResponse>(\"/api/resolve\", {\n query: {\n slug,\n appInstanceId: baseContext.appId,\n locale: locale || baseContext.locale,\n mode: baseContext.preview ? \"draft\" : \"published\",\n ...(baseContext.preview && config.previewToken ? { token: config.previewToken } : {}),\n },\n });\n },\n\n getPageList(params: GetPageListParams = {}) {\n return request<PageListResponse>(\"/api/page-lists\", {\n query: {\n appInstanceId: params.appInstanceId || baseContext.appId,\n locale: params.locale || baseContext.locale,\n parentId: params.parentId,\n tags: params.tags,\n limit: params.limit,\n offset: params.offset,\n type: params.type || \"page\",\n orderBy: params.orderBy,\n },\n });\n },\n\n getPageSlugList(params: GetPageSlugListParams = {}) {\n return request<PageSlugListResponse>(\"/api/page-lists/slugs\", {\n query: {\n appInstanceId: params.appInstanceId || baseContext.appId,\n locale: params.locale || baseContext.locale,\n limit: params.limit,\n offset: params.offset,\n },\n });\n },\n\n getMenu(key: string, locale?: string) {\n return request<MenuResponse>(\"/api/menus\", {\n query: { key, appInstanceId: baseContext.appId, locale: locale || baseContext.locale },\n });\n },\n\n getForm(key: string, locale?: string) {\n return request<FormResponse>(\"/api/forms\", {\n query: { key, appInstanceId: baseContext.appId, locale: locale || baseContext.locale },\n });\n },\n\n getList(appId: string, listId: string, locale?: string) {\n return request<ListResponse>(`/api/lists/${listId}/complete`, {\n query: { appInstanceId: appId, locale: locale || baseContext.locale },\n });\n },\n\n getConfigurator<T = unknown>(configuratorId: string, versionId?: string) {\n return request<T>(`/api/configurators/${configuratorId}`, {\n query: { versionId, appInstanceId: baseContext.appId, locale: baseContext.locale },\n });\n },\n\n /** GET `/api/{endpoint}` with the app and locale added to the query. */\n getData<T = unknown>(endpoint: string, queryParams?: BotlrQuery, locale?: string) {\n return request<T>(`/api/${endpoint}`, {\n method: \"GET\",\n query: { appInstanceId: baseContext.appId, locale: locale || baseContext.locale, ...queryParams },\n });\n },\n\n /** POST `data` as JSON to `/api/{endpoint}`, for forms and other submissions. */\n submitData<T = unknown>(\n endpoint: string,\n data: Record<string, unknown>,\n locale?: string,\n queryParams?: BotlrQuery,\n ) {\n return request<T>(`/api/${endpoint}`, {\n method: \"POST\",\n body: JSON.stringify(data),\n query: { appInstanceId: baseContext.appId, locale: locale || baseContext.locale, ...queryParams },\n });\n },\n\n getAppInfo<T = unknown>() {\n return request<T>(`/api/apps/${baseContext.appId}`);\n },\n\n getSitemap<T = unknown>() {\n return request<T>(`/api/sitemap/${baseContext.appId}`);\n },\n\n /** Every redirect for the app, across all locales. */\n getRedirects() {\n return request<RedirectsResponse>(`/api/redirects/${baseContext.appId}`);\n },\n\n getAssetById<T = unknown>(assetId: string) {\n return request<T>(`/api/assets/${assetId}`, {\n query: { appInstanceId: baseContext.appId },\n });\n },\n\n sections,\n };\n}\n\nexport function createBotlrFactory(defaults?: { fetch?: typeof fetch }) {\n return {\n create(config: Omit<BotlrClientConfig, \"fetch\">) {\n return createBotlrClient({ ...defaults, ...config });\n },\n };\n}\n","/**\n * Section source bridge: types and hash helpers.\n *\n * Mirrors the contract in xam-mono's docs/SECTION_SOURCE_BRIDGE.md. The hash\n * rules are reproduced here exactly as the server computes them, so a client\n * can tell \"clean\", \"modified\", \"behind\" and \"diverged\" apart without a\n * round trip:\n *\n * - `file.sha256` = sha256 of the UTF-8 content, hex, content exactly as sent\n * - `contentHash` = sha256 over `\"{path}\\n{sha256}\\n\"` for every file, sorted by path\n *\n * The manifest is not hashed.\n */\n\n/** The source variant a section is stored for, for example `react-next` or `html`. */\nexport type SectionFramework = string;\n\nexport type SectionSourceScope = \"instance\" | \"shared\";\n\n/** `<slug>.botlr.json`, kept next to the section's entry file. */\nexport interface SectionManifest {\n /** Section definition slug. Must equal the name used in the URL on push. */\n section: string;\n framework: SectionFramework;\n /** Entry file, relative to the manifest's directory. Must be listed in `files`. */\n entry: string;\n /** Named export of the component in `entry`. */\n exportName?: string;\n /** Every file of the section, relative to the manifest's directory, forward slashes only. */\n files: string[];\n title?: string;\n description?: string;\n category?: string;\n icon?: string;\n dependencies?: SectionDependencies;\n /** Unknown keys are kept by the server. */\n [key: string]: unknown;\n}\n\nexport interface SectionDependencies {\n /** npm packages the section imports, with the ranges the app uses. */\n npm?: Record<string, string>;\n /** Local modules the section imports; the pulling app must already have them. */\n internal?: string[];\n}\n\n/**\n * Pull state keys the server strips from a manifest. The CLI keeps these in\n * `.botlr/sections.lock.json`, never in the manifest.\n */\nexport const MANIFEST_STATE_KEYS = [\"version\", \"hash\", \"contentHash\", \"pulledAt\", \"scope\"] as const;\n\nexport interface SectionSourceFileInput {\n path: string;\n content: string;\n}\n\nexport interface SectionSourceFile extends SectionSourceFileInput {\n sha256: string;\n}\n\nexport interface SectionDefinitionScope {\n tenantId: string;\n organizationId: string | null;\n projectId: string | null;\n}\n\nexport interface SectionDefinition {\n id: string;\n name: string;\n title: string;\n schema: Record<string, unknown>;\n scope: SectionDefinitionScope;\n}\n\nexport interface SectionSource {\n scope: SectionSourceScope;\n framework: SectionFramework;\n version: number;\n contentHash: string;\n manifest: SectionManifest;\n message: string | null;\n createdAt: string;\n files: SectionSourceFile[];\n}\n\nexport interface SectionSourceScopeSummary {\n version: number;\n contentHash: string;\n updatedAt: string;\n}\n\nexport interface SectionSourceSummary {\n name: string;\n title: string;\n definitionId: string;\n instance: SectionSourceScopeSummary | null;\n shared: SectionSourceScopeSummary | null;\n /** Which scope a pull without `scope` returns. */\n effective: SectionSourceScope;\n}\n\nexport interface SectionSourceListResponse {\n framework: SectionFramework;\n sections: SectionSourceSummary[];\n}\n\nexport interface SectionSourcePullResponse {\n /** null only for an instance row whose section no longer resolves to a definition */\n definition: SectionDefinition | null;\n source: SectionSource;\n}\n\nexport interface SectionSourceListParams {\n framework: SectionFramework;\n}\n\nexport interface SectionSourcePullParams {\n framework: SectionFramework;\n /** A specific version; latest when omitted. */\n version?: number;\n /** Force one scope instead of \"instance first, shared second\". */\n scope?: SectionSourceScope;\n}\n\nexport interface SectionSourcePushBody {\n framework: SectionFramework;\n manifest: SectionManifest;\n files: SectionSourceFileInput[];\n /** Defaults to the client's `appId`. */\n appInstanceId?: string;\n /** Write the shared row (per section definition) instead of this app's row. */\n shared?: boolean;\n /**\n * The version the push is based on; 0 means \"none yet\". Omit to force the\n * push without a conflict check.\n */\n baseVersion?: number;\n /** At most 500 characters. */\n message?: string;\n /** Add a project-scoped section definition when none exists. */\n create?: boolean;\n}\n\nexport interface SectionSourcePushResponse {\n /** true on 201 (new version), false on 200 (nothing changed) */\n created: boolean;\n scope: SectionSourceScope;\n definition: SectionDefinition;\n version: number;\n contentHash: string;\n}\n\n// ---------------------------------------------------------------------------\n// Limits, as enforced by the server\n// ---------------------------------------------------------------------------\n\nexport const SECTION_SOURCE_LIMITS = {\n maxFiles: 50,\n maxFileBytes: 512 * 1024,\n maxTotalBytes: 2 * 1024 * 1024,\n maxPathLength: 200,\n maxMessageLength: 500,\n} as const;\n\nexport const SECTION_PATH_RE = /^[A-Za-z0-9._-]+(?:\\/[A-Za-z0-9._-]+)*$/;\nexport const SECTION_FRAMEWORK_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;\n\n/** True when `path` is a relative path the bridge accepts. */\nexport function isValidSectionPath(path: string): boolean {\n if (!path || path.length > SECTION_SOURCE_LIMITS.maxPathLength) return false;\n if (!SECTION_PATH_RE.test(path)) return false;\n return !path.split(\"/\").some((segment) => segment === \".\" || segment === \"..\");\n}\n\nexport function isValidFramework(framework: string): boolean {\n return SECTION_FRAMEWORK_RE.test(framework);\n}\n\n// ---------------------------------------------------------------------------\n// Hashing (Web Crypto, so it runs in the browser, Node 20+ and edge runtimes)\n// ---------------------------------------------------------------------------\n\nconst encoder = new TextEncoder();\n\nfunction subtle(): SubtleCrypto {\n const c = globalThis.crypto;\n if (!c || !c.subtle) {\n throw new Error(\"Web Crypto is not available in this runtime (globalThis.crypto.subtle)\");\n }\n return c.subtle;\n}\n\nfunction toHex(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let out = \"\";\n for (let i = 0; i < bytes.length; i++) {\n out += bytes[i]!.toString(16).padStart(2, \"0\");\n }\n return out;\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n return toHex(await subtle().digest(\"SHA-256\", encoder.encode(input)));\n}\n\n/** sha256 of the UTF-8 encoding of `content`, as lowercase hex. */\nexport function fileSha256(content: string): Promise<string> {\n return sha256Hex(content);\n}\n\n/**\n * The content hash of a file set: sha256 over `\"{path}\\n{sha256}\\n\"` for every\n * file, sorted by path. Accepts files with a precomputed `sha256` or plain\n * `{ path, content }` pairs.\n */\nexport async function contentHash(\n files: ReadonlyArray<SectionSourceFileInput | SectionSourceFile>,\n): Promise<string> {\n const hashed = await hashFiles(files);\n const lines = hashed\n .map((f) => [f.path, f.sha256] as const)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([path, sha]) => `${path}\\n${sha}\\n`)\n .join(\"\");\n return sha256Hex(lines);\n}\n\n/** Every file with its `sha256`, in the order given. Files that already carry one are kept as is. */\nexport async function hashFiles(\n files: ReadonlyArray<SectionSourceFileInput | SectionSourceFile>,\n): Promise<SectionSourceFile[]> {\n return Promise.all(\n files.map(async (f) => ({\n path: f.path,\n content: f.content,\n sha256: \"sha256\" in f && typeof f.sha256 === \"string\" ? f.sha256 : await fileSha256(f.content),\n })),\n );\n}\n\n/** A copy of `manifest` without the pull-state keys the server strips. */\nexport function stripManifestState<T extends Record<string, unknown>>(manifest: T): SectionManifest {\n const copy: Record<string, unknown> = { ...manifest };\n for (const key of MANIFEST_STATE_KEYS) delete copy[key];\n return copy as SectionManifest;\n}\n","import type { AssetData, PublisherScope } from \"./types\";\n\n/** An asset as a section prop: the legacy bare id, or the `AssetData` object. */\nexport type AssetRef = string | AssetData | null | undefined;\n\nexport function isAssetData(value: unknown): value is AssetData {\n return typeof value === \"object\" && value !== null && \"id\" in value && \"filename\" in value;\n}\n\nexport function getAssetId(asset: AssetRef): string | undefined {\n if (!asset) return undefined;\n return typeof asset === \"string\" ? asset : asset.id;\n}\n\n/** The filename, when the asset is an `AssetData` object. A bare id has none. */\nexport function getAssetFilename(asset: AssetRef): string | undefined {\n if (!asset || typeof asset === \"string\") return undefined;\n return asset.filename;\n}\n\nexport interface AssetUrlOptions {\n /** The media server origin, for example `https://media.example.com`. */\n mediaUrl: string;\n /** The image recipe segment; defaults to `view`. */\n recipe?: string;\n}\n\n/**\n * The media server URL of an asset:\n * `{mediaUrl}/images/{orgSlug}/{assetId}/{recipe}/{filename}`.\n *\n * Falls back to `image.jpg` for a bare id, and to `{mediaUrl}/images/{assetId}`\n * when no org slug is known.\n */\nexport function getAssetUrl(\n asset: AssetRef,\n scope: Partial<Pick<PublisherScope, \"orgSlug\">> | undefined,\n options: AssetUrlOptions,\n): string | undefined {\n const assetId = getAssetId(asset);\n if (!assetId) return undefined;\n\n const base = options.mediaUrl.replace(/\\/+$/, \"\");\n const recipe = options.recipe ?? \"view\";\n const filename = getAssetFilename(asset);\n\n if (scope?.orgSlug) {\n return `${base}/images/${scope.orgSlug}/${assetId}/${recipe}/${filename ?? \"image.jpg\"}`;\n }\n return `${base}/images/${assetId}`;\n}\n\n/**\n * `getAssetUrl` bound to a media server, for an app's own `getAssetUrl`:\n *\n * ```ts\n * export const getAssetUrl = createAssetUrl({ mediaUrl: process.env.NEXT_PUBLIC_MEDIA_URL! });\n * getAssetUrl(asset, scope, \"thumb\");\n * ```\n */\nexport function createAssetUrl(options: AssetUrlOptions) {\n return (\n asset: AssetRef,\n scope: Partial<Pick<PublisherScope, \"orgSlug\">> | undefined,\n recipe?: string,\n ): string | undefined => getAssetUrl(asset, scope, { ...options, recipe: recipe ?? options.recipe });\n}\n"],"mappings":"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","names":["contentHash"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/retry.ts","../src/client.ts","../src/sections.ts","../src/assets.ts"],"sourcesContent":["export * from \"./client\";\nexport * from \"./types\";\nexport * from \"./errors\";\nexport * from \"./sections\";\nexport * from \"./assets\";\nexport { DEFAULT_RETRY_STATUSES } from \"./retry\";\n","/**\n * Thrown for every non-2xx response. `status` is the HTTP status and\n * `details` the parsed JSON body (or the raw text when the body is not JSON).\n */\nexport class BotlrError extends Error {\n readonly status: number | undefined;\n readonly details: unknown;\n\n constructor(message: string, status?: number, details?: unknown) {\n super(message);\n this.name = \"BotlrError\";\n this.status = status;\n this.details = details;\n }\n}\n\n/** The latest version of a section source, as reported on a push conflict. */\nexport type ConflictLatest = {\n version: number;\n contentHash: string;\n};\n\n/**\n * Thrown by `sections.push` on 409: `baseVersion` was not the latest version,\n * or a concurrent push won. `latest` is null when the server has no version\n * at all (the client sent a `baseVersion` above 0 for a section with no source).\n */\nexport class BotlrConflictError extends BotlrError {\n readonly latest: ConflictLatest | null;\n\n constructor(message: string, latest: ConflictLatest | null, details?: unknown) {\n super(message, 409, details);\n this.name = \"BotlrConflictError\";\n this.latest = latest;\n }\n}\n","import type { BotlrRetryConfig, BotlrRetryInfo } from \"./types\";\n\n/** Statuses retried by default: timeouts, rate limits and server errors. */\nexport const DEFAULT_RETRY_STATUSES: readonly number[] = [408, 429, 500, 502, 503, 504];\n\nexport type ResolvedRetryConfig = {\n retries: number;\n delay: number;\n maxDelay: number;\n statuses: readonly number[];\n onRetry: ((info: BotlrRetryInfo) => void) | undefined;\n};\n\nexport function resolveRetryConfig(retry: BotlrRetryConfig | false | undefined): ResolvedRetryConfig {\n if (retry === false) {\n return { retries: 0, delay: 0, maxDelay: 0, statuses: [], onRetry: undefined };\n }\n return {\n retries: Math.max(0, retry?.retries ?? 2),\n delay: Math.max(0, retry?.delay ?? 300),\n maxDelay: Math.max(0, retry?.maxDelay ?? 5000),\n statuses: retry?.statuses ?? DEFAULT_RETRY_STATUSES,\n onRetry: retry?.onRetry,\n };\n}\n\n/** Only reads are retried: a repeated POST or PUT could submit twice. */\nexport function isRetryableMethod(method: string | undefined): boolean {\n const m = (method ?? \"GET\").toUpperCase();\n return m === \"GET\" || m === \"HEAD\";\n}\n\n/** `Retry-After` as milliseconds, from either the seconds or the date form. */\nexport function parseRetryAfter(value: string | null | undefined): number | undefined {\n if (!value) return undefined;\n const seconds = Number(value);\n if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);\n const date = Date.parse(value);\n if (Number.isFinite(date)) return Math.max(0, date - Date.now());\n return undefined;\n}\n\n/**\n * The wait before retry number `attempt + 1`: `Retry-After` when the server\n * sent one, else `delay * 2^attempt` with ±25% jitter, both capped at `maxDelay`.\n */\nexport function retryDelayMs(retry: ResolvedRetryConfig, attempt: number, retryAfter?: string | null): number {\n const fromHeader = parseRetryAfter(retryAfter);\n if (fromHeader !== undefined) return Math.min(fromHeader, retry.maxDelay);\n const base = Math.min(retry.maxDelay, retry.delay * 2 ** attempt);\n return Math.round(base * (0.75 + Math.random() * 0.5));\n}\n\nexport async function waitBeforeRetry(\n retry: ResolvedRetryConfig,\n attempt: number,\n error: unknown,\n url: string,\n retryAfter?: string | null,\n): Promise<void> {\n const delay = retryDelayMs(retry, attempt, retryAfter);\n retry.onRetry?.({ attempt: attempt + 1, delay, url, error });\n if (delay > 0) await new Promise<void>((resolve) => setTimeout(resolve, delay));\n}\n","import type {\n BotlrClientConfig,\n BotlrContext,\n BotlrEntry,\n BotlrQuery,\n BotlrQueryValue,\n FormResponse,\n GetEntriesParams,\n GetEntryBySlugParams,\n GetPageListParams,\n GetPageSlugListParams,\n GetSingletonParams,\n ListResponse,\n MenuResponse,\n PageListResponse,\n PageResponse,\n PageSlugListResponse,\n RedirectsResponse,\n ResolveResponse,\n} from \"./types\";\nimport type {\n SectionSourceListParams,\n SectionSourceListResponse,\n SectionSourcePullParams,\n SectionSourcePullResponse,\n SectionSourcePushBody,\n SectionSourcePushResponse,\n} from \"./sections\";\nimport { BotlrConflictError, BotlrError } from \"./errors\";\nimport { isRetryableMethod, resolveRetryConfig, waitBeforeRetry } from \"./retry\";\n\nexport type RequestOptions = Omit<RequestInit, \"headers\"> & {\n query?: BotlrQuery;\n context?: Partial<BotlrContext>;\n headers?: Record<string, string>;\n};\n\nexport function toQueryString(query: BotlrQuery): string {\n const params = new URLSearchParams();\n\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined || value === null) continue;\n\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null) {\n params.append(key, String(item));\n }\n }\n } else {\n params.set(key, String(value));\n }\n }\n\n const qs = params.toString();\n return qs ? `?${qs}` : \"\";\n}\n\nfunction mergeContext(\n base?: Partial<BotlrContext>,\n override?: Partial<BotlrContext>,\n): Partial<BotlrContext> | undefined {\n if (!base && !override) return undefined;\n return { ...base, ...override };\n}\n\nexport function contextToHeaders(context?: Partial<BotlrContext>): Record<string, string> {\n if (!context) return {};\n\n return {\n ...(context.appId ? { \"x-botlr-app-id\": context.appId } : {}),\n ...(context.locale ? { \"x-botlr-locale\": context.locale } : {}),\n ...(context.preview !== undefined ? { \"x-botlr-preview\": String(context.preview) } : {}),\n };\n}\n\nasync function errorDetails(response: Response): Promise<unknown> {\n const text = await response.text().catch(() => undefined);\n if (text === undefined || text === \"\") return undefined;\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return text;\n }\n}\n\nfunction conflictLatest(details: unknown): { version: number; contentHash: string } | null {\n if (!details || typeof details !== \"object\") return null;\n const detail = (details as { detail?: unknown }).detail;\n if (!detail || typeof detail !== \"object\") return null;\n const latest = (detail as { latest?: unknown }).latest;\n if (!latest || typeof latest !== \"object\") return null;\n const { version, contentHash } = latest as { version?: unknown; contentHash?: unknown };\n if (typeof version !== \"number\" || typeof contentHash !== \"string\") return null;\n return { version, contentHash };\n}\n\nfunction conflictMessage(details: unknown): string | undefined {\n if (!details || typeof details !== \"object\") return undefined;\n const detail = (details as { detail?: unknown }).detail;\n if (!detail || typeof detail !== \"object\") return undefined;\n const message = (detail as { message?: unknown }).message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nexport type BotlrClient = ReturnType<typeof createBotlrClient>;\n\n/**\n * A Botlr client. Every call sends the site token as `Authorization: Bearer`\n * plus `x-botlr-app-id`, `x-botlr-locale` and `x-botlr-preview` from the\n * config; `sections.push` sends the personal token instead.\n */\nexport function createBotlrClient(config: BotlrClientConfig) {\n const fetcher = config.fetch ?? globalThis.fetch;\n if (typeof fetcher !== \"function\") {\n throw new BotlrError(\"No fetch implementation: pass `fetch` in the client config\");\n }\n const baseUrl = config.baseUrl.replace(/\\/+$/, \"\");\n\n const baseContext: BotlrContext = {\n appId: config.appId,\n locale: config.locale,\n preview: config.preview,\n };\n\n const retry = resolveRetryConfig(config.retry);\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const { query, context: contextOverride, headers: extraHeaders, ...init } = options;\n const context = mergeContext(baseContext, contextOverride);\n const url = `${baseUrl}${path}${toQueryString(query ?? {})}`;\n const headers = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}),\n ...contextToHeaders(context),\n ...(extraHeaders ?? {}),\n };\n const retryable = retry.retries > 0 && isRetryableMethod(init.method);\n\n for (let attempt = 0; ; attempt++) {\n const canRetry = retryable && attempt < retry.retries && !init.signal?.aborted;\n\n let response: Response;\n try {\n response = await fetcher(url, { ...init, headers });\n } catch (error) {\n if (!canRetry) throw error;\n await waitBeforeRetry(retry, attempt, error, url);\n continue;\n }\n\n if (response.ok) {\n if (response.status === 204) return undefined as T;\n return (await response.json()) as T;\n }\n\n const details = await errorDetails(response);\n const error =\n response.status === 409\n ? new BotlrConflictError(\n conflictMessage(details) ?? `Botlr request failed with status 409`,\n conflictLatest(details),\n details,\n )\n : new BotlrError(`Botlr request failed with status ${response.status}`, response.status, details);\n\n if (!canRetry || !retry.statuses.includes(response.status)) throw error;\n await waitBeforeRetry(retry, attempt, error, url, response.headers.get(\"retry-after\"));\n }\n }\n\n const sections = {\n /** Every section the app can pull for `framework`, with the latest version per scope. */\n list(params: SectionSourceListParams): Promise<SectionSourceListResponse> {\n return request<SectionSourceListResponse>(\"/api/sections/source\", {\n method: \"GET\",\n query: { framework: params.framework, appInstanceId: baseContext.appId },\n });\n },\n\n /** The section's source: this app's row first, the shared row second. */\n pull(name: string, params: SectionSourcePullParams): Promise<SectionSourcePullResponse> {\n return request<SectionSourcePullResponse>(`/api/sections/${encodeURIComponent(name)}/source`, {\n method: \"GET\",\n query: {\n framework: params.framework,\n appInstanceId: baseContext.appId,\n version: params.version,\n scope: params.scope,\n },\n });\n },\n\n /**\n * Push a new version with the personal access token (`write:sections`).\n * Resolves with `created: true` on 201 and `created: false` when nothing\n * changed; throws `BotlrConflictError` when `baseVersion` is not the latest.\n */\n push(name: string, body: SectionSourcePushBody): Promise<SectionSourcePushResponse> {\n if (!config.personalToken) {\n return Promise.reject(\n new BotlrError(\"sections.push needs `personalToken` (a personal access token with write:sections)\"),\n );\n }\n const { appInstanceId, ...rest } = body;\n return request<SectionSourcePushResponse>(`/api/sections/${encodeURIComponent(name)}/source`, {\n method: \"PUT\",\n headers: { Authorization: `Bearer ${config.personalToken}` },\n body: JSON.stringify({ appInstanceId: appInstanceId ?? baseContext.appId, ...rest }),\n });\n },\n };\n\n return {\n request,\n\n /** The resolved config, for wrappers that need `baseUrl` or `appId`. */\n config: { ...config, baseUrl } as Readonly<BotlrClientConfig>,\n\n withContext(contextOverride: Partial<BotlrContext>) {\n return createBotlrClient({ ...config, ...contextOverride });\n },\n\n async getEntryBySlug<TFields>(params: GetEntryBySlugParams): Promise<BotlrEntry<TFields> | null> {\n const result = await request<{ item: BotlrEntry<TFields> | null }>(\"/entries/by-slug\", {\n method: \"GET\",\n query: { contentType: params.contentType, slug: params.slug, locale: params.locale },\n context: { locale: params.locale },\n });\n return result.item;\n },\n\n async getEntries<TFields>(\n params: GetEntriesParams,\n ): Promise<{ items: BotlrEntry<TFields>[]; total: number }> {\n const query: Record<string, BotlrQueryValue> = {\n contentType: params.contentType,\n locale: params.locale,\n limit: params.limit,\n offset: params.offset,\n sort: params.sort,\n };\n for (const [key, value] of Object.entries(params.filters ?? {})) {\n query[`filter.${key}`] = value;\n }\n return request<{ items: BotlrEntry<TFields>[]; total: number }>(\"/entries\", {\n method: \"GET\",\n query,\n context: { locale: params.locale },\n });\n },\n\n async getSingleton<TFields>(params: GetSingletonParams): Promise<BotlrEntry<TFields> | null> {\n const result = await request<{ item: BotlrEntry<TFields> | null }>(\"/singletons\", {\n method: \"GET\",\n query: { key: params.key, locale: params.locale },\n context: { locale: params.locale },\n });\n return result.item;\n },\n\n getPageBySlug(slug: string, locale?: string) {\n return request<PageResponse>(`/api/pages/${slug}`, {\n query: {\n appInstanceId: baseContext.appId,\n locale: locale || baseContext.locale,\n mode: baseContext.preview ? \"draft\" : \"published\",\n },\n });\n },\n\n resolveUrl(slug: string, locale?: string) {\n return request<ResolveResponse>(\"/api/resolve\", {\n query: {\n slug,\n appInstanceId: baseContext.appId,\n locale: locale || baseContext.locale,\n mode: baseContext.preview ? \"draft\" : \"published\",\n ...(baseContext.preview && config.previewToken ? { token: config.previewToken } : {}),\n },\n });\n },\n\n getPageList(params: GetPageListParams = {}) {\n return request<PageListResponse>(\"/api/page-lists\", {\n query: {\n appInstanceId: params.appInstanceId || baseContext.appId,\n locale: params.locale || baseContext.locale,\n parentId: params.parentId,\n tags: params.tags,\n limit: params.limit,\n offset: params.offset,\n type: params.type || \"page\",\n orderBy: params.orderBy,\n },\n });\n },\n\n getPageSlugList(params: GetPageSlugListParams = {}) {\n return request<PageSlugListResponse>(\"/api/page-lists/slugs\", {\n query: {\n appInstanceId: params.appInstanceId || baseContext.appId,\n locale: params.locale || baseContext.locale,\n limit: params.limit,\n offset: params.offset,\n },\n });\n },\n\n getMenu(key: string, locale?: string) {\n return request<MenuResponse>(\"/api/menus\", {\n query: { key, appInstanceId: baseContext.appId, locale: locale || baseContext.locale },\n });\n },\n\n getForm(key: string, locale?: string) {\n return request<FormResponse>(\"/api/forms\", {\n query: { key, appInstanceId: baseContext.appId, locale: locale || baseContext.locale },\n });\n },\n\n getList(appId: string, listId: string, locale?: string) {\n return request<ListResponse>(`/api/lists/${listId}/complete`, {\n query: { appInstanceId: appId, locale: locale || baseContext.locale },\n });\n },\n\n getConfigurator<T = unknown>(configuratorId: string, versionId?: string) {\n return request<T>(`/api/configurators/${configuratorId}`, {\n query: { versionId, appInstanceId: baseContext.appId, locale: baseContext.locale },\n });\n },\n\n /** GET `/api/{endpoint}` with the app and locale added to the query. */\n getData<T = unknown>(endpoint: string, queryParams?: BotlrQuery, locale?: string) {\n return request<T>(`/api/${endpoint}`, {\n method: \"GET\",\n query: { appInstanceId: baseContext.appId, locale: locale || baseContext.locale, ...queryParams },\n });\n },\n\n /** POST `data` as JSON to `/api/{endpoint}`, for forms and other submissions. */\n submitData<T = unknown>(\n endpoint: string,\n data: Record<string, unknown>,\n locale?: string,\n queryParams?: BotlrQuery,\n ) {\n return request<T>(`/api/${endpoint}`, {\n method: \"POST\",\n body: JSON.stringify(data),\n query: { appInstanceId: baseContext.appId, locale: locale || baseContext.locale, ...queryParams },\n });\n },\n\n getAppInfo<T = unknown>() {\n return request<T>(`/api/apps/${baseContext.appId}`);\n },\n\n getSitemap<T = unknown>() {\n return request<T>(`/api/sitemap/${baseContext.appId}`);\n },\n\n /** Every redirect for the app, across all locales. */\n getRedirects() {\n return request<RedirectsResponse>(`/api/redirects/${baseContext.appId}`);\n },\n\n getAssetById<T = unknown>(assetId: string) {\n return request<T>(`/api/assets/${assetId}`, {\n query: { appInstanceId: baseContext.appId },\n });\n },\n\n sections,\n };\n}\n\nexport function createBotlrFactory(defaults?: { fetch?: typeof fetch }) {\n return {\n create(config: Omit<BotlrClientConfig, \"fetch\">) {\n return createBotlrClient({ ...defaults, ...config });\n },\n };\n}\n","/**\n * Section source bridge: types and hash helpers.\n *\n * Mirrors the contract in xam-mono's docs/SECTION_SOURCE_BRIDGE.md. The hash\n * rules are reproduced here exactly as the server computes them, so a client\n * can tell \"clean\", \"modified\", \"behind\" and \"diverged\" apart without a\n * round trip:\n *\n * - `file.sha256` = sha256 of the UTF-8 content, hex, content exactly as sent\n * - `contentHash` = sha256 over `\"{path}\\n{sha256}\\n\"` for every file, sorted by path\n *\n * The manifest is not hashed.\n */\n\n/** The source variant a section is stored for, for example `react-next` or `html`. */\nexport type SectionFramework = string;\n\nexport type SectionSourceScope = \"instance\" | \"shared\";\n\n/** `<slug>.botlr.json`, kept next to the section's entry file. */\nexport interface SectionManifest {\n /** Section definition slug. Must equal the name used in the URL on push. */\n section: string;\n framework: SectionFramework;\n /** Entry file, relative to the manifest's directory. Must be listed in `files`. */\n entry: string;\n /** Named export of the component in `entry`. */\n exportName?: string;\n /** Every file of the section, relative to the manifest's directory, forward slashes only. */\n files: string[];\n title?: string;\n description?: string;\n category?: string;\n icon?: string;\n dependencies?: SectionDependencies;\n /** Unknown keys are kept by the server. */\n [key: string]: unknown;\n}\n\nexport interface SectionDependencies {\n /** npm packages the section imports, with the ranges the app uses. */\n npm?: Record<string, string>;\n /** Local modules the section imports; the pulling app must already have them. */\n internal?: string[];\n}\n\n/**\n * Pull state keys the server strips from a manifest. The CLI keeps these in\n * `.botlr/sections.lock.json`, never in the manifest.\n */\nexport const MANIFEST_STATE_KEYS = [\"version\", \"hash\", \"contentHash\", \"pulledAt\", \"scope\"] as const;\n\nexport interface SectionSourceFileInput {\n path: string;\n content: string;\n}\n\nexport interface SectionSourceFile extends SectionSourceFileInput {\n sha256: string;\n}\n\nexport interface SectionDefinitionScope {\n tenantId: string;\n organizationId: string | null;\n projectId: string | null;\n}\n\nexport interface SectionDefinition {\n id: string;\n name: string;\n title: string;\n schema: Record<string, unknown>;\n scope: SectionDefinitionScope;\n}\n\nexport interface SectionSource {\n scope: SectionSourceScope;\n framework: SectionFramework;\n version: number;\n contentHash: string;\n manifest: SectionManifest;\n message: string | null;\n createdAt: string;\n files: SectionSourceFile[];\n}\n\nexport interface SectionSourceScopeSummary {\n version: number;\n contentHash: string;\n updatedAt: string;\n}\n\nexport interface SectionSourceSummary {\n name: string;\n title: string;\n definitionId: string;\n instance: SectionSourceScopeSummary | null;\n shared: SectionSourceScopeSummary | null;\n /** Which scope a pull without `scope` returns. */\n effective: SectionSourceScope;\n}\n\nexport interface SectionSourceListResponse {\n framework: SectionFramework;\n sections: SectionSourceSummary[];\n}\n\nexport interface SectionSourcePullResponse {\n /** null only for an instance row whose section no longer resolves to a definition */\n definition: SectionDefinition | null;\n source: SectionSource;\n}\n\nexport interface SectionSourceListParams {\n framework: SectionFramework;\n}\n\nexport interface SectionSourcePullParams {\n framework: SectionFramework;\n /** A specific version; latest when omitted. */\n version?: number;\n /** Force one scope instead of \"instance first, shared second\". */\n scope?: SectionSourceScope;\n}\n\nexport interface SectionSourcePushBody {\n framework: SectionFramework;\n manifest: SectionManifest;\n files: SectionSourceFileInput[];\n /** Defaults to the client's `appId`. */\n appInstanceId?: string;\n /** Write the shared row (per section definition) instead of this app's row. */\n shared?: boolean;\n /**\n * The version the push is based on; 0 means \"none yet\". Omit to force the\n * push without a conflict check.\n */\n baseVersion?: number;\n /** At most 500 characters. */\n message?: string;\n /** Add a project-scoped section definition when none exists. */\n create?: boolean;\n}\n\nexport interface SectionSourcePushResponse {\n /** true on 201 (new version), false on 200 (nothing changed) */\n created: boolean;\n scope: SectionSourceScope;\n definition: SectionDefinition;\n version: number;\n contentHash: string;\n}\n\n// ---------------------------------------------------------------------------\n// Limits, as enforced by the server\n// ---------------------------------------------------------------------------\n\nexport const SECTION_SOURCE_LIMITS = {\n maxFiles: 50,\n maxFileBytes: 512 * 1024,\n maxTotalBytes: 2 * 1024 * 1024,\n maxPathLength: 200,\n maxMessageLength: 500,\n} as const;\n\nexport const SECTION_PATH_RE = /^[A-Za-z0-9._-]+(?:\\/[A-Za-z0-9._-]+)*$/;\nexport const SECTION_FRAMEWORK_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;\n\n/** True when `path` is a relative path the bridge accepts. */\nexport function isValidSectionPath(path: string): boolean {\n if (!path || path.length > SECTION_SOURCE_LIMITS.maxPathLength) return false;\n if (!SECTION_PATH_RE.test(path)) return false;\n return !path.split(\"/\").some((segment) => segment === \".\" || segment === \"..\");\n}\n\nexport function isValidFramework(framework: string): boolean {\n return SECTION_FRAMEWORK_RE.test(framework);\n}\n\n// ---------------------------------------------------------------------------\n// Hashing (Web Crypto, so it runs in the browser, Node 20+ and edge runtimes)\n// ---------------------------------------------------------------------------\n\nconst encoder = new TextEncoder();\n\nfunction subtle(): SubtleCrypto {\n const c = globalThis.crypto;\n if (!c || !c.subtle) {\n throw new Error(\"Web Crypto is not available in this runtime (globalThis.crypto.subtle)\");\n }\n return c.subtle;\n}\n\nfunction toHex(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let out = \"\";\n for (let i = 0; i < bytes.length; i++) {\n out += bytes[i]!.toString(16).padStart(2, \"0\");\n }\n return out;\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n return toHex(await subtle().digest(\"SHA-256\", encoder.encode(input)));\n}\n\n/** sha256 of the UTF-8 encoding of `content`, as lowercase hex. */\nexport function fileSha256(content: string): Promise<string> {\n return sha256Hex(content);\n}\n\n/**\n * The content hash of a file set: sha256 over `\"{path}\\n{sha256}\\n\"` for every\n * file, sorted by path. Accepts files with a precomputed `sha256` or plain\n * `{ path, content }` pairs.\n */\nexport async function contentHash(\n files: ReadonlyArray<SectionSourceFileInput | SectionSourceFile>,\n): Promise<string> {\n const hashed = await hashFiles(files);\n const lines = hashed\n .map((f) => [f.path, f.sha256] as const)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([path, sha]) => `${path}\\n${sha}\\n`)\n .join(\"\");\n return sha256Hex(lines);\n}\n\n/** Every file with its `sha256`, in the order given. Files that already carry one are kept as is. */\nexport async function hashFiles(\n files: ReadonlyArray<SectionSourceFileInput | SectionSourceFile>,\n): Promise<SectionSourceFile[]> {\n return Promise.all(\n files.map(async (f) => ({\n path: f.path,\n content: f.content,\n sha256: \"sha256\" in f && typeof f.sha256 === \"string\" ? f.sha256 : await fileSha256(f.content),\n })),\n );\n}\n\n/** A copy of `manifest` without the pull-state keys the server strips. */\nexport function stripManifestState<T extends Record<string, unknown>>(manifest: T): SectionManifest {\n const copy: Record<string, unknown> = { ...manifest };\n for (const key of MANIFEST_STATE_KEYS) delete copy[key];\n return copy as SectionManifest;\n}\n","import type { AssetData, PublisherScope } from \"./types\";\n\n/** An asset as a section prop: the legacy bare id, or the `AssetData` object. */\nexport type AssetRef = string | AssetData | null | undefined;\n\nexport function isAssetData(value: unknown): value is AssetData {\n return typeof value === \"object\" && value !== null && \"id\" in value && \"filename\" in value;\n}\n\nexport function getAssetId(asset: AssetRef): string | undefined {\n if (!asset) return undefined;\n return typeof asset === \"string\" ? asset : asset.id;\n}\n\n/** The filename, when the asset is an `AssetData` object. A bare id has none. */\nexport function getAssetFilename(asset: AssetRef): string | undefined {\n if (!asset || typeof asset === \"string\") return undefined;\n return asset.filename;\n}\n\nexport interface AssetUrlOptions {\n /** The media server origin, for example `https://media.example.com`. */\n mediaUrl: string;\n /** The image recipe segment; defaults to `view`. */\n recipe?: string;\n}\n\n/**\n * The media server URL of an asset:\n * `{mediaUrl}/images/{orgSlug}/{assetId}/{recipe}/{filename}`.\n *\n * Falls back to `image.jpg` for a bare id, and to `{mediaUrl}/images/{assetId}`\n * when no org slug is known.\n */\nexport function getAssetUrl(\n asset: AssetRef,\n scope: Partial<Pick<PublisherScope, \"orgSlug\">> | undefined,\n options: AssetUrlOptions,\n): string | undefined {\n const assetId = getAssetId(asset);\n if (!assetId) return undefined;\n\n const base = options.mediaUrl.replace(/\\/+$/, \"\");\n const recipe = options.recipe ?? \"view\";\n const filename = getAssetFilename(asset);\n\n if (scope?.orgSlug) {\n return `${base}/images/${scope.orgSlug}/${assetId}/${recipe}/${filename ?? \"image.jpg\"}`;\n }\n return `${base}/images/${assetId}`;\n}\n\n/**\n * `getAssetUrl` bound to a media server, for an app's own `getAssetUrl`:\n *\n * ```ts\n * export const getAssetUrl = createAssetUrl({ mediaUrl: process.env.NEXT_PUBLIC_MEDIA_URL! });\n * getAssetUrl(asset, scope, \"thumb\");\n * ```\n */\nexport function createAssetUrl(options: AssetUrlOptions) {\n return (\n asset: AssetRef,\n scope: Partial<Pick<PublisherScope, \"orgSlug\">> | undefined,\n recipe?: string,\n ): string | undefined => getAssetUrl(asset, scope, { ...options, recipe: recipe ?? options.recipe });\n}\n"],"mappings":"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","names":["contentHash","error"]}
package/dist/index.d.cts CHANGED
@@ -21,6 +21,33 @@ type BotlrClientConfig = {
21
21
  fetch?: typeof fetch;
22
22
  locale?: string;
23
23
  preview?: boolean;
24
+ /**
25
+ * Retry policy for GET requests that fail with a network error or a
26
+ * retryable status (408, 429 and 5xx by default). On by default with two
27
+ * retries; pass `false` to disable.
28
+ */
29
+ retry?: BotlrRetryConfig | false;
30
+ };
31
+ type BotlrRetryConfig = {
32
+ /** Retries after the first attempt. Default 2, so three requests in all. */
33
+ retries?: number;
34
+ /** Wait before the first retry in ms, doubling each time. Default 300. */
35
+ delay?: number;
36
+ /** Cap on a single wait in ms, `Retry-After` included. Default 5000. */
37
+ maxDelay?: number;
38
+ /** HTTP statuses that are retried. Default `[408, 429, 500, 502, 503, 504]`. */
39
+ statuses?: number[];
40
+ /** Called before each wait, for logging. */
41
+ onRetry?: (info: BotlrRetryInfo) => void;
42
+ };
43
+ type BotlrRetryInfo = {
44
+ /** 1 for the first retry. */
45
+ attempt: number;
46
+ /** The wait before this retry in ms. */
47
+ delay: number;
48
+ url: string;
49
+ /** The `BotlrError` for a bad status, or whatever `fetch` threw. */
50
+ error: unknown;
24
51
  };
25
52
  type BotlrQueryValue = string | number | boolean | null | undefined;
26
53
  type BotlrQuery = Record<string, BotlrQueryValue | BotlrQueryValue[]>;
@@ -658,4 +685,7 @@ declare function getAssetUrl(asset: AssetRef, scope: Partial<Pick<PublisherScope
658
685
  */
659
686
  declare function createAssetUrl(options: AssetUrlOptions): (asset: AssetRef, scope: Partial<Pick<PublisherScope, "orgSlug">> | undefined, recipe?: string) => string | undefined;
660
687
 
661
- export { type AlternateTranslation, type AssetData, type AssetRef, type AssetUrlOptions, type BotlrClient, type BotlrClientConfig, BotlrConflictError, type BotlrContext, type BotlrEntry, type BotlrEntryMeta, BotlrError, type BotlrQuery, type BotlrQueryValue, type BreadcrumbItem, type ConflictLatest, type ContentData, type FormResponse, type GetEntriesParams, type GetEntryBySlugParams, type GetPageListParams, type GetPageSlugListParams, type GetSingletonParams, type GridColumn, type GridContainerInstance, type InlineSectionInstance, type LayoutNode, type ListField, type ListResponse, MANIFEST_STATE_KEYS, type MenuResponse, type PageDiscoveryData, type PageListResponse, type PageResponse, type PageScope, type PageSlugListResponse, type PageTemplateData, type PublisherScope, type RedirectData, type RedirectEntry, type RedirectsResponse, type RequestOptions, type ResolveResponse, type ResponsiveGap, type ResponsiveSpan, SECTION_FRAMEWORK_RE, SECTION_PATH_RE, SECTION_SOURCE_LIMITS, type SectionDefinition, type SectionDefinitionScope, type SectionDependencies, type SectionFramework, type SectionManifest, type SectionSource, type SectionSourceFile, type SectionSourceFileInput, type SectionSourceListParams, type SectionSourceListResponse, type SectionSourcePullParams, type SectionSourcePullResponse, type SectionSourcePushBody, type SectionSourcePushResponse, type SectionSourceScope, type SectionSourceScopeSummary, type SectionSourceSummary, type SharedSectionInstance, type SpacingSize, type ThemeData, type VisibilityRules, contentHash, contextToHeaders, createAssetUrl, createBotlrClient, createBotlrFactory, fileSha256, getAssetFilename, getAssetId, getAssetUrl, hashFiles, isAssetData, isValidFramework, isValidSectionPath, stripManifestState, toQueryString };
688
+ /** Statuses retried by default: timeouts, rate limits and server errors. */
689
+ declare const DEFAULT_RETRY_STATUSES: readonly number[];
690
+
691
+ export { type AlternateTranslation, type AssetData, type AssetRef, type AssetUrlOptions, type BotlrClient, type BotlrClientConfig, BotlrConflictError, type BotlrContext, type BotlrEntry, type BotlrEntryMeta, BotlrError, type BotlrQuery, type BotlrQueryValue, type BotlrRetryConfig, type BotlrRetryInfo, type BreadcrumbItem, type ConflictLatest, type ContentData, DEFAULT_RETRY_STATUSES, type FormResponse, type GetEntriesParams, type GetEntryBySlugParams, type GetPageListParams, type GetPageSlugListParams, type GetSingletonParams, type GridColumn, type GridContainerInstance, type InlineSectionInstance, type LayoutNode, type ListField, type ListResponse, MANIFEST_STATE_KEYS, type MenuResponse, type PageDiscoveryData, type PageListResponse, type PageResponse, type PageScope, type PageSlugListResponse, type PageTemplateData, type PublisherScope, type RedirectData, type RedirectEntry, type RedirectsResponse, type RequestOptions, type ResolveResponse, type ResponsiveGap, type ResponsiveSpan, SECTION_FRAMEWORK_RE, SECTION_PATH_RE, SECTION_SOURCE_LIMITS, type SectionDefinition, type SectionDefinitionScope, type SectionDependencies, type SectionFramework, type SectionManifest, type SectionSource, type SectionSourceFile, type SectionSourceFileInput, type SectionSourceListParams, type SectionSourceListResponse, type SectionSourcePullParams, type SectionSourcePullResponse, type SectionSourcePushBody, type SectionSourcePushResponse, type SectionSourceScope, type SectionSourceScopeSummary, type SectionSourceSummary, type SharedSectionInstance, type SpacingSize, type ThemeData, type VisibilityRules, contentHash, contextToHeaders, createAssetUrl, createBotlrClient, createBotlrFactory, fileSha256, getAssetFilename, getAssetId, getAssetUrl, hashFiles, isAssetData, isValidFramework, isValidSectionPath, stripManifestState, toQueryString };
package/dist/index.d.ts CHANGED
@@ -21,6 +21,33 @@ type BotlrClientConfig = {
21
21
  fetch?: typeof fetch;
22
22
  locale?: string;
23
23
  preview?: boolean;
24
+ /**
25
+ * Retry policy for GET requests that fail with a network error or a
26
+ * retryable status (408, 429 and 5xx by default). On by default with two
27
+ * retries; pass `false` to disable.
28
+ */
29
+ retry?: BotlrRetryConfig | false;
30
+ };
31
+ type BotlrRetryConfig = {
32
+ /** Retries after the first attempt. Default 2, so three requests in all. */
33
+ retries?: number;
34
+ /** Wait before the first retry in ms, doubling each time. Default 300. */
35
+ delay?: number;
36
+ /** Cap on a single wait in ms, `Retry-After` included. Default 5000. */
37
+ maxDelay?: number;
38
+ /** HTTP statuses that are retried. Default `[408, 429, 500, 502, 503, 504]`. */
39
+ statuses?: number[];
40
+ /** Called before each wait, for logging. */
41
+ onRetry?: (info: BotlrRetryInfo) => void;
42
+ };
43
+ type BotlrRetryInfo = {
44
+ /** 1 for the first retry. */
45
+ attempt: number;
46
+ /** The wait before this retry in ms. */
47
+ delay: number;
48
+ url: string;
49
+ /** The `BotlrError` for a bad status, or whatever `fetch` threw. */
50
+ error: unknown;
24
51
  };
25
52
  type BotlrQueryValue = string | number | boolean | null | undefined;
26
53
  type BotlrQuery = Record<string, BotlrQueryValue | BotlrQueryValue[]>;
@@ -658,4 +685,7 @@ declare function getAssetUrl(asset: AssetRef, scope: Partial<Pick<PublisherScope
658
685
  */
659
686
  declare function createAssetUrl(options: AssetUrlOptions): (asset: AssetRef, scope: Partial<Pick<PublisherScope, "orgSlug">> | undefined, recipe?: string) => string | undefined;
660
687
 
661
- export { type AlternateTranslation, type AssetData, type AssetRef, type AssetUrlOptions, type BotlrClient, type BotlrClientConfig, BotlrConflictError, type BotlrContext, type BotlrEntry, type BotlrEntryMeta, BotlrError, type BotlrQuery, type BotlrQueryValue, type BreadcrumbItem, type ConflictLatest, type ContentData, type FormResponse, type GetEntriesParams, type GetEntryBySlugParams, type GetPageListParams, type GetPageSlugListParams, type GetSingletonParams, type GridColumn, type GridContainerInstance, type InlineSectionInstance, type LayoutNode, type ListField, type ListResponse, MANIFEST_STATE_KEYS, type MenuResponse, type PageDiscoveryData, type PageListResponse, type PageResponse, type PageScope, type PageSlugListResponse, type PageTemplateData, type PublisherScope, type RedirectData, type RedirectEntry, type RedirectsResponse, type RequestOptions, type ResolveResponse, type ResponsiveGap, type ResponsiveSpan, SECTION_FRAMEWORK_RE, SECTION_PATH_RE, SECTION_SOURCE_LIMITS, type SectionDefinition, type SectionDefinitionScope, type SectionDependencies, type SectionFramework, type SectionManifest, type SectionSource, type SectionSourceFile, type SectionSourceFileInput, type SectionSourceListParams, type SectionSourceListResponse, type SectionSourcePullParams, type SectionSourcePullResponse, type SectionSourcePushBody, type SectionSourcePushResponse, type SectionSourceScope, type SectionSourceScopeSummary, type SectionSourceSummary, type SharedSectionInstance, type SpacingSize, type ThemeData, type VisibilityRules, contentHash, contextToHeaders, createAssetUrl, createBotlrClient, createBotlrFactory, fileSha256, getAssetFilename, getAssetId, getAssetUrl, hashFiles, isAssetData, isValidFramework, isValidSectionPath, stripManifestState, toQueryString };
688
+ /** Statuses retried by default: timeouts, rate limits and server errors. */
689
+ declare const DEFAULT_RETRY_STATUSES: readonly number[];
690
+
691
+ export { type AlternateTranslation, type AssetData, type AssetRef, type AssetUrlOptions, type BotlrClient, type BotlrClientConfig, BotlrConflictError, type BotlrContext, type BotlrEntry, type BotlrEntryMeta, BotlrError, type BotlrQuery, type BotlrQueryValue, type BotlrRetryConfig, type BotlrRetryInfo, type BreadcrumbItem, type ConflictLatest, type ContentData, DEFAULT_RETRY_STATUSES, type FormResponse, type GetEntriesParams, type GetEntryBySlugParams, type GetPageListParams, type GetPageSlugListParams, type GetSingletonParams, type GridColumn, type GridContainerInstance, type InlineSectionInstance, type LayoutNode, type ListField, type ListResponse, MANIFEST_STATE_KEYS, type MenuResponse, type PageDiscoveryData, type PageListResponse, type PageResponse, type PageScope, type PageSlugListResponse, type PageTemplateData, type PublisherScope, type RedirectData, type RedirectEntry, type RedirectsResponse, type RequestOptions, type ResolveResponse, type ResponsiveGap, type ResponsiveSpan, SECTION_FRAMEWORK_RE, SECTION_PATH_RE, SECTION_SOURCE_LIMITS, type SectionDefinition, type SectionDefinitionScope, type SectionDependencies, type SectionFramework, type SectionManifest, type SectionSource, type SectionSourceFile, type SectionSourceFileInput, type SectionSourceListParams, type SectionSourceListResponse, type SectionSourcePullParams, type SectionSourcePullResponse, type SectionSourcePushBody, type SectionSourcePushResponse, type SectionSourceScope, type SectionSourceScopeSummary, type SectionSourceSummary, type SharedSectionInstance, type SpacingSize, type ThemeData, type VisibilityRules, contentHash, contextToHeaders, createAssetUrl, createBotlrClient, createBotlrFactory, fileSha256, getAssetFilename, getAssetId, getAssetUrl, hashFiles, isAssetData, isValidFramework, isValidSectionPath, stripManifestState, toQueryString };
package/dist/index.js CHANGED
@@ -18,6 +18,44 @@ var BotlrConflictError = class extends BotlrError {
18
18
  }
19
19
  };
20
20
 
21
+ // src/retry.ts
22
+ var DEFAULT_RETRY_STATUSES = [408, 429, 500, 502, 503, 504];
23
+ function resolveRetryConfig(retry) {
24
+ if (retry === false) {
25
+ return { retries: 0, delay: 0, maxDelay: 0, statuses: [], onRetry: void 0 };
26
+ }
27
+ return {
28
+ retries: Math.max(0, retry?.retries ?? 2),
29
+ delay: Math.max(0, retry?.delay ?? 300),
30
+ maxDelay: Math.max(0, retry?.maxDelay ?? 5e3),
31
+ statuses: retry?.statuses ?? DEFAULT_RETRY_STATUSES,
32
+ onRetry: retry?.onRetry
33
+ };
34
+ }
35
+ function isRetryableMethod(method) {
36
+ const m = (method ?? "GET").toUpperCase();
37
+ return m === "GET" || m === "HEAD";
38
+ }
39
+ function parseRetryAfter(value) {
40
+ if (!value) return void 0;
41
+ const seconds = Number(value);
42
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
43
+ const date = Date.parse(value);
44
+ if (Number.isFinite(date)) return Math.max(0, date - Date.now());
45
+ return void 0;
46
+ }
47
+ function retryDelayMs(retry, attempt, retryAfter) {
48
+ const fromHeader = parseRetryAfter(retryAfter);
49
+ if (fromHeader !== void 0) return Math.min(fromHeader, retry.maxDelay);
50
+ const base = Math.min(retry.maxDelay, retry.delay * 2 ** attempt);
51
+ return Math.round(base * (0.75 + Math.random() * 0.5));
52
+ }
53
+ async function waitBeforeRetry(retry, attempt, error, url, retryAfter) {
54
+ const delay = retryDelayMs(retry, attempt, retryAfter);
55
+ retry.onRetry?.({ attempt: attempt + 1, delay, url, error });
56
+ if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
57
+ }
58
+
21
59
  // src/client.ts
22
60
  function toQueryString(query) {
23
61
  const params = new URLSearchParams();
@@ -85,35 +123,42 @@ function createBotlrClient(config) {
85
123
  locale: config.locale,
86
124
  preview: config.preview
87
125
  };
126
+ const retry = resolveRetryConfig(config.retry);
88
127
  async function request(path, options = {}) {
89
128
  const { query, context: contextOverride, headers: extraHeaders, ...init } = options;
90
129
  const context = mergeContext(baseContext, contextOverride);
91
130
  const url = `${baseUrl}${path}${toQueryString(query ?? {})}`;
92
- const response = await fetcher(url, {
93
- ...init,
94
- headers: {
95
- Accept: "application/json",
96
- "Content-Type": "application/json",
97
- ...config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
98
- ...contextToHeaders(context),
99
- ...extraHeaders ?? {}
131
+ const headers = {
132
+ Accept: "application/json",
133
+ "Content-Type": "application/json",
134
+ ...config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
135
+ ...contextToHeaders(context),
136
+ ...extraHeaders ?? {}
137
+ };
138
+ const retryable = retry.retries > 0 && isRetryableMethod(init.method);
139
+ for (let attempt = 0; ; attempt++) {
140
+ const canRetry = retryable && attempt < retry.retries && !init.signal?.aborted;
141
+ let response;
142
+ try {
143
+ response = await fetcher(url, { ...init, headers });
144
+ } catch (error2) {
145
+ if (!canRetry) throw error2;
146
+ await waitBeforeRetry(retry, attempt, error2, url);
147
+ continue;
100
148
  }
101
- });
102
- if (!response.ok) {
103
- const details = await errorDetails(response);
104
- if (response.status === 409) {
105
- throw new BotlrConflictError(
106
- conflictMessage(details) ?? `Botlr request failed with status 409`,
107
- conflictLatest(details),
108
- details
109
- );
149
+ if (response.ok) {
150
+ if (response.status === 204) return void 0;
151
+ return await response.json();
110
152
  }
111
- throw new BotlrError(`Botlr request failed with status ${response.status}`, response.status, details);
112
- }
113
- if (response.status === 204) {
114
- return void 0;
153
+ const details = await errorDetails(response);
154
+ const error = response.status === 409 ? new BotlrConflictError(
155
+ conflictMessage(details) ?? `Botlr request failed with status 409`,
156
+ conflictLatest(details),
157
+ details
158
+ ) : new BotlrError(`Botlr request failed with status ${response.status}`, response.status, details);
159
+ if (!canRetry || !retry.statuses.includes(response.status)) throw error;
160
+ await waitBeforeRetry(retry, attempt, error, url, response.headers.get("retry-after"));
115
161
  }
116
- return await response.json();
117
162
  }
118
163
  const sections = {
119
164
  /** Every section the app can pull for `framework`, with the latest version per scope. */
@@ -391,6 +436,7 @@ function createAssetUrl(options) {
391
436
  export {
392
437
  BotlrConflictError,
393
438
  BotlrError,
439
+ DEFAULT_RETRY_STATUSES,
394
440
  MANIFEST_STATE_KEYS,
395
441
  SECTION_FRAMEWORK_RE,
396
442
  SECTION_PATH_RE,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/client.ts","../src/sections.ts","../src/assets.ts"],"sourcesContent":["/**\n * Thrown for every non-2xx response. `status` is the HTTP status and\n * `details` the parsed JSON body (or the raw text when the body is not JSON).\n */\nexport class BotlrError extends Error {\n readonly status: number | undefined;\n readonly details: unknown;\n\n constructor(message: string, status?: number, details?: unknown) {\n super(message);\n this.name = \"BotlrError\";\n this.status = status;\n this.details = details;\n }\n}\n\n/** The latest version of a section source, as reported on a push conflict. */\nexport type ConflictLatest = {\n version: number;\n contentHash: string;\n};\n\n/**\n * Thrown by `sections.push` on 409: `baseVersion` was not the latest version,\n * or a concurrent push won. `latest` is null when the server has no version\n * at all (the client sent a `baseVersion` above 0 for a section with no source).\n */\nexport class BotlrConflictError extends BotlrError {\n readonly latest: ConflictLatest | null;\n\n constructor(message: string, latest: ConflictLatest | null, details?: unknown) {\n super(message, 409, details);\n this.name = \"BotlrConflictError\";\n this.latest = latest;\n }\n}\n","import type {\n BotlrClientConfig,\n BotlrContext,\n BotlrEntry,\n BotlrQuery,\n BotlrQueryValue,\n FormResponse,\n GetEntriesParams,\n GetEntryBySlugParams,\n GetPageListParams,\n GetPageSlugListParams,\n GetSingletonParams,\n ListResponse,\n MenuResponse,\n PageListResponse,\n PageResponse,\n PageSlugListResponse,\n RedirectsResponse,\n ResolveResponse,\n} from \"./types\";\nimport type {\n SectionSourceListParams,\n SectionSourceListResponse,\n SectionSourcePullParams,\n SectionSourcePullResponse,\n SectionSourcePushBody,\n SectionSourcePushResponse,\n} from \"./sections\";\nimport { BotlrConflictError, BotlrError } from \"./errors\";\n\nexport type RequestOptions = Omit<RequestInit, \"headers\"> & {\n query?: BotlrQuery;\n context?: Partial<BotlrContext>;\n headers?: Record<string, string>;\n};\n\nexport function toQueryString(query: BotlrQuery): string {\n const params = new URLSearchParams();\n\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined || value === null) continue;\n\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null) {\n params.append(key, String(item));\n }\n }\n } else {\n params.set(key, String(value));\n }\n }\n\n const qs = params.toString();\n return qs ? `?${qs}` : \"\";\n}\n\nfunction mergeContext(\n base?: Partial<BotlrContext>,\n override?: Partial<BotlrContext>,\n): Partial<BotlrContext> | undefined {\n if (!base && !override) return undefined;\n return { ...base, ...override };\n}\n\nexport function contextToHeaders(context?: Partial<BotlrContext>): Record<string, string> {\n if (!context) return {};\n\n return {\n ...(context.appId ? { \"x-botlr-app-id\": context.appId } : {}),\n ...(context.locale ? { \"x-botlr-locale\": context.locale } : {}),\n ...(context.preview !== undefined ? { \"x-botlr-preview\": String(context.preview) } : {}),\n };\n}\n\nasync function errorDetails(response: Response): Promise<unknown> {\n const text = await response.text().catch(() => undefined);\n if (text === undefined || text === \"\") return undefined;\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return text;\n }\n}\n\nfunction conflictLatest(details: unknown): { version: number; contentHash: string } | null {\n if (!details || typeof details !== \"object\") return null;\n const detail = (details as { detail?: unknown }).detail;\n if (!detail || typeof detail !== \"object\") return null;\n const latest = (detail as { latest?: unknown }).latest;\n if (!latest || typeof latest !== \"object\") return null;\n const { version, contentHash } = latest as { version?: unknown; contentHash?: unknown };\n if (typeof version !== \"number\" || typeof contentHash !== \"string\") return null;\n return { version, contentHash };\n}\n\nfunction conflictMessage(details: unknown): string | undefined {\n if (!details || typeof details !== \"object\") return undefined;\n const detail = (details as { detail?: unknown }).detail;\n if (!detail || typeof detail !== \"object\") return undefined;\n const message = (detail as { message?: unknown }).message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nexport type BotlrClient = ReturnType<typeof createBotlrClient>;\n\n/**\n * A Botlr client. Every call sends the site token as `Authorization: Bearer`\n * plus `x-botlr-app-id`, `x-botlr-locale` and `x-botlr-preview` from the\n * config; `sections.push` sends the personal token instead.\n */\nexport function createBotlrClient(config: BotlrClientConfig) {\n const fetcher = config.fetch ?? globalThis.fetch;\n if (typeof fetcher !== \"function\") {\n throw new BotlrError(\"No fetch implementation: pass `fetch` in the client config\");\n }\n const baseUrl = config.baseUrl.replace(/\\/+$/, \"\");\n\n const baseContext: BotlrContext = {\n appId: config.appId,\n locale: config.locale,\n preview: config.preview,\n };\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const { query, context: contextOverride, headers: extraHeaders, ...init } = options;\n const context = mergeContext(baseContext, contextOverride);\n const url = `${baseUrl}${path}${toQueryString(query ?? {})}`;\n\n const response = await fetcher(url, {\n ...init,\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}),\n ...contextToHeaders(context),\n ...(extraHeaders ?? {}),\n },\n });\n\n if (!response.ok) {\n const details = await errorDetails(response);\n if (response.status === 409) {\n throw new BotlrConflictError(\n conflictMessage(details) ?? `Botlr request failed with status 409`,\n conflictLatest(details),\n details,\n );\n }\n throw new BotlrError(`Botlr request failed with status ${response.status}`, response.status, details);\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n return (await response.json()) as T;\n }\n\n const sections = {\n /** Every section the app can pull for `framework`, with the latest version per scope. */\n list(params: SectionSourceListParams): Promise<SectionSourceListResponse> {\n return request<SectionSourceListResponse>(\"/api/sections/source\", {\n method: \"GET\",\n query: { framework: params.framework, appInstanceId: baseContext.appId },\n });\n },\n\n /** The section's source: this app's row first, the shared row second. */\n pull(name: string, params: SectionSourcePullParams): Promise<SectionSourcePullResponse> {\n return request<SectionSourcePullResponse>(`/api/sections/${encodeURIComponent(name)}/source`, {\n method: \"GET\",\n query: {\n framework: params.framework,\n appInstanceId: baseContext.appId,\n version: params.version,\n scope: params.scope,\n },\n });\n },\n\n /**\n * Push a new version with the personal access token (`write:sections`).\n * Resolves with `created: true` on 201 and `created: false` when nothing\n * changed; throws `BotlrConflictError` when `baseVersion` is not the latest.\n */\n push(name: string, body: SectionSourcePushBody): Promise<SectionSourcePushResponse> {\n if (!config.personalToken) {\n return Promise.reject(\n new BotlrError(\"sections.push needs `personalToken` (a personal access token with write:sections)\"),\n );\n }\n const { appInstanceId, ...rest } = body;\n return request<SectionSourcePushResponse>(`/api/sections/${encodeURIComponent(name)}/source`, {\n method: \"PUT\",\n headers: { Authorization: `Bearer ${config.personalToken}` },\n body: JSON.stringify({ appInstanceId: appInstanceId ?? baseContext.appId, ...rest }),\n });\n },\n };\n\n return {\n request,\n\n /** The resolved config, for wrappers that need `baseUrl` or `appId`. */\n config: { ...config, baseUrl } as Readonly<BotlrClientConfig>,\n\n withContext(contextOverride: Partial<BotlrContext>) {\n return createBotlrClient({ ...config, ...contextOverride });\n },\n\n async getEntryBySlug<TFields>(params: GetEntryBySlugParams): Promise<BotlrEntry<TFields> | null> {\n const result = await request<{ item: BotlrEntry<TFields> | null }>(\"/entries/by-slug\", {\n method: \"GET\",\n query: { contentType: params.contentType, slug: params.slug, locale: params.locale },\n context: { locale: params.locale },\n });\n return result.item;\n },\n\n async getEntries<TFields>(\n params: GetEntriesParams,\n ): Promise<{ items: BotlrEntry<TFields>[]; total: number }> {\n const query: Record<string, BotlrQueryValue> = {\n contentType: params.contentType,\n locale: params.locale,\n limit: params.limit,\n offset: params.offset,\n sort: params.sort,\n };\n for (const [key, value] of Object.entries(params.filters ?? {})) {\n query[`filter.${key}`] = value;\n }\n return request<{ items: BotlrEntry<TFields>[]; total: number }>(\"/entries\", {\n method: \"GET\",\n query,\n context: { locale: params.locale },\n });\n },\n\n async getSingleton<TFields>(params: GetSingletonParams): Promise<BotlrEntry<TFields> | null> {\n const result = await request<{ item: BotlrEntry<TFields> | null }>(\"/singletons\", {\n method: \"GET\",\n query: { key: params.key, locale: params.locale },\n context: { locale: params.locale },\n });\n return result.item;\n },\n\n getPageBySlug(slug: string, locale?: string) {\n return request<PageResponse>(`/api/pages/${slug}`, {\n query: {\n appInstanceId: baseContext.appId,\n locale: locale || baseContext.locale,\n mode: baseContext.preview ? \"draft\" : \"published\",\n },\n });\n },\n\n resolveUrl(slug: string, locale?: string) {\n return request<ResolveResponse>(\"/api/resolve\", {\n query: {\n slug,\n appInstanceId: baseContext.appId,\n locale: locale || baseContext.locale,\n mode: baseContext.preview ? \"draft\" : \"published\",\n ...(baseContext.preview && config.previewToken ? { token: config.previewToken } : {}),\n },\n });\n },\n\n getPageList(params: GetPageListParams = {}) {\n return request<PageListResponse>(\"/api/page-lists\", {\n query: {\n appInstanceId: params.appInstanceId || baseContext.appId,\n locale: params.locale || baseContext.locale,\n parentId: params.parentId,\n tags: params.tags,\n limit: params.limit,\n offset: params.offset,\n type: params.type || \"page\",\n orderBy: params.orderBy,\n },\n });\n },\n\n getPageSlugList(params: GetPageSlugListParams = {}) {\n return request<PageSlugListResponse>(\"/api/page-lists/slugs\", {\n query: {\n appInstanceId: params.appInstanceId || baseContext.appId,\n locale: params.locale || baseContext.locale,\n limit: params.limit,\n offset: params.offset,\n },\n });\n },\n\n getMenu(key: string, locale?: string) {\n return request<MenuResponse>(\"/api/menus\", {\n query: { key, appInstanceId: baseContext.appId, locale: locale || baseContext.locale },\n });\n },\n\n getForm(key: string, locale?: string) {\n return request<FormResponse>(\"/api/forms\", {\n query: { key, appInstanceId: baseContext.appId, locale: locale || baseContext.locale },\n });\n },\n\n getList(appId: string, listId: string, locale?: string) {\n return request<ListResponse>(`/api/lists/${listId}/complete`, {\n query: { appInstanceId: appId, locale: locale || baseContext.locale },\n });\n },\n\n getConfigurator<T = unknown>(configuratorId: string, versionId?: string) {\n return request<T>(`/api/configurators/${configuratorId}`, {\n query: { versionId, appInstanceId: baseContext.appId, locale: baseContext.locale },\n });\n },\n\n /** GET `/api/{endpoint}` with the app and locale added to the query. */\n getData<T = unknown>(endpoint: string, queryParams?: BotlrQuery, locale?: string) {\n return request<T>(`/api/${endpoint}`, {\n method: \"GET\",\n query: { appInstanceId: baseContext.appId, locale: locale || baseContext.locale, ...queryParams },\n });\n },\n\n /** POST `data` as JSON to `/api/{endpoint}`, for forms and other submissions. */\n submitData<T = unknown>(\n endpoint: string,\n data: Record<string, unknown>,\n locale?: string,\n queryParams?: BotlrQuery,\n ) {\n return request<T>(`/api/${endpoint}`, {\n method: \"POST\",\n body: JSON.stringify(data),\n query: { appInstanceId: baseContext.appId, locale: locale || baseContext.locale, ...queryParams },\n });\n },\n\n getAppInfo<T = unknown>() {\n return request<T>(`/api/apps/${baseContext.appId}`);\n },\n\n getSitemap<T = unknown>() {\n return request<T>(`/api/sitemap/${baseContext.appId}`);\n },\n\n /** Every redirect for the app, across all locales. */\n getRedirects() {\n return request<RedirectsResponse>(`/api/redirects/${baseContext.appId}`);\n },\n\n getAssetById<T = unknown>(assetId: string) {\n return request<T>(`/api/assets/${assetId}`, {\n query: { appInstanceId: baseContext.appId },\n });\n },\n\n sections,\n };\n}\n\nexport function createBotlrFactory(defaults?: { fetch?: typeof fetch }) {\n return {\n create(config: Omit<BotlrClientConfig, \"fetch\">) {\n return createBotlrClient({ ...defaults, ...config });\n },\n };\n}\n","/**\n * Section source bridge: types and hash helpers.\n *\n * Mirrors the contract in xam-mono's docs/SECTION_SOURCE_BRIDGE.md. The hash\n * rules are reproduced here exactly as the server computes them, so a client\n * can tell \"clean\", \"modified\", \"behind\" and \"diverged\" apart without a\n * round trip:\n *\n * - `file.sha256` = sha256 of the UTF-8 content, hex, content exactly as sent\n * - `contentHash` = sha256 over `\"{path}\\n{sha256}\\n\"` for every file, sorted by path\n *\n * The manifest is not hashed.\n */\n\n/** The source variant a section is stored for, for example `react-next` or `html`. */\nexport type SectionFramework = string;\n\nexport type SectionSourceScope = \"instance\" | \"shared\";\n\n/** `<slug>.botlr.json`, kept next to the section's entry file. */\nexport interface SectionManifest {\n /** Section definition slug. Must equal the name used in the URL on push. */\n section: string;\n framework: SectionFramework;\n /** Entry file, relative to the manifest's directory. Must be listed in `files`. */\n entry: string;\n /** Named export of the component in `entry`. */\n exportName?: string;\n /** Every file of the section, relative to the manifest's directory, forward slashes only. */\n files: string[];\n title?: string;\n description?: string;\n category?: string;\n icon?: string;\n dependencies?: SectionDependencies;\n /** Unknown keys are kept by the server. */\n [key: string]: unknown;\n}\n\nexport interface SectionDependencies {\n /** npm packages the section imports, with the ranges the app uses. */\n npm?: Record<string, string>;\n /** Local modules the section imports; the pulling app must already have them. */\n internal?: string[];\n}\n\n/**\n * Pull state keys the server strips from a manifest. The CLI keeps these in\n * `.botlr/sections.lock.json`, never in the manifest.\n */\nexport const MANIFEST_STATE_KEYS = [\"version\", \"hash\", \"contentHash\", \"pulledAt\", \"scope\"] as const;\n\nexport interface SectionSourceFileInput {\n path: string;\n content: string;\n}\n\nexport interface SectionSourceFile extends SectionSourceFileInput {\n sha256: string;\n}\n\nexport interface SectionDefinitionScope {\n tenantId: string;\n organizationId: string | null;\n projectId: string | null;\n}\n\nexport interface SectionDefinition {\n id: string;\n name: string;\n title: string;\n schema: Record<string, unknown>;\n scope: SectionDefinitionScope;\n}\n\nexport interface SectionSource {\n scope: SectionSourceScope;\n framework: SectionFramework;\n version: number;\n contentHash: string;\n manifest: SectionManifest;\n message: string | null;\n createdAt: string;\n files: SectionSourceFile[];\n}\n\nexport interface SectionSourceScopeSummary {\n version: number;\n contentHash: string;\n updatedAt: string;\n}\n\nexport interface SectionSourceSummary {\n name: string;\n title: string;\n definitionId: string;\n instance: SectionSourceScopeSummary | null;\n shared: SectionSourceScopeSummary | null;\n /** Which scope a pull without `scope` returns. */\n effective: SectionSourceScope;\n}\n\nexport interface SectionSourceListResponse {\n framework: SectionFramework;\n sections: SectionSourceSummary[];\n}\n\nexport interface SectionSourcePullResponse {\n /** null only for an instance row whose section no longer resolves to a definition */\n definition: SectionDefinition | null;\n source: SectionSource;\n}\n\nexport interface SectionSourceListParams {\n framework: SectionFramework;\n}\n\nexport interface SectionSourcePullParams {\n framework: SectionFramework;\n /** A specific version; latest when omitted. */\n version?: number;\n /** Force one scope instead of \"instance first, shared second\". */\n scope?: SectionSourceScope;\n}\n\nexport interface SectionSourcePushBody {\n framework: SectionFramework;\n manifest: SectionManifest;\n files: SectionSourceFileInput[];\n /** Defaults to the client's `appId`. */\n appInstanceId?: string;\n /** Write the shared row (per section definition) instead of this app's row. */\n shared?: boolean;\n /**\n * The version the push is based on; 0 means \"none yet\". Omit to force the\n * push without a conflict check.\n */\n baseVersion?: number;\n /** At most 500 characters. */\n message?: string;\n /** Add a project-scoped section definition when none exists. */\n create?: boolean;\n}\n\nexport interface SectionSourcePushResponse {\n /** true on 201 (new version), false on 200 (nothing changed) */\n created: boolean;\n scope: SectionSourceScope;\n definition: SectionDefinition;\n version: number;\n contentHash: string;\n}\n\n// ---------------------------------------------------------------------------\n// Limits, as enforced by the server\n// ---------------------------------------------------------------------------\n\nexport const SECTION_SOURCE_LIMITS = {\n maxFiles: 50,\n maxFileBytes: 512 * 1024,\n maxTotalBytes: 2 * 1024 * 1024,\n maxPathLength: 200,\n maxMessageLength: 500,\n} as const;\n\nexport const SECTION_PATH_RE = /^[A-Za-z0-9._-]+(?:\\/[A-Za-z0-9._-]+)*$/;\nexport const SECTION_FRAMEWORK_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;\n\n/** True when `path` is a relative path the bridge accepts. */\nexport function isValidSectionPath(path: string): boolean {\n if (!path || path.length > SECTION_SOURCE_LIMITS.maxPathLength) return false;\n if (!SECTION_PATH_RE.test(path)) return false;\n return !path.split(\"/\").some((segment) => segment === \".\" || segment === \"..\");\n}\n\nexport function isValidFramework(framework: string): boolean {\n return SECTION_FRAMEWORK_RE.test(framework);\n}\n\n// ---------------------------------------------------------------------------\n// Hashing (Web Crypto, so it runs in the browser, Node 20+ and edge runtimes)\n// ---------------------------------------------------------------------------\n\nconst encoder = new TextEncoder();\n\nfunction subtle(): SubtleCrypto {\n const c = globalThis.crypto;\n if (!c || !c.subtle) {\n throw new Error(\"Web Crypto is not available in this runtime (globalThis.crypto.subtle)\");\n }\n return c.subtle;\n}\n\nfunction toHex(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let out = \"\";\n for (let i = 0; i < bytes.length; i++) {\n out += bytes[i]!.toString(16).padStart(2, \"0\");\n }\n return out;\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n return toHex(await subtle().digest(\"SHA-256\", encoder.encode(input)));\n}\n\n/** sha256 of the UTF-8 encoding of `content`, as lowercase hex. */\nexport function fileSha256(content: string): Promise<string> {\n return sha256Hex(content);\n}\n\n/**\n * The content hash of a file set: sha256 over `\"{path}\\n{sha256}\\n\"` for every\n * file, sorted by path. Accepts files with a precomputed `sha256` or plain\n * `{ path, content }` pairs.\n */\nexport async function contentHash(\n files: ReadonlyArray<SectionSourceFileInput | SectionSourceFile>,\n): Promise<string> {\n const hashed = await hashFiles(files);\n const lines = hashed\n .map((f) => [f.path, f.sha256] as const)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([path, sha]) => `${path}\\n${sha}\\n`)\n .join(\"\");\n return sha256Hex(lines);\n}\n\n/** Every file with its `sha256`, in the order given. Files that already carry one are kept as is. */\nexport async function hashFiles(\n files: ReadonlyArray<SectionSourceFileInput | SectionSourceFile>,\n): Promise<SectionSourceFile[]> {\n return Promise.all(\n files.map(async (f) => ({\n path: f.path,\n content: f.content,\n sha256: \"sha256\" in f && typeof f.sha256 === \"string\" ? f.sha256 : await fileSha256(f.content),\n })),\n );\n}\n\n/** A copy of `manifest` without the pull-state keys the server strips. */\nexport function stripManifestState<T extends Record<string, unknown>>(manifest: T): SectionManifest {\n const copy: Record<string, unknown> = { ...manifest };\n for (const key of MANIFEST_STATE_KEYS) delete copy[key];\n return copy as SectionManifest;\n}\n","import type { AssetData, PublisherScope } from \"./types\";\n\n/** An asset as a section prop: the legacy bare id, or the `AssetData` object. */\nexport type AssetRef = string | AssetData | null | undefined;\n\nexport function isAssetData(value: unknown): value is AssetData {\n return typeof value === \"object\" && value !== null && \"id\" in value && \"filename\" in value;\n}\n\nexport function getAssetId(asset: AssetRef): string | undefined {\n if (!asset) return undefined;\n return typeof asset === \"string\" ? asset : asset.id;\n}\n\n/** The filename, when the asset is an `AssetData` object. A bare id has none. */\nexport function getAssetFilename(asset: AssetRef): string | undefined {\n if (!asset || typeof asset === \"string\") return undefined;\n return asset.filename;\n}\n\nexport interface AssetUrlOptions {\n /** The media server origin, for example `https://media.example.com`. */\n mediaUrl: string;\n /** The image recipe segment; defaults to `view`. */\n recipe?: string;\n}\n\n/**\n * The media server URL of an asset:\n * `{mediaUrl}/images/{orgSlug}/{assetId}/{recipe}/{filename}`.\n *\n * Falls back to `image.jpg` for a bare id, and to `{mediaUrl}/images/{assetId}`\n * when no org slug is known.\n */\nexport function getAssetUrl(\n asset: AssetRef,\n scope: Partial<Pick<PublisherScope, \"orgSlug\">> | undefined,\n options: AssetUrlOptions,\n): string | undefined {\n const assetId = getAssetId(asset);\n if (!assetId) return undefined;\n\n const base = options.mediaUrl.replace(/\\/+$/, \"\");\n const recipe = options.recipe ?? \"view\";\n const filename = getAssetFilename(asset);\n\n if (scope?.orgSlug) {\n return `${base}/images/${scope.orgSlug}/${assetId}/${recipe}/${filename ?? \"image.jpg\"}`;\n }\n return `${base}/images/${assetId}`;\n}\n\n/**\n * `getAssetUrl` bound to a media server, for an app's own `getAssetUrl`:\n *\n * ```ts\n * export const getAssetUrl = createAssetUrl({ mediaUrl: process.env.NEXT_PUBLIC_MEDIA_URL! });\n * getAssetUrl(asset, scope, \"thumb\");\n * ```\n */\nexport function createAssetUrl(options: AssetUrlOptions) {\n return (\n asset: AssetRef,\n scope: Partial<Pick<PublisherScope, \"orgSlug\">> | undefined,\n recipe?: string,\n ): string | undefined => getAssetUrl(asset, scope, { ...options, recipe: recipe ?? options.recipe });\n}\n"],"mappings":"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","names":["contentHash"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/retry.ts","../src/client.ts","../src/sections.ts","../src/assets.ts"],"sourcesContent":["/**\n * Thrown for every non-2xx response. `status` is the HTTP status and\n * `details` the parsed JSON body (or the raw text when the body is not JSON).\n */\nexport class BotlrError extends Error {\n readonly status: number | undefined;\n readonly details: unknown;\n\n constructor(message: string, status?: number, details?: unknown) {\n super(message);\n this.name = \"BotlrError\";\n this.status = status;\n this.details = details;\n }\n}\n\n/** The latest version of a section source, as reported on a push conflict. */\nexport type ConflictLatest = {\n version: number;\n contentHash: string;\n};\n\n/**\n * Thrown by `sections.push` on 409: `baseVersion` was not the latest version,\n * or a concurrent push won. `latest` is null when the server has no version\n * at all (the client sent a `baseVersion` above 0 for a section with no source).\n */\nexport class BotlrConflictError extends BotlrError {\n readonly latest: ConflictLatest | null;\n\n constructor(message: string, latest: ConflictLatest | null, details?: unknown) {\n super(message, 409, details);\n this.name = \"BotlrConflictError\";\n this.latest = latest;\n }\n}\n","import type { BotlrRetryConfig, BotlrRetryInfo } from \"./types\";\n\n/** Statuses retried by default: timeouts, rate limits and server errors. */\nexport const DEFAULT_RETRY_STATUSES: readonly number[] = [408, 429, 500, 502, 503, 504];\n\nexport type ResolvedRetryConfig = {\n retries: number;\n delay: number;\n maxDelay: number;\n statuses: readonly number[];\n onRetry: ((info: BotlrRetryInfo) => void) | undefined;\n};\n\nexport function resolveRetryConfig(retry: BotlrRetryConfig | false | undefined): ResolvedRetryConfig {\n if (retry === false) {\n return { retries: 0, delay: 0, maxDelay: 0, statuses: [], onRetry: undefined };\n }\n return {\n retries: Math.max(0, retry?.retries ?? 2),\n delay: Math.max(0, retry?.delay ?? 300),\n maxDelay: Math.max(0, retry?.maxDelay ?? 5000),\n statuses: retry?.statuses ?? DEFAULT_RETRY_STATUSES,\n onRetry: retry?.onRetry,\n };\n}\n\n/** Only reads are retried: a repeated POST or PUT could submit twice. */\nexport function isRetryableMethod(method: string | undefined): boolean {\n const m = (method ?? \"GET\").toUpperCase();\n return m === \"GET\" || m === \"HEAD\";\n}\n\n/** `Retry-After` as milliseconds, from either the seconds or the date form. */\nexport function parseRetryAfter(value: string | null | undefined): number | undefined {\n if (!value) return undefined;\n const seconds = Number(value);\n if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);\n const date = Date.parse(value);\n if (Number.isFinite(date)) return Math.max(0, date - Date.now());\n return undefined;\n}\n\n/**\n * The wait before retry number `attempt + 1`: `Retry-After` when the server\n * sent one, else `delay * 2^attempt` with ±25% jitter, both capped at `maxDelay`.\n */\nexport function retryDelayMs(retry: ResolvedRetryConfig, attempt: number, retryAfter?: string | null): number {\n const fromHeader = parseRetryAfter(retryAfter);\n if (fromHeader !== undefined) return Math.min(fromHeader, retry.maxDelay);\n const base = Math.min(retry.maxDelay, retry.delay * 2 ** attempt);\n return Math.round(base * (0.75 + Math.random() * 0.5));\n}\n\nexport async function waitBeforeRetry(\n retry: ResolvedRetryConfig,\n attempt: number,\n error: unknown,\n url: string,\n retryAfter?: string | null,\n): Promise<void> {\n const delay = retryDelayMs(retry, attempt, retryAfter);\n retry.onRetry?.({ attempt: attempt + 1, delay, url, error });\n if (delay > 0) await new Promise<void>((resolve) => setTimeout(resolve, delay));\n}\n","import type {\n BotlrClientConfig,\n BotlrContext,\n BotlrEntry,\n BotlrQuery,\n BotlrQueryValue,\n FormResponse,\n GetEntriesParams,\n GetEntryBySlugParams,\n GetPageListParams,\n GetPageSlugListParams,\n GetSingletonParams,\n ListResponse,\n MenuResponse,\n PageListResponse,\n PageResponse,\n PageSlugListResponse,\n RedirectsResponse,\n ResolveResponse,\n} from \"./types\";\nimport type {\n SectionSourceListParams,\n SectionSourceListResponse,\n SectionSourcePullParams,\n SectionSourcePullResponse,\n SectionSourcePushBody,\n SectionSourcePushResponse,\n} from \"./sections\";\nimport { BotlrConflictError, BotlrError } from \"./errors\";\nimport { isRetryableMethod, resolveRetryConfig, waitBeforeRetry } from \"./retry\";\n\nexport type RequestOptions = Omit<RequestInit, \"headers\"> & {\n query?: BotlrQuery;\n context?: Partial<BotlrContext>;\n headers?: Record<string, string>;\n};\n\nexport function toQueryString(query: BotlrQuery): string {\n const params = new URLSearchParams();\n\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined || value === null) continue;\n\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null) {\n params.append(key, String(item));\n }\n }\n } else {\n params.set(key, String(value));\n }\n }\n\n const qs = params.toString();\n return qs ? `?${qs}` : \"\";\n}\n\nfunction mergeContext(\n base?: Partial<BotlrContext>,\n override?: Partial<BotlrContext>,\n): Partial<BotlrContext> | undefined {\n if (!base && !override) return undefined;\n return { ...base, ...override };\n}\n\nexport function contextToHeaders(context?: Partial<BotlrContext>): Record<string, string> {\n if (!context) return {};\n\n return {\n ...(context.appId ? { \"x-botlr-app-id\": context.appId } : {}),\n ...(context.locale ? { \"x-botlr-locale\": context.locale } : {}),\n ...(context.preview !== undefined ? { \"x-botlr-preview\": String(context.preview) } : {}),\n };\n}\n\nasync function errorDetails(response: Response): Promise<unknown> {\n const text = await response.text().catch(() => undefined);\n if (text === undefined || text === \"\") return undefined;\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return text;\n }\n}\n\nfunction conflictLatest(details: unknown): { version: number; contentHash: string } | null {\n if (!details || typeof details !== \"object\") return null;\n const detail = (details as { detail?: unknown }).detail;\n if (!detail || typeof detail !== \"object\") return null;\n const latest = (detail as { latest?: unknown }).latest;\n if (!latest || typeof latest !== \"object\") return null;\n const { version, contentHash } = latest as { version?: unknown; contentHash?: unknown };\n if (typeof version !== \"number\" || typeof contentHash !== \"string\") return null;\n return { version, contentHash };\n}\n\nfunction conflictMessage(details: unknown): string | undefined {\n if (!details || typeof details !== \"object\") return undefined;\n const detail = (details as { detail?: unknown }).detail;\n if (!detail || typeof detail !== \"object\") return undefined;\n const message = (detail as { message?: unknown }).message;\n return typeof message === \"string\" ? message : undefined;\n}\n\nexport type BotlrClient = ReturnType<typeof createBotlrClient>;\n\n/**\n * A Botlr client. Every call sends the site token as `Authorization: Bearer`\n * plus `x-botlr-app-id`, `x-botlr-locale` and `x-botlr-preview` from the\n * config; `sections.push` sends the personal token instead.\n */\nexport function createBotlrClient(config: BotlrClientConfig) {\n const fetcher = config.fetch ?? globalThis.fetch;\n if (typeof fetcher !== \"function\") {\n throw new BotlrError(\"No fetch implementation: pass `fetch` in the client config\");\n }\n const baseUrl = config.baseUrl.replace(/\\/+$/, \"\");\n\n const baseContext: BotlrContext = {\n appId: config.appId,\n locale: config.locale,\n preview: config.preview,\n };\n\n const retry = resolveRetryConfig(config.retry);\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const { query, context: contextOverride, headers: extraHeaders, ...init } = options;\n const context = mergeContext(baseContext, contextOverride);\n const url = `${baseUrl}${path}${toQueryString(query ?? {})}`;\n const headers = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}),\n ...contextToHeaders(context),\n ...(extraHeaders ?? {}),\n };\n const retryable = retry.retries > 0 && isRetryableMethod(init.method);\n\n for (let attempt = 0; ; attempt++) {\n const canRetry = retryable && attempt < retry.retries && !init.signal?.aborted;\n\n let response: Response;\n try {\n response = await fetcher(url, { ...init, headers });\n } catch (error) {\n if (!canRetry) throw error;\n await waitBeforeRetry(retry, attempt, error, url);\n continue;\n }\n\n if (response.ok) {\n if (response.status === 204) return undefined as T;\n return (await response.json()) as T;\n }\n\n const details = await errorDetails(response);\n const error =\n response.status === 409\n ? new BotlrConflictError(\n conflictMessage(details) ?? `Botlr request failed with status 409`,\n conflictLatest(details),\n details,\n )\n : new BotlrError(`Botlr request failed with status ${response.status}`, response.status, details);\n\n if (!canRetry || !retry.statuses.includes(response.status)) throw error;\n await waitBeforeRetry(retry, attempt, error, url, response.headers.get(\"retry-after\"));\n }\n }\n\n const sections = {\n /** Every section the app can pull for `framework`, with the latest version per scope. */\n list(params: SectionSourceListParams): Promise<SectionSourceListResponse> {\n return request<SectionSourceListResponse>(\"/api/sections/source\", {\n method: \"GET\",\n query: { framework: params.framework, appInstanceId: baseContext.appId },\n });\n },\n\n /** The section's source: this app's row first, the shared row second. */\n pull(name: string, params: SectionSourcePullParams): Promise<SectionSourcePullResponse> {\n return request<SectionSourcePullResponse>(`/api/sections/${encodeURIComponent(name)}/source`, {\n method: \"GET\",\n query: {\n framework: params.framework,\n appInstanceId: baseContext.appId,\n version: params.version,\n scope: params.scope,\n },\n });\n },\n\n /**\n * Push a new version with the personal access token (`write:sections`).\n * Resolves with `created: true` on 201 and `created: false` when nothing\n * changed; throws `BotlrConflictError` when `baseVersion` is not the latest.\n */\n push(name: string, body: SectionSourcePushBody): Promise<SectionSourcePushResponse> {\n if (!config.personalToken) {\n return Promise.reject(\n new BotlrError(\"sections.push needs `personalToken` (a personal access token with write:sections)\"),\n );\n }\n const { appInstanceId, ...rest } = body;\n return request<SectionSourcePushResponse>(`/api/sections/${encodeURIComponent(name)}/source`, {\n method: \"PUT\",\n headers: { Authorization: `Bearer ${config.personalToken}` },\n body: JSON.stringify({ appInstanceId: appInstanceId ?? baseContext.appId, ...rest }),\n });\n },\n };\n\n return {\n request,\n\n /** The resolved config, for wrappers that need `baseUrl` or `appId`. */\n config: { ...config, baseUrl } as Readonly<BotlrClientConfig>,\n\n withContext(contextOverride: Partial<BotlrContext>) {\n return createBotlrClient({ ...config, ...contextOverride });\n },\n\n async getEntryBySlug<TFields>(params: GetEntryBySlugParams): Promise<BotlrEntry<TFields> | null> {\n const result = await request<{ item: BotlrEntry<TFields> | null }>(\"/entries/by-slug\", {\n method: \"GET\",\n query: { contentType: params.contentType, slug: params.slug, locale: params.locale },\n context: { locale: params.locale },\n });\n return result.item;\n },\n\n async getEntries<TFields>(\n params: GetEntriesParams,\n ): Promise<{ items: BotlrEntry<TFields>[]; total: number }> {\n const query: Record<string, BotlrQueryValue> = {\n contentType: params.contentType,\n locale: params.locale,\n limit: params.limit,\n offset: params.offset,\n sort: params.sort,\n };\n for (const [key, value] of Object.entries(params.filters ?? {})) {\n query[`filter.${key}`] = value;\n }\n return request<{ items: BotlrEntry<TFields>[]; total: number }>(\"/entries\", {\n method: \"GET\",\n query,\n context: { locale: params.locale },\n });\n },\n\n async getSingleton<TFields>(params: GetSingletonParams): Promise<BotlrEntry<TFields> | null> {\n const result = await request<{ item: BotlrEntry<TFields> | null }>(\"/singletons\", {\n method: \"GET\",\n query: { key: params.key, locale: params.locale },\n context: { locale: params.locale },\n });\n return result.item;\n },\n\n getPageBySlug(slug: string, locale?: string) {\n return request<PageResponse>(`/api/pages/${slug}`, {\n query: {\n appInstanceId: baseContext.appId,\n locale: locale || baseContext.locale,\n mode: baseContext.preview ? \"draft\" : \"published\",\n },\n });\n },\n\n resolveUrl(slug: string, locale?: string) {\n return request<ResolveResponse>(\"/api/resolve\", {\n query: {\n slug,\n appInstanceId: baseContext.appId,\n locale: locale || baseContext.locale,\n mode: baseContext.preview ? \"draft\" : \"published\",\n ...(baseContext.preview && config.previewToken ? { token: config.previewToken } : {}),\n },\n });\n },\n\n getPageList(params: GetPageListParams = {}) {\n return request<PageListResponse>(\"/api/page-lists\", {\n query: {\n appInstanceId: params.appInstanceId || baseContext.appId,\n locale: params.locale || baseContext.locale,\n parentId: params.parentId,\n tags: params.tags,\n limit: params.limit,\n offset: params.offset,\n type: params.type || \"page\",\n orderBy: params.orderBy,\n },\n });\n },\n\n getPageSlugList(params: GetPageSlugListParams = {}) {\n return request<PageSlugListResponse>(\"/api/page-lists/slugs\", {\n query: {\n appInstanceId: params.appInstanceId || baseContext.appId,\n locale: params.locale || baseContext.locale,\n limit: params.limit,\n offset: params.offset,\n },\n });\n },\n\n getMenu(key: string, locale?: string) {\n return request<MenuResponse>(\"/api/menus\", {\n query: { key, appInstanceId: baseContext.appId, locale: locale || baseContext.locale },\n });\n },\n\n getForm(key: string, locale?: string) {\n return request<FormResponse>(\"/api/forms\", {\n query: { key, appInstanceId: baseContext.appId, locale: locale || baseContext.locale },\n });\n },\n\n getList(appId: string, listId: string, locale?: string) {\n return request<ListResponse>(`/api/lists/${listId}/complete`, {\n query: { appInstanceId: appId, locale: locale || baseContext.locale },\n });\n },\n\n getConfigurator<T = unknown>(configuratorId: string, versionId?: string) {\n return request<T>(`/api/configurators/${configuratorId}`, {\n query: { versionId, appInstanceId: baseContext.appId, locale: baseContext.locale },\n });\n },\n\n /** GET `/api/{endpoint}` with the app and locale added to the query. */\n getData<T = unknown>(endpoint: string, queryParams?: BotlrQuery, locale?: string) {\n return request<T>(`/api/${endpoint}`, {\n method: \"GET\",\n query: { appInstanceId: baseContext.appId, locale: locale || baseContext.locale, ...queryParams },\n });\n },\n\n /** POST `data` as JSON to `/api/{endpoint}`, for forms and other submissions. */\n submitData<T = unknown>(\n endpoint: string,\n data: Record<string, unknown>,\n locale?: string,\n queryParams?: BotlrQuery,\n ) {\n return request<T>(`/api/${endpoint}`, {\n method: \"POST\",\n body: JSON.stringify(data),\n query: { appInstanceId: baseContext.appId, locale: locale || baseContext.locale, ...queryParams },\n });\n },\n\n getAppInfo<T = unknown>() {\n return request<T>(`/api/apps/${baseContext.appId}`);\n },\n\n getSitemap<T = unknown>() {\n return request<T>(`/api/sitemap/${baseContext.appId}`);\n },\n\n /** Every redirect for the app, across all locales. */\n getRedirects() {\n return request<RedirectsResponse>(`/api/redirects/${baseContext.appId}`);\n },\n\n getAssetById<T = unknown>(assetId: string) {\n return request<T>(`/api/assets/${assetId}`, {\n query: { appInstanceId: baseContext.appId },\n });\n },\n\n sections,\n };\n}\n\nexport function createBotlrFactory(defaults?: { fetch?: typeof fetch }) {\n return {\n create(config: Omit<BotlrClientConfig, \"fetch\">) {\n return createBotlrClient({ ...defaults, ...config });\n },\n };\n}\n","/**\n * Section source bridge: types and hash helpers.\n *\n * Mirrors the contract in xam-mono's docs/SECTION_SOURCE_BRIDGE.md. The hash\n * rules are reproduced here exactly as the server computes them, so a client\n * can tell \"clean\", \"modified\", \"behind\" and \"diverged\" apart without a\n * round trip:\n *\n * - `file.sha256` = sha256 of the UTF-8 content, hex, content exactly as sent\n * - `contentHash` = sha256 over `\"{path}\\n{sha256}\\n\"` for every file, sorted by path\n *\n * The manifest is not hashed.\n */\n\n/** The source variant a section is stored for, for example `react-next` or `html`. */\nexport type SectionFramework = string;\n\nexport type SectionSourceScope = \"instance\" | \"shared\";\n\n/** `<slug>.botlr.json`, kept next to the section's entry file. */\nexport interface SectionManifest {\n /** Section definition slug. Must equal the name used in the URL on push. */\n section: string;\n framework: SectionFramework;\n /** Entry file, relative to the manifest's directory. Must be listed in `files`. */\n entry: string;\n /** Named export of the component in `entry`. */\n exportName?: string;\n /** Every file of the section, relative to the manifest's directory, forward slashes only. */\n files: string[];\n title?: string;\n description?: string;\n category?: string;\n icon?: string;\n dependencies?: SectionDependencies;\n /** Unknown keys are kept by the server. */\n [key: string]: unknown;\n}\n\nexport interface SectionDependencies {\n /** npm packages the section imports, with the ranges the app uses. */\n npm?: Record<string, string>;\n /** Local modules the section imports; the pulling app must already have them. */\n internal?: string[];\n}\n\n/**\n * Pull state keys the server strips from a manifest. The CLI keeps these in\n * `.botlr/sections.lock.json`, never in the manifest.\n */\nexport const MANIFEST_STATE_KEYS = [\"version\", \"hash\", \"contentHash\", \"pulledAt\", \"scope\"] as const;\n\nexport interface SectionSourceFileInput {\n path: string;\n content: string;\n}\n\nexport interface SectionSourceFile extends SectionSourceFileInput {\n sha256: string;\n}\n\nexport interface SectionDefinitionScope {\n tenantId: string;\n organizationId: string | null;\n projectId: string | null;\n}\n\nexport interface SectionDefinition {\n id: string;\n name: string;\n title: string;\n schema: Record<string, unknown>;\n scope: SectionDefinitionScope;\n}\n\nexport interface SectionSource {\n scope: SectionSourceScope;\n framework: SectionFramework;\n version: number;\n contentHash: string;\n manifest: SectionManifest;\n message: string | null;\n createdAt: string;\n files: SectionSourceFile[];\n}\n\nexport interface SectionSourceScopeSummary {\n version: number;\n contentHash: string;\n updatedAt: string;\n}\n\nexport interface SectionSourceSummary {\n name: string;\n title: string;\n definitionId: string;\n instance: SectionSourceScopeSummary | null;\n shared: SectionSourceScopeSummary | null;\n /** Which scope a pull without `scope` returns. */\n effective: SectionSourceScope;\n}\n\nexport interface SectionSourceListResponse {\n framework: SectionFramework;\n sections: SectionSourceSummary[];\n}\n\nexport interface SectionSourcePullResponse {\n /** null only for an instance row whose section no longer resolves to a definition */\n definition: SectionDefinition | null;\n source: SectionSource;\n}\n\nexport interface SectionSourceListParams {\n framework: SectionFramework;\n}\n\nexport interface SectionSourcePullParams {\n framework: SectionFramework;\n /** A specific version; latest when omitted. */\n version?: number;\n /** Force one scope instead of \"instance first, shared second\". */\n scope?: SectionSourceScope;\n}\n\nexport interface SectionSourcePushBody {\n framework: SectionFramework;\n manifest: SectionManifest;\n files: SectionSourceFileInput[];\n /** Defaults to the client's `appId`. */\n appInstanceId?: string;\n /** Write the shared row (per section definition) instead of this app's row. */\n shared?: boolean;\n /**\n * The version the push is based on; 0 means \"none yet\". Omit to force the\n * push without a conflict check.\n */\n baseVersion?: number;\n /** At most 500 characters. */\n message?: string;\n /** Add a project-scoped section definition when none exists. */\n create?: boolean;\n}\n\nexport interface SectionSourcePushResponse {\n /** true on 201 (new version), false on 200 (nothing changed) */\n created: boolean;\n scope: SectionSourceScope;\n definition: SectionDefinition;\n version: number;\n contentHash: string;\n}\n\n// ---------------------------------------------------------------------------\n// Limits, as enforced by the server\n// ---------------------------------------------------------------------------\n\nexport const SECTION_SOURCE_LIMITS = {\n maxFiles: 50,\n maxFileBytes: 512 * 1024,\n maxTotalBytes: 2 * 1024 * 1024,\n maxPathLength: 200,\n maxMessageLength: 500,\n} as const;\n\nexport const SECTION_PATH_RE = /^[A-Za-z0-9._-]+(?:\\/[A-Za-z0-9._-]+)*$/;\nexport const SECTION_FRAMEWORK_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;\n\n/** True when `path` is a relative path the bridge accepts. */\nexport function isValidSectionPath(path: string): boolean {\n if (!path || path.length > SECTION_SOURCE_LIMITS.maxPathLength) return false;\n if (!SECTION_PATH_RE.test(path)) return false;\n return !path.split(\"/\").some((segment) => segment === \".\" || segment === \"..\");\n}\n\nexport function isValidFramework(framework: string): boolean {\n return SECTION_FRAMEWORK_RE.test(framework);\n}\n\n// ---------------------------------------------------------------------------\n// Hashing (Web Crypto, so it runs in the browser, Node 20+ and edge runtimes)\n// ---------------------------------------------------------------------------\n\nconst encoder = new TextEncoder();\n\nfunction subtle(): SubtleCrypto {\n const c = globalThis.crypto;\n if (!c || !c.subtle) {\n throw new Error(\"Web Crypto is not available in this runtime (globalThis.crypto.subtle)\");\n }\n return c.subtle;\n}\n\nfunction toHex(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let out = \"\";\n for (let i = 0; i < bytes.length; i++) {\n out += bytes[i]!.toString(16).padStart(2, \"0\");\n }\n return out;\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n return toHex(await subtle().digest(\"SHA-256\", encoder.encode(input)));\n}\n\n/** sha256 of the UTF-8 encoding of `content`, as lowercase hex. */\nexport function fileSha256(content: string): Promise<string> {\n return sha256Hex(content);\n}\n\n/**\n * The content hash of a file set: sha256 over `\"{path}\\n{sha256}\\n\"` for every\n * file, sorted by path. Accepts files with a precomputed `sha256` or plain\n * `{ path, content }` pairs.\n */\nexport async function contentHash(\n files: ReadonlyArray<SectionSourceFileInput | SectionSourceFile>,\n): Promise<string> {\n const hashed = await hashFiles(files);\n const lines = hashed\n .map((f) => [f.path, f.sha256] as const)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([path, sha]) => `${path}\\n${sha}\\n`)\n .join(\"\");\n return sha256Hex(lines);\n}\n\n/** Every file with its `sha256`, in the order given. Files that already carry one are kept as is. */\nexport async function hashFiles(\n files: ReadonlyArray<SectionSourceFileInput | SectionSourceFile>,\n): Promise<SectionSourceFile[]> {\n return Promise.all(\n files.map(async (f) => ({\n path: f.path,\n content: f.content,\n sha256: \"sha256\" in f && typeof f.sha256 === \"string\" ? f.sha256 : await fileSha256(f.content),\n })),\n );\n}\n\n/** A copy of `manifest` without the pull-state keys the server strips. */\nexport function stripManifestState<T extends Record<string, unknown>>(manifest: T): SectionManifest {\n const copy: Record<string, unknown> = { ...manifest };\n for (const key of MANIFEST_STATE_KEYS) delete copy[key];\n return copy as SectionManifest;\n}\n","import type { AssetData, PublisherScope } from \"./types\";\n\n/** An asset as a section prop: the legacy bare id, or the `AssetData` object. */\nexport type AssetRef = string | AssetData | null | undefined;\n\nexport function isAssetData(value: unknown): value is AssetData {\n return typeof value === \"object\" && value !== null && \"id\" in value && \"filename\" in value;\n}\n\nexport function getAssetId(asset: AssetRef): string | undefined {\n if (!asset) return undefined;\n return typeof asset === \"string\" ? asset : asset.id;\n}\n\n/** The filename, when the asset is an `AssetData` object. A bare id has none. */\nexport function getAssetFilename(asset: AssetRef): string | undefined {\n if (!asset || typeof asset === \"string\") return undefined;\n return asset.filename;\n}\n\nexport interface AssetUrlOptions {\n /** The media server origin, for example `https://media.example.com`. */\n mediaUrl: string;\n /** The image recipe segment; defaults to `view`. */\n recipe?: string;\n}\n\n/**\n * The media server URL of an asset:\n * `{mediaUrl}/images/{orgSlug}/{assetId}/{recipe}/{filename}`.\n *\n * Falls back to `image.jpg` for a bare id, and to `{mediaUrl}/images/{assetId}`\n * when no org slug is known.\n */\nexport function getAssetUrl(\n asset: AssetRef,\n scope: Partial<Pick<PublisherScope, \"orgSlug\">> | undefined,\n options: AssetUrlOptions,\n): string | undefined {\n const assetId = getAssetId(asset);\n if (!assetId) return undefined;\n\n const base = options.mediaUrl.replace(/\\/+$/, \"\");\n const recipe = options.recipe ?? \"view\";\n const filename = getAssetFilename(asset);\n\n if (scope?.orgSlug) {\n return `${base}/images/${scope.orgSlug}/${assetId}/${recipe}/${filename ?? \"image.jpg\"}`;\n }\n return `${base}/images/${assetId}`;\n}\n\n/**\n * `getAssetUrl` bound to a media server, for an app's own `getAssetUrl`:\n *\n * ```ts\n * export const getAssetUrl = createAssetUrl({ mediaUrl: process.env.NEXT_PUBLIC_MEDIA_URL! });\n * getAssetUrl(asset, scope, \"thumb\");\n * ```\n */\nexport function createAssetUrl(options: AssetUrlOptions) {\n return (\n asset: AssetRef,\n scope: Partial<Pick<PublisherScope, \"orgSlug\">> | undefined,\n recipe?: string,\n ): string | undefined => getAssetUrl(asset, scope, { ...options, recipe: recipe ?? options.recipe });\n}\n"],"mappings":"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","names":["contentHash","error"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botlr/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Fetch-only Botlr client and shared types. Works in the browser, Node and edge runtimes.",
5
5
  "license": "MIT",
6
6
  "repository": {