@botlr/sdk 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @botlr/sdk
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `botlr schema push` follows the server's merge behaviour for FormBuilder schemas instead of asking for `--force`.
8
+
9
+ - Field-DSL `schema` in the manifest pushed at a definition whose dashboard schema is in the FormBuilder format is merged by the server: a node per field the dashboard lacks, every existing node kept with its id, label and settings, nothing removed. The CLI's diff header says so (`the server will add the missing fields as FormBuilder nodes and keep the dashboard's (format kept; --force would replace it)`), lists the dashboard's extra nodes as "dashboard only, kept", asks as before, and then prints what the server did: `merged schema of <name> (form-builder): added ...; kept ...`, plus `the dashboard also defines: ... (not in the code)` when the dashboard has nodes the code does not declare. Same format: `replaced schema of <name>`. `changed: false`: `nothing to change`.
10
+ - A FormBuilder `schema` pushed at a field-DSL definition still needs `--force`: the CLI stops right after the diff with the hint, without asking or sending anything (the server would answer 409). `--force` replaces the schema outright whatever the formats, and the question says which format replaces which.
11
+ - `@botlr/sdk`: `SectionSchemaPutResponse` gains `mode: "replaced" | "merged"` (the `SectionSchemaPushMode` type), `added` and `dashboardOnly`.
12
+
13
+ ## 0.5.0
14
+
15
+ ### Minor Changes
16
+
17
+ - `botlr schema pull` and `botlr schema push`: the explicit way to resolve props schema drift in either direction. A code push still never changes the schema.
18
+
19
+ - `botlr schema pull <name...>` (or `--all` for every manifest in `sectionsDir`) fetches the section definition's props schema from the dashboard and writes it as `schema` into `<name>.botlr.json`, in the CLI's fixed key order and in whichever format the dashboard has (field DSL or FormBuilder). It says "unchanged" when the manifest already holds the same schema, and regenerates `<name>.props.ts` afterwards unless `--no-types` or `"types": false`.
20
+ - `botlr schema push <name> [--force] [--yes]` reads `schema` from the manifest (an error when it is missing), fetches the dashboard's current schema, prints both formats and a diff of the field keys (added, removed, kept; node names for FormBuilder schemas), asks for confirmation unless `--yes`, then replaces the dashboard's schema (needs `BOTLR_PAT`). When the two schemas are in different formats the server answers 409 and the CLI prints both formats and says to rerun with `--force`; a 422 (not an object of fields, or the definition is not project-scoped) prints the server's message. A successful push regenerates the props file.
21
+ - The drift warning of `push` and `sections status` now names both commands: `botlr schema pull <name>` to take the dashboard's schema, `botlr schema push <name>` to send the code's. A manifest `schema` in the FormBuilder format now takes part in the drift comparison too (by node names), since `schema pull` can write one.
22
+ - `@botlr/sdk`: `client.sections.getSchema(name)` (site token, `GET /api/sections/{name}/schema`) and `client.sections.putSchema(name, { schema, force })` (personal token, `PUT /api/sections/{name}/schema`), with the `SectionSchemaFormat`, `SectionSchemaGetResponse`, `SectionSchemaPutBody` and `SectionSchemaPutResponse` types, `schemaFormat(schema)`, `SectionDefinition.format`, `BotlrSchemaFormatConflictError` (the 409 with `currentFormat` and `newFormat`) and `botlrErrorMessage(error)` to read a server's `detail` message. `SectionManifest.schema` is typed as `SectionSchema` (either format).
23
+
3
24
  ## 0.4.0
4
25
 
5
26
  ### Minor Changes
package/README.md CHANGED
@@ -66,12 +66,34 @@ await writer.sections.push("hero", {
66
66
  A pull also returns `definition`: the section definition with its props
67
67
  `schema` as the dashboard stores it, in either the field DSL (a map of
68
68
  field key to `{ type, label, required, options, ... }`) or the older
69
- FormBuilder format (`{ nodes: [...] }`). `isFieldSchema` and
70
- `isFormBuilderSchema` tell the two apart; `@botlr/cli` generates prop types
71
- from it. The dashboard owns that schema. A manifest may carry its own
72
- `schema` (field DSL only) as the props the code expects: the server uses it
73
- as the seed when `push` creates a definition, and the CLI compares it with
74
- the definition's schema to warn about drift.
69
+ FormBuilder format (`{ nodes: [...] }`). `isFieldSchema`,
70
+ `isFormBuilderSchema` and `schemaFormat` (`"field-dsl" | "form-builder"`,
71
+ the `SectionSchemaFormat` type) tell the two apart; `@botlr/cli` generates
72
+ prop types from it. The dashboard owns that schema: a source push never
73
+ changes it. A manifest may carry its own `schema` as the props the code
74
+ expects: the server uses it as the seed when `push` creates a definition,
75
+ and the CLI compares it with the definition's schema to warn about drift.
76
+
77
+ The schema itself moves through two dedicated calls, which work whether or
78
+ not source has been pushed:
79
+
80
+ ```ts
81
+ // site token: the definition with its schema and format
82
+ const { definition } = await botlr.sections.getSchema("hero");
83
+ definition.format; // "field-dsl" | "form-builder"
84
+
85
+ // personal token: replace the dashboard's schema
86
+ const result = await writer.sections.putSchema("hero", { schema, force: false });
87
+ result.changed; // false when the stored schema already equalled `schema`
88
+ result.previousFormat;
89
+ ```
90
+
91
+ `putSchema` throws `BotlrSchemaFormatConflictError` (status 409, with
92
+ `currentFormat` and `newFormat`) when the stored schema is in the other
93
+ format and `force` is not set, and a `BotlrError` with status 422 when the
94
+ schema is not an object of field definitions or the definition is not
95
+ project-scoped. `botlrErrorMessage(error)` returns the server's `detail`
96
+ message for any `BotlrError`, when there is one.
75
97
 
76
98
  `fileSha256(content)` and `contentHash(files)` reproduce the server's hash
77
99
  rules exactly (sha256 hex of the UTF-8 content; content hash = sha256 over
package/dist/index.cjs CHANGED
@@ -22,11 +22,13 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  BotlrConflictError: () => BotlrConflictError,
24
24
  BotlrError: () => BotlrError,
25
+ BotlrSchemaFormatConflictError: () => BotlrSchemaFormatConflictError,
25
26
  DEFAULT_RETRY_STATUSES: () => DEFAULT_RETRY_STATUSES,
26
27
  MANIFEST_STATE_KEYS: () => MANIFEST_STATE_KEYS,
27
28
  SECTION_FRAMEWORK_RE: () => SECTION_FRAMEWORK_RE,
28
29
  SECTION_PATH_RE: () => SECTION_PATH_RE,
29
30
  SECTION_SOURCE_LIMITS: () => SECTION_SOURCE_LIMITS,
31
+ botlrErrorMessage: () => botlrErrorMessage,
30
32
  contentHash: () => contentHash,
31
33
  contextToHeaders: () => contextToHeaders,
32
34
  createAssetUrl: () => createAssetUrl,
@@ -42,6 +44,7 @@ __export(index_exports, {
42
44
  isFormBuilderSchema: () => isFormBuilderSchema,
43
45
  isValidFramework: () => isValidFramework,
44
46
  isValidSectionPath: () => isValidSectionPath,
47
+ schemaFormat: () => schemaFormat,
45
48
  stripManifestState: () => stripManifestState,
46
49
  toQueryString: () => toQueryString
47
50
  });
@@ -66,6 +69,32 @@ var BotlrConflictError = class extends BotlrError {
66
69
  this.latest = latest;
67
70
  }
68
71
  };
72
+ var BotlrSchemaFormatConflictError = class extends BotlrError {
73
+ currentFormat;
74
+ newFormat;
75
+ constructor(message, currentFormat, newFormat, details) {
76
+ super(message, 409, details);
77
+ this.name = "BotlrSchemaFormatConflictError";
78
+ this.currentFormat = currentFormat;
79
+ this.newFormat = newFormat;
80
+ }
81
+ };
82
+ function botlrErrorMessage(error) {
83
+ if (!(error instanceof BotlrError)) return void 0;
84
+ const details = error.details;
85
+ if (!details || typeof details !== "object") return typeof details === "string" ? details : void 0;
86
+ const detail = details.detail;
87
+ if (typeof detail === "string") return detail;
88
+ if (Array.isArray(detail)) {
89
+ const lines = detail.map((item) => item && typeof item === "object" ? item.msg : void 0).filter((msg) => typeof msg === "string");
90
+ return lines.length ? lines.join("; ") : void 0;
91
+ }
92
+ if (detail && typeof detail === "object") {
93
+ const message = detail.message;
94
+ return typeof message === "string" ? message : void 0;
95
+ }
96
+ return void 0;
97
+ }
69
98
 
70
99
  // src/retry.ts
71
100
  var DEFAULT_RETRY_STATUSES = [408, 429, 500, 502, 503, 504];
@@ -154,6 +183,14 @@ function conflictLatest(details) {
154
183
  if (typeof version !== "number" || typeof contentHash2 !== "string") return null;
155
184
  return { version, contentHash: contentHash2 };
156
185
  }
186
+ function conflictFormats(details) {
187
+ if (!details || typeof details !== "object") return null;
188
+ const detail = details.detail;
189
+ if (!detail || typeof detail !== "object") return null;
190
+ const { currentFormat, newFormat } = detail;
191
+ if (typeof currentFormat !== "string" || typeof newFormat !== "string") return null;
192
+ return { currentFormat, newFormat };
193
+ }
157
194
  function conflictMessage(details) {
158
195
  if (!details || typeof details !== "object") return void 0;
159
196
  const detail = details.detail;
@@ -246,6 +283,49 @@ function createBotlrClient(config) {
246
283
  headers: { Authorization: `Bearer ${config.personalToken}` },
247
284
  body: JSON.stringify({ appInstanceId: appInstanceId ?? baseContext.appId, ...rest })
248
285
  });
286
+ },
287
+ /**
288
+ * The section definition with its props schema and format, with the site
289
+ * token. Works whether or not source has been pushed; 404 when the app has
290
+ * no section type of that name.
291
+ */
292
+ getSchema(name) {
293
+ return request(`/api/sections/${encodeURIComponent(name)}/schema`, {
294
+ method: "GET",
295
+ query: { appInstanceId: baseContext.appId }
296
+ });
297
+ },
298
+ /**
299
+ * Write the definition's props schema with the personal access token
300
+ * (`write:sections`). Same format, or `force`: the schema is replaced
301
+ * (`mode: "replaced"`). Field-DSL fields at a FormBuilder schema without
302
+ * `force`: the missing fields are added as nodes and nothing is removed
303
+ * (`mode: "merged"`, with `added` and `dashboardOnly`). A FormBuilder
304
+ * schema at a field-DSL one without `force` throws
305
+ * `BotlrSchemaFormatConflictError`; a `BotlrError` with status 422 means
306
+ * the schema is not an object of fields or the definition is not
307
+ * project-scoped.
308
+ */
309
+ async putSchema(name, body) {
310
+ if (!config.personalToken) {
311
+ throw new BotlrError("sections.putSchema needs `personalToken` (a personal access token with write:sections)");
312
+ }
313
+ const { appInstanceId, schema, force } = body;
314
+ try {
315
+ return await request(`/api/sections/${encodeURIComponent(name)}/schema`, {
316
+ method: "PUT",
317
+ headers: { Authorization: `Bearer ${config.personalToken}` },
318
+ body: JSON.stringify({ appInstanceId: appInstanceId ?? baseContext.appId, schema, force: force ?? false })
319
+ });
320
+ } catch (error) {
321
+ if (error instanceof BotlrConflictError) {
322
+ const formats = conflictFormats(error.details);
323
+ if (formats) {
324
+ throw new BotlrSchemaFormatConflictError(error.message, formats.currentFormat, formats.newFormat, error.details);
325
+ }
326
+ }
327
+ throw error;
328
+ }
249
329
  }
250
330
  };
251
331
  return {
@@ -401,6 +481,11 @@ function isFormBuilderSchema(schema) {
401
481
  function isFieldSchema(schema) {
402
482
  return !!schema && typeof schema === "object" && !Array.isArray(schema) && !isFormBuilderSchema(schema);
403
483
  }
484
+ function schemaFormat(schema) {
485
+ if (isFormBuilderSchema(schema)) return "form-builder";
486
+ if (isFieldSchema(schema)) return "field-dsl";
487
+ return null;
488
+ }
404
489
  var SECTION_SOURCE_LIMITS = {
405
490
  maxFiles: 50,
406
491
  maxFileBytes: 512 * 1024,
@@ -1 +1 @@
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 /**\n * The props the code expects, in the field DSL. The dashboard owns the\n * definition's schema; this is only a seed when `--create` adds the\n * definition, and what the CLI compares against for a drift warning.\n */\n schema?: SectionFieldSchema;\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\n/**\n * One field of the dashboard's props schema (the field DSL): the value under\n * a field key in `SectionDefinition.schema`. Other keys are kept as given.\n */\nexport interface SectionSchemaField {\n type: string;\n label?: string;\n required?: boolean;\n /** For `select`: plain values or `{ value, label }` objects. */\n options?: Array<string | number | { value: string | number; label?: string }>;\n min?: number;\n max?: number;\n multiple?: boolean;\n [key: string]: unknown;\n}\n\n/** The field DSL: a map of field key to field. This is what the dashboard's properties editor stores. */\nexport type SectionFieldSchema = Record<string, SectionSchemaField>;\n\n/**\n * A node of the older FormBuilder schema format. A `field` node carries its\n * field type in `fieldType`; `section`, `fieldset` and `composite` nodes\n * group `children`; a `repeater` node lists its item `fields`.\n */\nexport interface SectionFormBuilderNode {\n type: string;\n fieldType?: string;\n name?: string;\n key?: string;\n label?: string;\n required?: boolean;\n options?: SectionSchemaField[\"options\"];\n children?: SectionFormBuilderNode[];\n fields?: SectionFormBuilderNode[];\n [key: string]: unknown;\n}\n\n/** The older FormBuilder schema format. */\nexport interface SectionFormBuilderSchema {\n nodes: SectionFormBuilderNode[];\n [key: string]: unknown;\n}\n\n/** A section definition's props schema, in either format the dashboard writes. */\nexport type SectionSchema = SectionFieldSchema | SectionFormBuilderSchema;\n\n/** True for the FormBuilder format (`{ nodes: [...] }`). */\nexport function isFormBuilderSchema(schema: unknown): schema is SectionFormBuilderSchema {\n return !!schema && typeof schema === \"object\" && Array.isArray((schema as { nodes?: unknown }).nodes);\n}\n\n/** True for the field DSL: an object that is not the FormBuilder format. Empty objects count. */\nexport function isFieldSchema(schema: unknown): schema is SectionFieldSchema {\n return !!schema && typeof schema === \"object\" && !Array.isArray(schema) && !isFormBuilderSchema(schema);\n}\n\nexport interface SectionDefinition {\n id: string;\n name: string;\n title: string;\n /** The props schema as the dashboard stores it; `{}` for a definition without one. */\n schema: SectionSchema | 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"]}
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\n/**\n * Thrown by `sections.putSchema` on 409: the definition's current schema and\n * the new one are in different formats and `force` was not set.\n */\nexport class BotlrSchemaFormatConflictError extends BotlrError {\n readonly currentFormat: string;\n readonly newFormat: string;\n\n constructor(message: string, currentFormat: string, newFormat: string, details?: unknown) {\n super(message, 409, details);\n this.name = \"BotlrSchemaFormatConflictError\";\n this.currentFormat = currentFormat;\n this.newFormat = newFormat;\n }\n}\n\n/**\n * The human-readable message of an error response, when the server sent one:\n * `detail` as a string, `detail.message`, or the `msg` lines of a FastAPI\n * validation error. Undefined for anything else.\n */\nexport function botlrErrorMessage(error: unknown): string | undefined {\n if (!(error instanceof BotlrError)) return undefined;\n const details = error.details;\n if (!details || typeof details !== \"object\") return typeof details === \"string\" ? details : undefined;\n const detail = (details as { detail?: unknown }).detail;\n if (typeof detail === \"string\") return detail;\n if (Array.isArray(detail)) {\n const lines = detail\n .map((item) => (item && typeof item === \"object\" ? (item as { msg?: unknown }).msg : undefined))\n .filter((msg): msg is string => typeof msg === \"string\");\n return lines.length ? lines.join(\"; \") : undefined;\n }\n if (detail && typeof detail === \"object\") {\n const message = (detail as { message?: unknown }).message;\n return typeof message === \"string\" ? message : undefined;\n }\n return undefined;\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 SectionSchemaGetResponse,\n SectionSchemaPutBody,\n SectionSchemaPutResponse,\n SectionSourceListParams,\n SectionSourceListResponse,\n SectionSourcePullParams,\n SectionSourcePullResponse,\n SectionSourcePushBody,\n SectionSourcePushResponse,\n} from \"./sections\";\nimport { BotlrConflictError, BotlrError, BotlrSchemaFormatConflictError } 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 conflictFormats(details: unknown): { currentFormat: string; newFormat: 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 { currentFormat, newFormat } = detail as { currentFormat?: unknown; newFormat?: unknown };\n if (typeof currentFormat !== \"string\" || typeof newFormat !== \"string\") return null;\n return { currentFormat, newFormat };\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` and `sections.putSchema` send 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 * The section definition with its props schema and format, with the site\n * token. Works whether or not source has been pushed; 404 when the app has\n * no section type of that name.\n */\n getSchema(name: string): Promise<SectionSchemaGetResponse> {\n return request<SectionSchemaGetResponse>(`/api/sections/${encodeURIComponent(name)}/schema`, {\n method: \"GET\",\n query: { appInstanceId: baseContext.appId },\n });\n },\n\n /**\n * Write the definition's props schema with the personal access token\n * (`write:sections`). Same format, or `force`: the schema is replaced\n * (`mode: \"replaced\"`). Field-DSL fields at a FormBuilder schema without\n * `force`: the missing fields are added as nodes and nothing is removed\n * (`mode: \"merged\"`, with `added` and `dashboardOnly`). A FormBuilder\n * schema at a field-DSL one without `force` throws\n * `BotlrSchemaFormatConflictError`; a `BotlrError` with status 422 means\n * the schema is not an object of fields or the definition is not\n * project-scoped.\n */\n async putSchema(name: string, body: SectionSchemaPutBody): Promise<SectionSchemaPutResponse> {\n if (!config.personalToken) {\n throw new BotlrError(\"sections.putSchema needs `personalToken` (a personal access token with write:sections)\");\n }\n const { appInstanceId, schema, force } = body;\n try {\n return await request<SectionSchemaPutResponse>(`/api/sections/${encodeURIComponent(name)}/schema`, {\n method: \"PUT\",\n headers: { Authorization: `Bearer ${config.personalToken}` },\n body: JSON.stringify({ appInstanceId: appInstanceId ?? baseContext.appId, schema, force: force ?? false }),\n });\n } catch (error) {\n if (error instanceof BotlrConflictError) {\n const formats = conflictFormats(error.details);\n if (formats) {\n throw new BotlrSchemaFormatConflictError(error.message, formats.currentFormat, formats.newFormat, error.details);\n }\n }\n throw error;\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 /**\n * The props the code expects, normally in the field DSL. The dashboard owns\n * the definition's schema: a source push never changes it (this is only a\n * seed when `--create` adds the definition, and what the CLI compares\n * against for a drift warning). `botlr schema pull` copies the dashboard's\n * schema here in whichever format it has; `botlr schema push` sends it back.\n */\n schema?: SectionSchema;\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\n/**\n * One field of the dashboard's props schema (the field DSL): the value under\n * a field key in `SectionDefinition.schema`. Other keys are kept as given.\n */\nexport interface SectionSchemaField {\n type: string;\n label?: string;\n required?: boolean;\n /** For `select`: plain values or `{ value, label }` objects. */\n options?: Array<string | number | { value: string | number; label?: string }>;\n min?: number;\n max?: number;\n multiple?: boolean;\n [key: string]: unknown;\n}\n\n/** The field DSL: a map of field key to field. This is what the dashboard's properties editor stores. */\nexport type SectionFieldSchema = Record<string, SectionSchemaField>;\n\n/**\n * A node of the older FormBuilder schema format. A `field` node carries its\n * field type in `fieldType`; `section`, `fieldset` and `composite` nodes\n * group `children`; a `repeater` node lists its item `fields`.\n */\nexport interface SectionFormBuilderNode {\n type: string;\n fieldType?: string;\n name?: string;\n key?: string;\n label?: string;\n required?: boolean;\n options?: SectionSchemaField[\"options\"];\n children?: SectionFormBuilderNode[];\n fields?: SectionFormBuilderNode[];\n [key: string]: unknown;\n}\n\n/** The older FormBuilder schema format. */\nexport interface SectionFormBuilderSchema {\n nodes: SectionFormBuilderNode[];\n [key: string]: unknown;\n}\n\n/** A section definition's props schema, in either format the dashboard writes. */\nexport type SectionSchema = SectionFieldSchema | SectionFormBuilderSchema;\n\n/** True for the FormBuilder format (`{ nodes: [...] }`). */\nexport function isFormBuilderSchema(schema: unknown): schema is SectionFormBuilderSchema {\n return !!schema && typeof schema === \"object\" && Array.isArray((schema as { nodes?: unknown }).nodes);\n}\n\n/** True for the field DSL: an object that is not the FormBuilder format. Empty objects count. */\nexport function isFieldSchema(schema: unknown): schema is SectionFieldSchema {\n return !!schema && typeof schema === \"object\" && !Array.isArray(schema) && !isFormBuilderSchema(schema);\n}\n\n/** How a definition's props schema is stored: the field DSL or the older FormBuilder format. */\nexport type SectionSchemaFormat = \"field-dsl\" | \"form-builder\";\n\n/** The format of a schema object, or null for anything that is not a schema. */\nexport function schemaFormat(schema: unknown): SectionSchemaFormat | null {\n if (isFormBuilderSchema(schema)) return \"form-builder\";\n if (isFieldSchema(schema)) return \"field-dsl\";\n return null;\n}\n\nexport interface SectionDefinition {\n id: string;\n name: string;\n title: string;\n /** The props schema as the dashboard stores it; `{}` for a definition without one. */\n schema: SectionSchema | Record<string, unknown>;\n /** The schema's format. The schema routes always send it; the source routes may not. */\n format?: SectionSchemaFormat;\n scope: SectionDefinitionScope;\n}\n\n/** `GET /api/sections/{name}/schema` */\nexport interface SectionSchemaGetResponse {\n definition: SectionDefinition & { format: SectionSchemaFormat };\n}\n\n/** `PUT /api/sections/{name}/schema` */\nexport interface SectionSchemaPutBody {\n /** The new props schema, field DSL or FormBuilder. */\n schema: SectionSchema;\n /**\n * Replace the schema outright whatever the formats. Without it, field-DSL\n * fields pushed at a FormBuilder schema are merged in as nodes, and a\n * FormBuilder schema pushed at a field-DSL one gets 409\n * (`BotlrSchemaFormatConflictError`).\n */\n force?: boolean;\n /** Defaults to the client's `appId`. */\n appInstanceId?: string;\n}\n\n/**\n * What a schema push did: `replaced` the stored schema (same format, or\n * `force`), or `merged` field-DSL fields into a FormBuilder schema as new\n * nodes, keeping every existing node.\n */\nexport type SectionSchemaPushMode = \"replaced\" | \"merged\";\n\nexport interface SectionSchemaPutResponse {\n definition: SectionDefinition & { format: SectionSchemaFormat };\n /** The format the definition had before this call. */\n previousFormat: SectionSchemaFormat;\n /** False when nothing had to change. */\n changed: boolean;\n mode: SectionSchemaPushMode;\n /** Field names that became new nodes (`merged`), else empty. */\n added: string[];\n /** Node names the dashboard has that the code does not declare, kept (`merged`), else empty. */\n dashboardOnly: string[];\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
@@ -13,7 +13,7 @@ type BotlrClientConfig = {
13
13
  /** Preview JWT, passed as `token` to `/api/resolve` when `preview` is on. */
14
14
  previewToken?: string;
15
15
  /**
16
- * Personal access token with `write:sections`. Used only by `sections.push`;
16
+ * Personal access token with `write:sections`. Used only by `sections.push` and `sections.putSchema`;
17
17
  * the server never accepts it for reads.
18
18
  */
19
19
  personalToken?: string;
@@ -365,11 +365,13 @@ interface SectionManifest {
365
365
  icon?: string;
366
366
  dependencies?: SectionDependencies;
367
367
  /**
368
- * The props the code expects, in the field DSL. The dashboard owns the
369
- * definition's schema; this is only a seed when `--create` adds the
370
- * definition, and what the CLI compares against for a drift warning.
368
+ * The props the code expects, normally in the field DSL. The dashboard owns
369
+ * the definition's schema: a source push never changes it (this is only a
370
+ * seed when `--create` adds the definition, and what the CLI compares
371
+ * against for a drift warning). `botlr schema pull` copies the dashboard's
372
+ * schema here in whichever format it has; `botlr schema push` sends it back.
371
373
  */
372
- schema?: SectionFieldSchema;
374
+ schema?: SectionSchema;
373
375
  /** Unknown keys are kept by the server. */
374
376
  [key: string]: unknown;
375
377
  }
@@ -444,14 +446,60 @@ type SectionSchema = SectionFieldSchema | SectionFormBuilderSchema;
444
446
  declare function isFormBuilderSchema(schema: unknown): schema is SectionFormBuilderSchema;
445
447
  /** True for the field DSL: an object that is not the FormBuilder format. Empty objects count. */
446
448
  declare function isFieldSchema(schema: unknown): schema is SectionFieldSchema;
449
+ /** How a definition's props schema is stored: the field DSL or the older FormBuilder format. */
450
+ type SectionSchemaFormat = "field-dsl" | "form-builder";
451
+ /** The format of a schema object, or null for anything that is not a schema. */
452
+ declare function schemaFormat(schema: unknown): SectionSchemaFormat | null;
447
453
  interface SectionDefinition {
448
454
  id: string;
449
455
  name: string;
450
456
  title: string;
451
457
  /** The props schema as the dashboard stores it; `{}` for a definition without one. */
452
458
  schema: SectionSchema | Record<string, unknown>;
459
+ /** The schema's format. The schema routes always send it; the source routes may not. */
460
+ format?: SectionSchemaFormat;
453
461
  scope: SectionDefinitionScope;
454
462
  }
463
+ /** `GET /api/sections/{name}/schema` */
464
+ interface SectionSchemaGetResponse {
465
+ definition: SectionDefinition & {
466
+ format: SectionSchemaFormat;
467
+ };
468
+ }
469
+ /** `PUT /api/sections/{name}/schema` */
470
+ interface SectionSchemaPutBody {
471
+ /** The new props schema, field DSL or FormBuilder. */
472
+ schema: SectionSchema;
473
+ /**
474
+ * Replace the schema outright whatever the formats. Without it, field-DSL
475
+ * fields pushed at a FormBuilder schema are merged in as nodes, and a
476
+ * FormBuilder schema pushed at a field-DSL one gets 409
477
+ * (`BotlrSchemaFormatConflictError`).
478
+ */
479
+ force?: boolean;
480
+ /** Defaults to the client's `appId`. */
481
+ appInstanceId?: string;
482
+ }
483
+ /**
484
+ * What a schema push did: `replaced` the stored schema (same format, or
485
+ * `force`), or `merged` field-DSL fields into a FormBuilder schema as new
486
+ * nodes, keeping every existing node.
487
+ */
488
+ type SectionSchemaPushMode = "replaced" | "merged";
489
+ interface SectionSchemaPutResponse {
490
+ definition: SectionDefinition & {
491
+ format: SectionSchemaFormat;
492
+ };
493
+ /** The format the definition had before this call. */
494
+ previousFormat: SectionSchemaFormat;
495
+ /** False when nothing had to change. */
496
+ changed: boolean;
497
+ mode: SectionSchemaPushMode;
498
+ /** Field names that became new nodes (`merged`), else empty. */
499
+ added: string[];
500
+ /** Node names the dashboard has that the code does not declare, kept (`merged`), else empty. */
501
+ dashboardOnly: string[];
502
+ }
455
503
  interface SectionSource {
456
504
  scope: SectionSourceScope;
457
505
  framework: SectionFramework;
@@ -557,7 +605,7 @@ type BotlrClient = ReturnType<typeof createBotlrClient>;
557
605
  /**
558
606
  * A Botlr client. Every call sends the site token as `Authorization: Bearer`
559
607
  * plus `x-botlr-app-id`, `x-botlr-locale` and `x-botlr-preview` from the
560
- * config; `sections.push` sends the personal token instead.
608
+ * config; `sections.push` and `sections.putSchema` send the personal token instead.
561
609
  */
562
610
  declare function createBotlrClient(config: BotlrClientConfig): {
563
611
  request: <T>(path: string, options?: RequestOptions) => Promise<T>;
@@ -602,6 +650,24 @@ declare function createBotlrClient(config: BotlrClientConfig): {
602
650
  * changed; throws `BotlrConflictError` when `baseVersion` is not the latest.
603
651
  */
604
652
  push(name: string, body: SectionSourcePushBody): Promise<SectionSourcePushResponse>;
653
+ /**
654
+ * The section definition with its props schema and format, with the site
655
+ * token. Works whether or not source has been pushed; 404 when the app has
656
+ * no section type of that name.
657
+ */
658
+ getSchema(name: string): Promise<SectionSchemaGetResponse>;
659
+ /**
660
+ * Write the definition's props schema with the personal access token
661
+ * (`write:sections`). Same format, or `force`: the schema is replaced
662
+ * (`mode: "replaced"`). Field-DSL fields at a FormBuilder schema without
663
+ * `force`: the missing fields are added as nodes and nothing is removed
664
+ * (`mode: "merged"`, with `added` and `dashboardOnly`). A FormBuilder
665
+ * schema at a field-DSL one without `force` throws
666
+ * `BotlrSchemaFormatConflictError`; a `BotlrError` with status 422 means
667
+ * the schema is not an object of fields or the definition is not
668
+ * project-scoped.
669
+ */
670
+ putSchema(name: string, body: SectionSchemaPutBody): Promise<SectionSchemaPutResponse>;
605
671
  };
606
672
  };
607
673
  getEntryBySlug<TFields>(params: GetEntryBySlugParams): Promise<BotlrEntry<TFields> | null>;
@@ -638,6 +704,24 @@ declare function createBotlrClient(config: BotlrClientConfig): {
638
704
  * changed; throws `BotlrConflictError` when `baseVersion` is not the latest.
639
705
  */
640
706
  push(name: string, body: SectionSourcePushBody): Promise<SectionSourcePushResponse>;
707
+ /**
708
+ * The section definition with its props schema and format, with the site
709
+ * token. Works whether or not source has been pushed; 404 when the app has
710
+ * no section type of that name.
711
+ */
712
+ getSchema(name: string): Promise<SectionSchemaGetResponse>;
713
+ /**
714
+ * Write the definition's props schema with the personal access token
715
+ * (`write:sections`). Same format, or `force`: the schema is replaced
716
+ * (`mode: "replaced"`). Field-DSL fields at a FormBuilder schema without
717
+ * `force`: the missing fields are added as nodes and nothing is removed
718
+ * (`mode: "merged"`, with `added` and `dashboardOnly`). A FormBuilder
719
+ * schema at a field-DSL one without `force` throws
720
+ * `BotlrSchemaFormatConflictError`; a `BotlrError` with status 422 means
721
+ * the schema is not an object of fields or the definition is not
722
+ * project-scoped.
723
+ */
724
+ putSchema(name: string, body: SectionSchemaPutBody): Promise<SectionSchemaPutResponse>;
641
725
  };
642
726
  };
643
727
  declare function createBotlrFactory(defaults?: {
@@ -682,6 +766,24 @@ declare function createBotlrFactory(defaults?: {
682
766
  * changed; throws `BotlrConflictError` when `baseVersion` is not the latest.
683
767
  */
684
768
  push(name: string, body: SectionSourcePushBody): Promise<SectionSourcePushResponse>;
769
+ /**
770
+ * The section definition with its props schema and format, with the site
771
+ * token. Works whether or not source has been pushed; 404 when the app has
772
+ * no section type of that name.
773
+ */
774
+ getSchema(name: string): Promise<SectionSchemaGetResponse>;
775
+ /**
776
+ * Write the definition's props schema with the personal access token
777
+ * (`write:sections`). Same format, or `force`: the schema is replaced
778
+ * (`mode: "replaced"`). Field-DSL fields at a FormBuilder schema without
779
+ * `force`: the missing fields are added as nodes and nothing is removed
780
+ * (`mode: "merged"`, with `added` and `dashboardOnly`). A FormBuilder
781
+ * schema at a field-DSL one without `force` throws
782
+ * `BotlrSchemaFormatConflictError`; a `BotlrError` with status 422 means
783
+ * the schema is not an object of fields or the definition is not
784
+ * project-scoped.
785
+ */
786
+ putSchema(name: string, body: SectionSchemaPutBody): Promise<SectionSchemaPutResponse>;
685
787
  };
686
788
  };
687
789
  };
@@ -709,6 +811,21 @@ declare class BotlrConflictError extends BotlrError {
709
811
  readonly latest: ConflictLatest | null;
710
812
  constructor(message: string, latest: ConflictLatest | null, details?: unknown);
711
813
  }
814
+ /**
815
+ * Thrown by `sections.putSchema` on 409: the definition's current schema and
816
+ * the new one are in different formats and `force` was not set.
817
+ */
818
+ declare class BotlrSchemaFormatConflictError extends BotlrError {
819
+ readonly currentFormat: string;
820
+ readonly newFormat: string;
821
+ constructor(message: string, currentFormat: string, newFormat: string, details?: unknown);
822
+ }
823
+ /**
824
+ * The human-readable message of an error response, when the server sent one:
825
+ * `detail` as a string, `detail.message`, or the `msg` lines of a FastAPI
826
+ * validation error. Undefined for anything else.
827
+ */
828
+ declare function botlrErrorMessage(error: unknown): string | undefined;
712
829
 
713
830
  /** An asset as a section prop: the legacy bare id, or the `AssetData` object. */
714
831
  type AssetRef = string | AssetData | null | undefined;
@@ -743,4 +860,4 @@ declare function createAssetUrl(options: AssetUrlOptions): (asset: AssetRef, sco
743
860
  /** Statuses retried by default: timeouts, rate limits and server errors. */
744
861
  declare const DEFAULT_RETRY_STATUSES: readonly number[];
745
862
 
746
- 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 SectionFieldSchema, type SectionFormBuilderNode, type SectionFormBuilderSchema, type SectionFramework, type SectionManifest, type SectionSchema, type SectionSchemaField, 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, isFieldSchema, isFormBuilderSchema, isValidFramework, isValidSectionPath, stripManifestState, toQueryString };
863
+ 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, BotlrSchemaFormatConflictError, 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 SectionFieldSchema, type SectionFormBuilderNode, type SectionFormBuilderSchema, type SectionFramework, type SectionManifest, type SectionSchema, type SectionSchemaField, type SectionSchemaFormat, type SectionSchemaGetResponse, type SectionSchemaPushMode, type SectionSchemaPutBody, type SectionSchemaPutResponse, 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, botlrErrorMessage, contentHash, contextToHeaders, createAssetUrl, createBotlrClient, createBotlrFactory, fileSha256, getAssetFilename, getAssetId, getAssetUrl, hashFiles, isAssetData, isFieldSchema, isFormBuilderSchema, isValidFramework, isValidSectionPath, schemaFormat, stripManifestState, toQueryString };
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ type BotlrClientConfig = {
13
13
  /** Preview JWT, passed as `token` to `/api/resolve` when `preview` is on. */
14
14
  previewToken?: string;
15
15
  /**
16
- * Personal access token with `write:sections`. Used only by `sections.push`;
16
+ * Personal access token with `write:sections`. Used only by `sections.push` and `sections.putSchema`;
17
17
  * the server never accepts it for reads.
18
18
  */
19
19
  personalToken?: string;
@@ -365,11 +365,13 @@ interface SectionManifest {
365
365
  icon?: string;
366
366
  dependencies?: SectionDependencies;
367
367
  /**
368
- * The props the code expects, in the field DSL. The dashboard owns the
369
- * definition's schema; this is only a seed when `--create` adds the
370
- * definition, and what the CLI compares against for a drift warning.
368
+ * The props the code expects, normally in the field DSL. The dashboard owns
369
+ * the definition's schema: a source push never changes it (this is only a
370
+ * seed when `--create` adds the definition, and what the CLI compares
371
+ * against for a drift warning). `botlr schema pull` copies the dashboard's
372
+ * schema here in whichever format it has; `botlr schema push` sends it back.
371
373
  */
372
- schema?: SectionFieldSchema;
374
+ schema?: SectionSchema;
373
375
  /** Unknown keys are kept by the server. */
374
376
  [key: string]: unknown;
375
377
  }
@@ -444,14 +446,60 @@ type SectionSchema = SectionFieldSchema | SectionFormBuilderSchema;
444
446
  declare function isFormBuilderSchema(schema: unknown): schema is SectionFormBuilderSchema;
445
447
  /** True for the field DSL: an object that is not the FormBuilder format. Empty objects count. */
446
448
  declare function isFieldSchema(schema: unknown): schema is SectionFieldSchema;
449
+ /** How a definition's props schema is stored: the field DSL or the older FormBuilder format. */
450
+ type SectionSchemaFormat = "field-dsl" | "form-builder";
451
+ /** The format of a schema object, or null for anything that is not a schema. */
452
+ declare function schemaFormat(schema: unknown): SectionSchemaFormat | null;
447
453
  interface SectionDefinition {
448
454
  id: string;
449
455
  name: string;
450
456
  title: string;
451
457
  /** The props schema as the dashboard stores it; `{}` for a definition without one. */
452
458
  schema: SectionSchema | Record<string, unknown>;
459
+ /** The schema's format. The schema routes always send it; the source routes may not. */
460
+ format?: SectionSchemaFormat;
453
461
  scope: SectionDefinitionScope;
454
462
  }
463
+ /** `GET /api/sections/{name}/schema` */
464
+ interface SectionSchemaGetResponse {
465
+ definition: SectionDefinition & {
466
+ format: SectionSchemaFormat;
467
+ };
468
+ }
469
+ /** `PUT /api/sections/{name}/schema` */
470
+ interface SectionSchemaPutBody {
471
+ /** The new props schema, field DSL or FormBuilder. */
472
+ schema: SectionSchema;
473
+ /**
474
+ * Replace the schema outright whatever the formats. Without it, field-DSL
475
+ * fields pushed at a FormBuilder schema are merged in as nodes, and a
476
+ * FormBuilder schema pushed at a field-DSL one gets 409
477
+ * (`BotlrSchemaFormatConflictError`).
478
+ */
479
+ force?: boolean;
480
+ /** Defaults to the client's `appId`. */
481
+ appInstanceId?: string;
482
+ }
483
+ /**
484
+ * What a schema push did: `replaced` the stored schema (same format, or
485
+ * `force`), or `merged` field-DSL fields into a FormBuilder schema as new
486
+ * nodes, keeping every existing node.
487
+ */
488
+ type SectionSchemaPushMode = "replaced" | "merged";
489
+ interface SectionSchemaPutResponse {
490
+ definition: SectionDefinition & {
491
+ format: SectionSchemaFormat;
492
+ };
493
+ /** The format the definition had before this call. */
494
+ previousFormat: SectionSchemaFormat;
495
+ /** False when nothing had to change. */
496
+ changed: boolean;
497
+ mode: SectionSchemaPushMode;
498
+ /** Field names that became new nodes (`merged`), else empty. */
499
+ added: string[];
500
+ /** Node names the dashboard has that the code does not declare, kept (`merged`), else empty. */
501
+ dashboardOnly: string[];
502
+ }
455
503
  interface SectionSource {
456
504
  scope: SectionSourceScope;
457
505
  framework: SectionFramework;
@@ -557,7 +605,7 @@ type BotlrClient = ReturnType<typeof createBotlrClient>;
557
605
  /**
558
606
  * A Botlr client. Every call sends the site token as `Authorization: Bearer`
559
607
  * plus `x-botlr-app-id`, `x-botlr-locale` and `x-botlr-preview` from the
560
- * config; `sections.push` sends the personal token instead.
608
+ * config; `sections.push` and `sections.putSchema` send the personal token instead.
561
609
  */
562
610
  declare function createBotlrClient(config: BotlrClientConfig): {
563
611
  request: <T>(path: string, options?: RequestOptions) => Promise<T>;
@@ -602,6 +650,24 @@ declare function createBotlrClient(config: BotlrClientConfig): {
602
650
  * changed; throws `BotlrConflictError` when `baseVersion` is not the latest.
603
651
  */
604
652
  push(name: string, body: SectionSourcePushBody): Promise<SectionSourcePushResponse>;
653
+ /**
654
+ * The section definition with its props schema and format, with the site
655
+ * token. Works whether or not source has been pushed; 404 when the app has
656
+ * no section type of that name.
657
+ */
658
+ getSchema(name: string): Promise<SectionSchemaGetResponse>;
659
+ /**
660
+ * Write the definition's props schema with the personal access token
661
+ * (`write:sections`). Same format, or `force`: the schema is replaced
662
+ * (`mode: "replaced"`). Field-DSL fields at a FormBuilder schema without
663
+ * `force`: the missing fields are added as nodes and nothing is removed
664
+ * (`mode: "merged"`, with `added` and `dashboardOnly`). A FormBuilder
665
+ * schema at a field-DSL one without `force` throws
666
+ * `BotlrSchemaFormatConflictError`; a `BotlrError` with status 422 means
667
+ * the schema is not an object of fields or the definition is not
668
+ * project-scoped.
669
+ */
670
+ putSchema(name: string, body: SectionSchemaPutBody): Promise<SectionSchemaPutResponse>;
605
671
  };
606
672
  };
607
673
  getEntryBySlug<TFields>(params: GetEntryBySlugParams): Promise<BotlrEntry<TFields> | null>;
@@ -638,6 +704,24 @@ declare function createBotlrClient(config: BotlrClientConfig): {
638
704
  * changed; throws `BotlrConflictError` when `baseVersion` is not the latest.
639
705
  */
640
706
  push(name: string, body: SectionSourcePushBody): Promise<SectionSourcePushResponse>;
707
+ /**
708
+ * The section definition with its props schema and format, with the site
709
+ * token. Works whether or not source has been pushed; 404 when the app has
710
+ * no section type of that name.
711
+ */
712
+ getSchema(name: string): Promise<SectionSchemaGetResponse>;
713
+ /**
714
+ * Write the definition's props schema with the personal access token
715
+ * (`write:sections`). Same format, or `force`: the schema is replaced
716
+ * (`mode: "replaced"`). Field-DSL fields at a FormBuilder schema without
717
+ * `force`: the missing fields are added as nodes and nothing is removed
718
+ * (`mode: "merged"`, with `added` and `dashboardOnly`). A FormBuilder
719
+ * schema at a field-DSL one without `force` throws
720
+ * `BotlrSchemaFormatConflictError`; a `BotlrError` with status 422 means
721
+ * the schema is not an object of fields or the definition is not
722
+ * project-scoped.
723
+ */
724
+ putSchema(name: string, body: SectionSchemaPutBody): Promise<SectionSchemaPutResponse>;
641
725
  };
642
726
  };
643
727
  declare function createBotlrFactory(defaults?: {
@@ -682,6 +766,24 @@ declare function createBotlrFactory(defaults?: {
682
766
  * changed; throws `BotlrConflictError` when `baseVersion` is not the latest.
683
767
  */
684
768
  push(name: string, body: SectionSourcePushBody): Promise<SectionSourcePushResponse>;
769
+ /**
770
+ * The section definition with its props schema and format, with the site
771
+ * token. Works whether or not source has been pushed; 404 when the app has
772
+ * no section type of that name.
773
+ */
774
+ getSchema(name: string): Promise<SectionSchemaGetResponse>;
775
+ /**
776
+ * Write the definition's props schema with the personal access token
777
+ * (`write:sections`). Same format, or `force`: the schema is replaced
778
+ * (`mode: "replaced"`). Field-DSL fields at a FormBuilder schema without
779
+ * `force`: the missing fields are added as nodes and nothing is removed
780
+ * (`mode: "merged"`, with `added` and `dashboardOnly`). A FormBuilder
781
+ * schema at a field-DSL one without `force` throws
782
+ * `BotlrSchemaFormatConflictError`; a `BotlrError` with status 422 means
783
+ * the schema is not an object of fields or the definition is not
784
+ * project-scoped.
785
+ */
786
+ putSchema(name: string, body: SectionSchemaPutBody): Promise<SectionSchemaPutResponse>;
685
787
  };
686
788
  };
687
789
  };
@@ -709,6 +811,21 @@ declare class BotlrConflictError extends BotlrError {
709
811
  readonly latest: ConflictLatest | null;
710
812
  constructor(message: string, latest: ConflictLatest | null, details?: unknown);
711
813
  }
814
+ /**
815
+ * Thrown by `sections.putSchema` on 409: the definition's current schema and
816
+ * the new one are in different formats and `force` was not set.
817
+ */
818
+ declare class BotlrSchemaFormatConflictError extends BotlrError {
819
+ readonly currentFormat: string;
820
+ readonly newFormat: string;
821
+ constructor(message: string, currentFormat: string, newFormat: string, details?: unknown);
822
+ }
823
+ /**
824
+ * The human-readable message of an error response, when the server sent one:
825
+ * `detail` as a string, `detail.message`, or the `msg` lines of a FastAPI
826
+ * validation error. Undefined for anything else.
827
+ */
828
+ declare function botlrErrorMessage(error: unknown): string | undefined;
712
829
 
713
830
  /** An asset as a section prop: the legacy bare id, or the `AssetData` object. */
714
831
  type AssetRef = string | AssetData | null | undefined;
@@ -743,4 +860,4 @@ declare function createAssetUrl(options: AssetUrlOptions): (asset: AssetRef, sco
743
860
  /** Statuses retried by default: timeouts, rate limits and server errors. */
744
861
  declare const DEFAULT_RETRY_STATUSES: readonly number[];
745
862
 
746
- 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 SectionFieldSchema, type SectionFormBuilderNode, type SectionFormBuilderSchema, type SectionFramework, type SectionManifest, type SectionSchema, type SectionSchemaField, 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, isFieldSchema, isFormBuilderSchema, isValidFramework, isValidSectionPath, stripManifestState, toQueryString };
863
+ 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, BotlrSchemaFormatConflictError, 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 SectionFieldSchema, type SectionFormBuilderNode, type SectionFormBuilderSchema, type SectionFramework, type SectionManifest, type SectionSchema, type SectionSchemaField, type SectionSchemaFormat, type SectionSchemaGetResponse, type SectionSchemaPushMode, type SectionSchemaPutBody, type SectionSchemaPutResponse, 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, botlrErrorMessage, contentHash, contextToHeaders, createAssetUrl, createBotlrClient, createBotlrFactory, fileSha256, getAssetFilename, getAssetId, getAssetUrl, hashFiles, isAssetData, isFieldSchema, isFormBuilderSchema, isValidFramework, isValidSectionPath, schemaFormat, stripManifestState, toQueryString };
package/dist/index.js CHANGED
@@ -17,6 +17,32 @@ var BotlrConflictError = class extends BotlrError {
17
17
  this.latest = latest;
18
18
  }
19
19
  };
20
+ var BotlrSchemaFormatConflictError = class extends BotlrError {
21
+ currentFormat;
22
+ newFormat;
23
+ constructor(message, currentFormat, newFormat, details) {
24
+ super(message, 409, details);
25
+ this.name = "BotlrSchemaFormatConflictError";
26
+ this.currentFormat = currentFormat;
27
+ this.newFormat = newFormat;
28
+ }
29
+ };
30
+ function botlrErrorMessage(error) {
31
+ if (!(error instanceof BotlrError)) return void 0;
32
+ const details = error.details;
33
+ if (!details || typeof details !== "object") return typeof details === "string" ? details : void 0;
34
+ const detail = details.detail;
35
+ if (typeof detail === "string") return detail;
36
+ if (Array.isArray(detail)) {
37
+ const lines = detail.map((item) => item && typeof item === "object" ? item.msg : void 0).filter((msg) => typeof msg === "string");
38
+ return lines.length ? lines.join("; ") : void 0;
39
+ }
40
+ if (detail && typeof detail === "object") {
41
+ const message = detail.message;
42
+ return typeof message === "string" ? message : void 0;
43
+ }
44
+ return void 0;
45
+ }
20
46
 
21
47
  // src/retry.ts
22
48
  var DEFAULT_RETRY_STATUSES = [408, 429, 500, 502, 503, 504];
@@ -105,6 +131,14 @@ function conflictLatest(details) {
105
131
  if (typeof version !== "number" || typeof contentHash2 !== "string") return null;
106
132
  return { version, contentHash: contentHash2 };
107
133
  }
134
+ function conflictFormats(details) {
135
+ if (!details || typeof details !== "object") return null;
136
+ const detail = details.detail;
137
+ if (!detail || typeof detail !== "object") return null;
138
+ const { currentFormat, newFormat } = detail;
139
+ if (typeof currentFormat !== "string" || typeof newFormat !== "string") return null;
140
+ return { currentFormat, newFormat };
141
+ }
108
142
  function conflictMessage(details) {
109
143
  if (!details || typeof details !== "object") return void 0;
110
144
  const detail = details.detail;
@@ -197,6 +231,49 @@ function createBotlrClient(config) {
197
231
  headers: { Authorization: `Bearer ${config.personalToken}` },
198
232
  body: JSON.stringify({ appInstanceId: appInstanceId ?? baseContext.appId, ...rest })
199
233
  });
234
+ },
235
+ /**
236
+ * The section definition with its props schema and format, with the site
237
+ * token. Works whether or not source has been pushed; 404 when the app has
238
+ * no section type of that name.
239
+ */
240
+ getSchema(name) {
241
+ return request(`/api/sections/${encodeURIComponent(name)}/schema`, {
242
+ method: "GET",
243
+ query: { appInstanceId: baseContext.appId }
244
+ });
245
+ },
246
+ /**
247
+ * Write the definition's props schema with the personal access token
248
+ * (`write:sections`). Same format, or `force`: the schema is replaced
249
+ * (`mode: "replaced"`). Field-DSL fields at a FormBuilder schema without
250
+ * `force`: the missing fields are added as nodes and nothing is removed
251
+ * (`mode: "merged"`, with `added` and `dashboardOnly`). A FormBuilder
252
+ * schema at a field-DSL one without `force` throws
253
+ * `BotlrSchemaFormatConflictError`; a `BotlrError` with status 422 means
254
+ * the schema is not an object of fields or the definition is not
255
+ * project-scoped.
256
+ */
257
+ async putSchema(name, body) {
258
+ if (!config.personalToken) {
259
+ throw new BotlrError("sections.putSchema needs `personalToken` (a personal access token with write:sections)");
260
+ }
261
+ const { appInstanceId, schema, force } = body;
262
+ try {
263
+ return await request(`/api/sections/${encodeURIComponent(name)}/schema`, {
264
+ method: "PUT",
265
+ headers: { Authorization: `Bearer ${config.personalToken}` },
266
+ body: JSON.stringify({ appInstanceId: appInstanceId ?? baseContext.appId, schema, force: force ?? false })
267
+ });
268
+ } catch (error) {
269
+ if (error instanceof BotlrConflictError) {
270
+ const formats = conflictFormats(error.details);
271
+ if (formats) {
272
+ throw new BotlrSchemaFormatConflictError(error.message, formats.currentFormat, formats.newFormat, error.details);
273
+ }
274
+ }
275
+ throw error;
276
+ }
200
277
  }
201
278
  };
202
279
  return {
@@ -352,6 +429,11 @@ function isFormBuilderSchema(schema) {
352
429
  function isFieldSchema(schema) {
353
430
  return !!schema && typeof schema === "object" && !Array.isArray(schema) && !isFormBuilderSchema(schema);
354
431
  }
432
+ function schemaFormat(schema) {
433
+ if (isFormBuilderSchema(schema)) return "form-builder";
434
+ if (isFieldSchema(schema)) return "field-dsl";
435
+ return null;
436
+ }
355
437
  var SECTION_SOURCE_LIMITS = {
356
438
  maxFiles: 50,
357
439
  maxFileBytes: 512 * 1024,
@@ -442,11 +524,13 @@ function createAssetUrl(options) {
442
524
  export {
443
525
  BotlrConflictError,
444
526
  BotlrError,
527
+ BotlrSchemaFormatConflictError,
445
528
  DEFAULT_RETRY_STATUSES,
446
529
  MANIFEST_STATE_KEYS,
447
530
  SECTION_FRAMEWORK_RE,
448
531
  SECTION_PATH_RE,
449
532
  SECTION_SOURCE_LIMITS,
533
+ botlrErrorMessage,
450
534
  contentHash,
451
535
  contextToHeaders,
452
536
  createAssetUrl,
@@ -462,6 +546,7 @@ export {
462
546
  isFormBuilderSchema,
463
547
  isValidFramework,
464
548
  isValidSectionPath,
549
+ schemaFormat,
465
550
  stripManifestState,
466
551
  toQueryString
467
552
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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 /**\n * The props the code expects, in the field DSL. The dashboard owns the\n * definition's schema; this is only a seed when `--create` adds the\n * definition, and what the CLI compares against for a drift warning.\n */\n schema?: SectionFieldSchema;\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\n/**\n * One field of the dashboard's props schema (the field DSL): the value under\n * a field key in `SectionDefinition.schema`. Other keys are kept as given.\n */\nexport interface SectionSchemaField {\n type: string;\n label?: string;\n required?: boolean;\n /** For `select`: plain values or `{ value, label }` objects. */\n options?: Array<string | number | { value: string | number; label?: string }>;\n min?: number;\n max?: number;\n multiple?: boolean;\n [key: string]: unknown;\n}\n\n/** The field DSL: a map of field key to field. This is what the dashboard's properties editor stores. */\nexport type SectionFieldSchema = Record<string, SectionSchemaField>;\n\n/**\n * A node of the older FormBuilder schema format. A `field` node carries its\n * field type in `fieldType`; `section`, `fieldset` and `composite` nodes\n * group `children`; a `repeater` node lists its item `fields`.\n */\nexport interface SectionFormBuilderNode {\n type: string;\n fieldType?: string;\n name?: string;\n key?: string;\n label?: string;\n required?: boolean;\n options?: SectionSchemaField[\"options\"];\n children?: SectionFormBuilderNode[];\n fields?: SectionFormBuilderNode[];\n [key: string]: unknown;\n}\n\n/** The older FormBuilder schema format. */\nexport interface SectionFormBuilderSchema {\n nodes: SectionFormBuilderNode[];\n [key: string]: unknown;\n}\n\n/** A section definition's props schema, in either format the dashboard writes. */\nexport type SectionSchema = SectionFieldSchema | SectionFormBuilderSchema;\n\n/** True for the FormBuilder format (`{ nodes: [...] }`). */\nexport function isFormBuilderSchema(schema: unknown): schema is SectionFormBuilderSchema {\n return !!schema && typeof schema === \"object\" && Array.isArray((schema as { nodes?: unknown }).nodes);\n}\n\n/** True for the field DSL: an object that is not the FormBuilder format. Empty objects count. */\nexport function isFieldSchema(schema: unknown): schema is SectionFieldSchema {\n return !!schema && typeof schema === \"object\" && !Array.isArray(schema) && !isFormBuilderSchema(schema);\n}\n\nexport interface SectionDefinition {\n id: string;\n name: string;\n title: string;\n /** The props schema as the dashboard stores it; `{}` for a definition without one. */\n schema: SectionSchema | 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"]}
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\n/**\n * Thrown by `sections.putSchema` on 409: the definition's current schema and\n * the new one are in different formats and `force` was not set.\n */\nexport class BotlrSchemaFormatConflictError extends BotlrError {\n readonly currentFormat: string;\n readonly newFormat: string;\n\n constructor(message: string, currentFormat: string, newFormat: string, details?: unknown) {\n super(message, 409, details);\n this.name = \"BotlrSchemaFormatConflictError\";\n this.currentFormat = currentFormat;\n this.newFormat = newFormat;\n }\n}\n\n/**\n * The human-readable message of an error response, when the server sent one:\n * `detail` as a string, `detail.message`, or the `msg` lines of a FastAPI\n * validation error. Undefined for anything else.\n */\nexport function botlrErrorMessage(error: unknown): string | undefined {\n if (!(error instanceof BotlrError)) return undefined;\n const details = error.details;\n if (!details || typeof details !== \"object\") return typeof details === \"string\" ? details : undefined;\n const detail = (details as { detail?: unknown }).detail;\n if (typeof detail === \"string\") return detail;\n if (Array.isArray(detail)) {\n const lines = detail\n .map((item) => (item && typeof item === \"object\" ? (item as { msg?: unknown }).msg : undefined))\n .filter((msg): msg is string => typeof msg === \"string\");\n return lines.length ? lines.join(\"; \") : undefined;\n }\n if (detail && typeof detail === \"object\") {\n const message = (detail as { message?: unknown }).message;\n return typeof message === \"string\" ? message : undefined;\n }\n return undefined;\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 SectionSchemaGetResponse,\n SectionSchemaPutBody,\n SectionSchemaPutResponse,\n SectionSourceListParams,\n SectionSourceListResponse,\n SectionSourcePullParams,\n SectionSourcePullResponse,\n SectionSourcePushBody,\n SectionSourcePushResponse,\n} from \"./sections\";\nimport { BotlrConflictError, BotlrError, BotlrSchemaFormatConflictError } 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 conflictFormats(details: unknown): { currentFormat: string; newFormat: 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 { currentFormat, newFormat } = detail as { currentFormat?: unknown; newFormat?: unknown };\n if (typeof currentFormat !== \"string\" || typeof newFormat !== \"string\") return null;\n return { currentFormat, newFormat };\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` and `sections.putSchema` send 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 * The section definition with its props schema and format, with the site\n * token. Works whether or not source has been pushed; 404 when the app has\n * no section type of that name.\n */\n getSchema(name: string): Promise<SectionSchemaGetResponse> {\n return request<SectionSchemaGetResponse>(`/api/sections/${encodeURIComponent(name)}/schema`, {\n method: \"GET\",\n query: { appInstanceId: baseContext.appId },\n });\n },\n\n /**\n * Write the definition's props schema with the personal access token\n * (`write:sections`). Same format, or `force`: the schema is replaced\n * (`mode: \"replaced\"`). Field-DSL fields at a FormBuilder schema without\n * `force`: the missing fields are added as nodes and nothing is removed\n * (`mode: \"merged\"`, with `added` and `dashboardOnly`). A FormBuilder\n * schema at a field-DSL one without `force` throws\n * `BotlrSchemaFormatConflictError`; a `BotlrError` with status 422 means\n * the schema is not an object of fields or the definition is not\n * project-scoped.\n */\n async putSchema(name: string, body: SectionSchemaPutBody): Promise<SectionSchemaPutResponse> {\n if (!config.personalToken) {\n throw new BotlrError(\"sections.putSchema needs `personalToken` (a personal access token with write:sections)\");\n }\n const { appInstanceId, schema, force } = body;\n try {\n return await request<SectionSchemaPutResponse>(`/api/sections/${encodeURIComponent(name)}/schema`, {\n method: \"PUT\",\n headers: { Authorization: `Bearer ${config.personalToken}` },\n body: JSON.stringify({ appInstanceId: appInstanceId ?? baseContext.appId, schema, force: force ?? false }),\n });\n } catch (error) {\n if (error instanceof BotlrConflictError) {\n const formats = conflictFormats(error.details);\n if (formats) {\n throw new BotlrSchemaFormatConflictError(error.message, formats.currentFormat, formats.newFormat, error.details);\n }\n }\n throw error;\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 /**\n * The props the code expects, normally in the field DSL. The dashboard owns\n * the definition's schema: a source push never changes it (this is only a\n * seed when `--create` adds the definition, and what the CLI compares\n * against for a drift warning). `botlr schema pull` copies the dashboard's\n * schema here in whichever format it has; `botlr schema push` sends it back.\n */\n schema?: SectionSchema;\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\n/**\n * One field of the dashboard's props schema (the field DSL): the value under\n * a field key in `SectionDefinition.schema`. Other keys are kept as given.\n */\nexport interface SectionSchemaField {\n type: string;\n label?: string;\n required?: boolean;\n /** For `select`: plain values or `{ value, label }` objects. */\n options?: Array<string | number | { value: string | number; label?: string }>;\n min?: number;\n max?: number;\n multiple?: boolean;\n [key: string]: unknown;\n}\n\n/** The field DSL: a map of field key to field. This is what the dashboard's properties editor stores. */\nexport type SectionFieldSchema = Record<string, SectionSchemaField>;\n\n/**\n * A node of the older FormBuilder schema format. A `field` node carries its\n * field type in `fieldType`; `section`, `fieldset` and `composite` nodes\n * group `children`; a `repeater` node lists its item `fields`.\n */\nexport interface SectionFormBuilderNode {\n type: string;\n fieldType?: string;\n name?: string;\n key?: string;\n label?: string;\n required?: boolean;\n options?: SectionSchemaField[\"options\"];\n children?: SectionFormBuilderNode[];\n fields?: SectionFormBuilderNode[];\n [key: string]: unknown;\n}\n\n/** The older FormBuilder schema format. */\nexport interface SectionFormBuilderSchema {\n nodes: SectionFormBuilderNode[];\n [key: string]: unknown;\n}\n\n/** A section definition's props schema, in either format the dashboard writes. */\nexport type SectionSchema = SectionFieldSchema | SectionFormBuilderSchema;\n\n/** True for the FormBuilder format (`{ nodes: [...] }`). */\nexport function isFormBuilderSchema(schema: unknown): schema is SectionFormBuilderSchema {\n return !!schema && typeof schema === \"object\" && Array.isArray((schema as { nodes?: unknown }).nodes);\n}\n\n/** True for the field DSL: an object that is not the FormBuilder format. Empty objects count. */\nexport function isFieldSchema(schema: unknown): schema is SectionFieldSchema {\n return !!schema && typeof schema === \"object\" && !Array.isArray(schema) && !isFormBuilderSchema(schema);\n}\n\n/** How a definition's props schema is stored: the field DSL or the older FormBuilder format. */\nexport type SectionSchemaFormat = \"field-dsl\" | \"form-builder\";\n\n/** The format of a schema object, or null for anything that is not a schema. */\nexport function schemaFormat(schema: unknown): SectionSchemaFormat | null {\n if (isFormBuilderSchema(schema)) return \"form-builder\";\n if (isFieldSchema(schema)) return \"field-dsl\";\n return null;\n}\n\nexport interface SectionDefinition {\n id: string;\n name: string;\n title: string;\n /** The props schema as the dashboard stores it; `{}` for a definition without one. */\n schema: SectionSchema | Record<string, unknown>;\n /** The schema's format. The schema routes always send it; the source routes may not. */\n format?: SectionSchemaFormat;\n scope: SectionDefinitionScope;\n}\n\n/** `GET /api/sections/{name}/schema` */\nexport interface SectionSchemaGetResponse {\n definition: SectionDefinition & { format: SectionSchemaFormat };\n}\n\n/** `PUT /api/sections/{name}/schema` */\nexport interface SectionSchemaPutBody {\n /** The new props schema, field DSL or FormBuilder. */\n schema: SectionSchema;\n /**\n * Replace the schema outright whatever the formats. Without it, field-DSL\n * fields pushed at a FormBuilder schema are merged in as nodes, and a\n * FormBuilder schema pushed at a field-DSL one gets 409\n * (`BotlrSchemaFormatConflictError`).\n */\n force?: boolean;\n /** Defaults to the client's `appId`. */\n appInstanceId?: string;\n}\n\n/**\n * What a schema push did: `replaced` the stored schema (same format, or\n * `force`), or `merged` field-DSL fields into a FormBuilder schema as new\n * nodes, keeping every existing node.\n */\nexport type SectionSchemaPushMode = \"replaced\" | \"merged\";\n\nexport interface SectionSchemaPutResponse {\n definition: SectionDefinition & { format: SectionSchemaFormat };\n /** The format the definition had before this call. */\n previousFormat: SectionSchemaFormat;\n /** False when nothing had to change. */\n changed: boolean;\n mode: SectionSchemaPushMode;\n /** Field names that became new nodes (`merged`), else empty. */\n added: string[];\n /** Node names the dashboard has that the code does not declare, kept (`merged`), else empty. */\n dashboardOnly: string[];\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":";AAIO,IAAM,aAAN,cAAyB,MAAM;AAAA,EAC3B;AAAA,EACA;AAAA,EAET,YAAY,SAAiB,QAAiB,SAAmB;AAC/D,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,UAAU;AAAA,EACjB;AACF;AAaO,IAAM,qBAAN,cAAiC,WAAW;AAAA,EACxC;AAAA,EAET,YAAY,SAAiB,QAA+B,SAAmB;AAC7E,UAAM,SAAS,KAAK,OAAO;AAC3B,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AAMO,IAAM,iCAAN,cAA6C,WAAW;AAAA,EACpD;AAAA,EACA;AAAA,EAET,YAAY,SAAiB,eAAuB,WAAmB,SAAmB;AACxF,UAAM,SAAS,KAAK,OAAO;AAC3B,SAAK,OAAO;AACZ,SAAK,gBAAgB;AACrB,SAAK,YAAY;AAAA,EACnB;AACF;AAOO,SAAS,kBAAkB,OAAoC;AACpE,MAAI,EAAE,iBAAiB,YAAa,QAAO;AAC3C,QAAM,UAAU,MAAM;AACtB,MAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO,OAAO,YAAY,WAAW,UAAU;AAC5F,QAAM,SAAU,QAAiC;AACjD,MAAI,OAAO,WAAW,SAAU,QAAO;AACvC,MAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,UAAM,QAAQ,OACX,IAAI,CAAC,SAAU,QAAQ,OAAO,SAAS,WAAY,KAA2B,MAAM,MAAU,EAC9F,OAAO,CAAC,QAAuB,OAAO,QAAQ,QAAQ;AACzD,WAAO,MAAM,SAAS,MAAM,KAAK,IAAI,IAAI;AAAA,EAC3C;AACA,MAAI,UAAU,OAAO,WAAW,UAAU;AACxC,UAAM,UAAW,OAAiC;AAClD,WAAO,OAAO,YAAY,WAAW,UAAU;AAAA,EACjD;AACA,SAAO;AACT;;;ACxEO,IAAM,yBAA4C,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG;AAU/E,SAAS,mBAAmB,OAAkE;AACnG,MAAI,UAAU,OAAO;AACnB,WAAO,EAAE,SAAS,GAAG,OAAO,GAAG,UAAU,GAAG,UAAU,CAAC,GAAG,SAAS,OAAU;AAAA,EAC/E;AACA,SAAO;AAAA,IACL,SAAS,KAAK,IAAI,GAAG,OAAO,WAAW,CAAC;AAAA,IACxC,OAAO,KAAK,IAAI,GAAG,OAAO,SAAS,GAAG;AAAA,IACtC,UAAU,KAAK,IAAI,GAAG,OAAO,YAAY,GAAI;AAAA,IAC7C,UAAU,OAAO,YAAY;AAAA,IAC7B,SAAS,OAAO;AAAA,EAClB;AACF;AAGO,SAAS,kBAAkB,QAAqC;AACrE,QAAM,KAAK,UAAU,OAAO,YAAY;AACxC,SAAO,MAAM,SAAS,MAAM;AAC9B;AAGO,SAAS,gBAAgB,OAAsD;AACpF,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,UAAU,OAAO,KAAK;AAC5B,MAAI,OAAO,SAAS,OAAO,EAAG,QAAO,KAAK,IAAI,GAAG,UAAU,GAAI;AAC/D,QAAM,OAAO,KAAK,MAAM,KAAK;AAC7B,MAAI,OAAO,SAAS,IAAI,EAAG,QAAO,KAAK,IAAI,GAAG,OAAO,KAAK,IAAI,CAAC;AAC/D,SAAO;AACT;AAMO,SAAS,aAAa,OAA4B,SAAiB,YAAoC;AAC5G,QAAM,aAAa,gBAAgB,UAAU;AAC7C,MAAI,eAAe,OAAW,QAAO,KAAK,IAAI,YAAY,MAAM,QAAQ;AACxE,QAAM,OAAO,KAAK,IAAI,MAAM,UAAU,MAAM,QAAQ,KAAK,OAAO;AAChE,SAAO,KAAK,MAAM,QAAQ,OAAO,KAAK,OAAO,IAAI,IAAI;AACvD;AAEA,eAAsB,gBACpB,OACA,SACA,OACA,KACA,YACe;AACf,QAAM,QAAQ,aAAa,OAAO,SAAS,UAAU;AACrD,QAAM,UAAU,EAAE,SAAS,UAAU,GAAG,OAAO,KAAK,MAAM,CAAC;AAC3D,MAAI,QAAQ,EAAG,OAAM,IAAI,QAAc,CAAC,YAAY,WAAW,SAAS,KAAK,CAAC;AAChF;;;ACvBO,SAAS,cAAc,OAA2B;AACvD,QAAM,SAAS,IAAI,gBAAgB;AAEnC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,UAAU,UAAa,UAAU,KAAM;AAE3C,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,iBAAW,QAAQ,OAAO;AACxB,YAAI,SAAS,UAAa,SAAS,MAAM;AACvC,iBAAO,OAAO,KAAK,OAAO,IAAI,CAAC;AAAA,QACjC;AAAA,MACF;AAAA,IACF,OAAO;AACL,aAAO,IAAI,KAAK,OAAO,KAAK,CAAC;AAAA,IAC/B;AAAA,EACF;AAEA,QAAM,KAAK,OAAO,SAAS;AAC3B,SAAO,KAAK,IAAI,EAAE,KAAK;AACzB;AAEA,SAAS,aACP,MACA,UACmC;AACnC,MAAI,CAAC,QAAQ,CAAC,SAAU,QAAO;AAC/B,SAAO,EAAE,GAAG,MAAM,GAAG,SAAS;AAChC;AAEO,SAAS,iBAAiB,SAAyD;AACxF,MAAI,CAAC,QAAS,QAAO,CAAC;AAEtB,SAAO;AAAA,IACL,GAAI,QAAQ,QAAQ,EAAE,kBAAkB,QAAQ,MAAM,IAAI,CAAC;AAAA,IAC3D,GAAI,QAAQ,SAAS,EAAE,kBAAkB,QAAQ,OAAO,IAAI,CAAC;AAAA,IAC7D,GAAI,QAAQ,YAAY,SAAY,EAAE,mBAAmB,OAAO,QAAQ,OAAO,EAAE,IAAI,CAAC;AAAA,EACxF;AACF;AAEA,eAAe,aAAa,UAAsC;AAChE,QAAM,OAAO,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,MAAS;AACxD,MAAI,SAAS,UAAa,SAAS,GAAI,QAAO;AAC9C,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,eAAe,SAAmE;AACzF,MAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AACpD,QAAM,SAAU,QAAiC;AACjD,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,QAAM,SAAU,OAAgC;AAChD,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,QAAM,EAAE,SAAS,aAAAA,aAAY,IAAI;AACjC,MAAI,OAAO,YAAY,YAAY,OAAOA,iBAAgB,SAAU,QAAO;AAC3E,SAAO,EAAE,SAAS,aAAAA,aAAY;AAChC;AAEA,SAAS,gBAAgB,SAAuE;AAC9F,MAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AACpD,QAAM,SAAU,QAAiC;AACjD,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,QAAM,EAAE,eAAe,UAAU,IAAI;AACrC,MAAI,OAAO,kBAAkB,YAAY,OAAO,cAAc,SAAU,QAAO;AAC/E,SAAO,EAAE,eAAe,UAAU;AACpC;AAEA,SAAS,gBAAgB,SAAsC;AAC7D,MAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AACpD,QAAM,SAAU,QAAiC;AACjD,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,QAAM,UAAW,OAAiC;AAClD,SAAO,OAAO,YAAY,WAAW,UAAU;AACjD;AASO,SAAS,kBAAkB,QAA2B;AAC3D,QAAM,UAAU,OAAO,SAAS,WAAW;AAC3C,MAAI,OAAO,YAAY,YAAY;AACjC,UAAM,IAAI,WAAW,4DAA4D;AAAA,EACnF;AACA,QAAM,UAAU,OAAO,QAAQ,QAAQ,QAAQ,EAAE;AAEjD,QAAM,cAA4B;AAAA,IAChC,OAAO,OAAO;AAAA,IACd,QAAQ,OAAO;AAAA,IACf,SAAS,OAAO;AAAA,EAClB;AAEA,QAAM,QAAQ,mBAAmB,OAAO,KAAK;AAE7C,iBAAe,QAAW,MAAc,UAA0B,CAAC,GAAe;AAChF,UAAM,EAAE,OAAO,SAAS,iBAAiB,SAAS,cAAc,GAAG,KAAK,IAAI;AAC5E,UAAM,UAAU,aAAa,aAAa,eAAe;AACzD,UAAM,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,cAAc,SAAS,CAAC,CAAC,CAAC;AAC1D,UAAM,UAAU;AAAA,MACd,QAAQ;AAAA,MACR,gBAAgB;AAAA,MAChB,GAAI,OAAO,SAAS,EAAE,eAAe,UAAU,OAAO,MAAM,GAAG,IAAI,CAAC;AAAA,MACpE,GAAG,iBAAiB,OAAO;AAAA,MAC3B,GAAI,gBAAgB,CAAC;AAAA,IACvB;AACA,UAAM,YAAY,MAAM,UAAU,KAAK,kBAAkB,KAAK,MAAM;AAEpE,aAAS,UAAU,KAAK,WAAW;AACjC,YAAM,WAAW,aAAa,UAAU,MAAM,WAAW,CAAC,KAAK,QAAQ;AAEvE,UAAI;AACJ,UAAI;AACF,mBAAW,MAAM,QAAQ,KAAK,EAAE,GAAG,MAAM,QAAQ,CAAC;AAAA,MACpD,SAASC,QAAO;AACd,YAAI,CAAC,SAAU,OAAMA;AACrB,cAAM,gBAAgB,OAAO,SAASA,QAAO,GAAG;AAChD;AAAA,MACF;AAEA,UAAI,SAAS,IAAI;AACf,YAAI,SAAS,WAAW,IAAK,QAAO;AACpC,eAAQ,MAAM,SAAS,KAAK;AAAA,MAC9B;AAEA,YAAM,UAAU,MAAM,aAAa,QAAQ;AAC3C,YAAM,QACJ,SAAS,WAAW,MAChB,IAAI;AAAA,QACF,gBAAgB,OAAO,KAAK;AAAA,QAC5B,eAAe,OAAO;AAAA,QACtB;AAAA,MACF,IACA,IAAI,WAAW,oCAAoC,SAAS,MAAM,IAAI,SAAS,QAAQ,OAAO;AAEpG,UAAI,CAAC,YAAY,CAAC,MAAM,SAAS,SAAS,SAAS,MAAM,EAAG,OAAM;AAClE,YAAM,gBAAgB,OAAO,SAAS,OAAO,KAAK,SAAS,QAAQ,IAAI,aAAa,CAAC;AAAA,IACvF;AAAA,EACF;AAEA,QAAM,WAAW;AAAA;AAAA,IAEf,KAAK,QAAqE;AACxE,aAAO,QAAmC,wBAAwB;AAAA,QAChE,QAAQ;AAAA,QACR,OAAO,EAAE,WAAW,OAAO,WAAW,eAAe,YAAY,MAAM;AAAA,MACzE,CAAC;AAAA,IACH;AAAA;AAAA,IAGA,KAAK,MAAc,QAAqE;AACtF,aAAO,QAAmC,iBAAiB,mBAAmB,IAAI,CAAC,WAAW;AAAA,QAC5F,QAAQ;AAAA,QACR,OAAO;AAAA,UACL,WAAW,OAAO;AAAA,UAClB,eAAe,YAAY;AAAA,UAC3B,SAAS,OAAO;AAAA,UAChB,OAAO,OAAO;AAAA,QAChB;AAAA,MACF,CAAC;AAAA,IACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,KAAK,MAAc,MAAiE;AAClF,UAAI,CAAC,OAAO,eAAe;AACzB,eAAO,QAAQ;AAAA,UACb,IAAI,WAAW,mFAAmF;AAAA,QACpG;AAAA,MACF;AACA,YAAM,EAAE,eAAe,GAAG,KAAK,IAAI;AACnC,aAAO,QAAmC,iBAAiB,mBAAmB,IAAI,CAAC,WAAW;AAAA,QAC5F,QAAQ;AAAA,QACR,SAAS,EAAE,eAAe,UAAU,OAAO,aAAa,GAAG;AAAA,QAC3D,MAAM,KAAK,UAAU,EAAE,eAAe,iBAAiB,YAAY,OAAO,GAAG,KAAK,CAAC;AAAA,MACrF,CAAC;AAAA,IACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,UAAU,MAAiD;AACzD,aAAO,QAAkC,iBAAiB,mBAAmB,IAAI,CAAC,WAAW;AAAA,QAC3F,QAAQ;AAAA,QACR,OAAO,EAAE,eAAe,YAAY,MAAM;AAAA,MAC5C,CAAC;AAAA,IACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAaA,MAAM,UAAU,MAAc,MAA+D;AAC3F,UAAI,CAAC,OAAO,eAAe;AACzB,cAAM,IAAI,WAAW,wFAAwF;AAAA,MAC/G;AACA,YAAM,EAAE,eAAe,QAAQ,MAAM,IAAI;AACzC,UAAI;AACF,eAAO,MAAM,QAAkC,iBAAiB,mBAAmB,IAAI,CAAC,WAAW;AAAA,UACjG,QAAQ;AAAA,UACR,SAAS,EAAE,eAAe,UAAU,OAAO,aAAa,GAAG;AAAA,UAC3D,MAAM,KAAK,UAAU,EAAE,eAAe,iBAAiB,YAAY,OAAO,QAAQ,OAAO,SAAS,MAAM,CAAC;AAAA,QAC3G,CAAC;AAAA,MACH,SAAS,OAAO;AACd,YAAI,iBAAiB,oBAAoB;AACvC,gBAAM,UAAU,gBAAgB,MAAM,OAAO;AAC7C,cAAI,SAAS;AACX,kBAAM,IAAI,+BAA+B,MAAM,SAAS,QAAQ,eAAe,QAAQ,WAAW,MAAM,OAAO;AAAA,UACjH;AAAA,QACF;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA;AAAA,IAGA,QAAQ,EAAE,GAAG,QAAQ,QAAQ;AAAA,IAE7B,YAAY,iBAAwC;AAClD,aAAO,kBAAkB,EAAE,GAAG,QAAQ,GAAG,gBAAgB,CAAC;AAAA,IAC5D;AAAA,IAEA,MAAM,eAAwB,QAAmE;AAC/F,YAAM,SAAS,MAAM,QAA8C,oBAAoB;AAAA,QACrF,QAAQ;AAAA,QACR,OAAO,EAAE,aAAa,OAAO,aAAa,MAAM,OAAO,MAAM,QAAQ,OAAO,OAAO;AAAA,QACnF,SAAS,EAAE,QAAQ,OAAO,OAAO;AAAA,MACnC,CAAC;AACD,aAAO,OAAO;AAAA,IAChB;AAAA,IAEA,MAAM,WACJ,QAC0D;AAC1D,YAAM,QAAyC;AAAA,QAC7C,aAAa,OAAO;AAAA,QACpB,QAAQ,OAAO;AAAA,QACf,OAAO,OAAO;AAAA,QACd,QAAQ,OAAO;AAAA,QACf,MAAM,OAAO;AAAA,MACf;AACA,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,WAAW,CAAC,CAAC,GAAG;AAC/D,cAAM,UAAU,GAAG,EAAE,IAAI;AAAA,MAC3B;AACA,aAAO,QAAyD,YAAY;AAAA,QAC1E,QAAQ;AAAA,QACR;AAAA,QACA,SAAS,EAAE,QAAQ,OAAO,OAAO;AAAA,MACnC,CAAC;AAAA,IACH;AAAA,IAEA,MAAM,aAAsB,QAAiE;AAC3F,YAAM,SAAS,MAAM,QAA8C,eAAe;AAAA,QAChF,QAAQ;AAAA,QACR,OAAO,EAAE,KAAK,OAAO,KAAK,QAAQ,OAAO,OAAO;AAAA,QAChD,SAAS,EAAE,QAAQ,OAAO,OAAO;AAAA,MACnC,CAAC;AACD,aAAO,OAAO;AAAA,IAChB;AAAA,IAEA,cAAc,MAAc,QAAiB;AAC3C,aAAO,QAAsB,cAAc,IAAI,IAAI;AAAA,QACjD,OAAO;AAAA,UACL,eAAe,YAAY;AAAA,UAC3B,QAAQ,UAAU,YAAY;AAAA,UAC9B,MAAM,YAAY,UAAU,UAAU;AAAA,QACxC;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IAEA,WAAW,MAAc,QAAiB;AACxC,aAAO,QAAyB,gBAAgB;AAAA,QAC9C,OAAO;AAAA,UACL;AAAA,UACA,eAAe,YAAY;AAAA,UAC3B,QAAQ,UAAU,YAAY;AAAA,UAC9B,MAAM,YAAY,UAAU,UAAU;AAAA,UACtC,GAAI,YAAY,WAAW,OAAO,eAAe,EAAE,OAAO,OAAO,aAAa,IAAI,CAAC;AAAA,QACrF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IAEA,YAAY,SAA4B,CAAC,GAAG;AAC1C,aAAO,QAA0B,mBAAmB;AAAA,QAClD,OAAO;AAAA,UACL,eAAe,OAAO,iBAAiB,YAAY;AAAA,UACnD,QAAQ,OAAO,UAAU,YAAY;AAAA,UACrC,UAAU,OAAO;AAAA,UACjB,MAAM,OAAO;AAAA,UACb,OAAO,OAAO;AAAA,UACd,QAAQ,OAAO;AAAA,UACf,MAAM,OAAO,QAAQ;AAAA,UACrB,SAAS,OAAO;AAAA,QAClB;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IAEA,gBAAgB,SAAgC,CAAC,GAAG;AAClD,aAAO,QAA8B,yBAAyB;AAAA,QAC5D,OAAO;AAAA,UACL,eAAe,OAAO,iBAAiB,YAAY;AAAA,UACnD,QAAQ,OAAO,UAAU,YAAY;AAAA,UACrC,OAAO,OAAO;AAAA,UACd,QAAQ,OAAO;AAAA,QACjB;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IAEA,QAAQ,KAAa,QAAiB;AACpC,aAAO,QAAsB,cAAc;AAAA,QACzC,OAAO,EAAE,KAAK,eAAe,YAAY,OAAO,QAAQ,UAAU,YAAY,OAAO;AAAA,MACvF,CAAC;AAAA,IACH;AAAA,IAEA,QAAQ,KAAa,QAAiB;AACpC,aAAO,QAAsB,cAAc;AAAA,QACzC,OAAO,EAAE,KAAK,eAAe,YAAY,OAAO,QAAQ,UAAU,YAAY,OAAO;AAAA,MACvF,CAAC;AAAA,IACH;AAAA,IAEA,QAAQ,OAAe,QAAgB,QAAiB;AACtD,aAAO,QAAsB,cAAc,MAAM,aAAa;AAAA,QAC5D,OAAO,EAAE,eAAe,OAAO,QAAQ,UAAU,YAAY,OAAO;AAAA,MACtE,CAAC;AAAA,IACH;AAAA,IAEA,gBAA6B,gBAAwB,WAAoB;AACvE,aAAO,QAAW,sBAAsB,cAAc,IAAI;AAAA,QACxD,OAAO,EAAE,WAAW,eAAe,YAAY,OAAO,QAAQ,YAAY,OAAO;AAAA,MACnF,CAAC;AAAA,IACH;AAAA;AAAA,IAGA,QAAqB,UAAkB,aAA0B,QAAiB;AAChF,aAAO,QAAW,QAAQ,QAAQ,IAAI;AAAA,QACpC,QAAQ;AAAA,QACR,OAAO,EAAE,eAAe,YAAY,OAAO,QAAQ,UAAU,YAAY,QAAQ,GAAG,YAAY;AAAA,MAClG,CAAC;AAAA,IACH;AAAA;AAAA,IAGA,WACE,UACA,MACA,QACA,aACA;AACA,aAAO,QAAW,QAAQ,QAAQ,IAAI;AAAA,QACpC,QAAQ;AAAA,QACR,MAAM,KAAK,UAAU,IAAI;AAAA,QACzB,OAAO,EAAE,eAAe,YAAY,OAAO,QAAQ,UAAU,YAAY,QAAQ,GAAG,YAAY;AAAA,MAClG,CAAC;AAAA,IACH;AAAA,IAEA,aAA0B;AACxB,aAAO,QAAW,aAAa,YAAY,KAAK,EAAE;AAAA,IACpD;AAAA,IAEA,aAA0B;AACxB,aAAO,QAAW,gBAAgB,YAAY,KAAK,EAAE;AAAA,IACvD;AAAA;AAAA,IAGA,eAAe;AACb,aAAO,QAA2B,kBAAkB,YAAY,KAAK,EAAE;AAAA,IACzE;AAAA,IAEA,aAA0B,SAAiB;AACzC,aAAO,QAAW,eAAe,OAAO,IAAI;AAAA,QAC1C,OAAO,EAAE,eAAe,YAAY,MAAM;AAAA,MAC5C,CAAC;AAAA,IACH;AAAA,IAEA;AAAA,EACF;AACF;AAEO,SAAS,mBAAmB,UAAqC;AACtE,SAAO;AAAA,IACL,OAAO,QAA0C;AAC/C,aAAO,kBAAkB,EAAE,GAAG,UAAU,GAAG,OAAO,CAAC;AAAA,IACrD;AAAA,EACF;AACF;;;AChYO,IAAM,sBAAsB,CAAC,WAAW,QAAQ,eAAe,YAAY,OAAO;AAgElF,SAAS,oBAAoB,QAAqD;AACvF,SAAO,CAAC,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAS,OAA+B,KAAK;AACtG;AAGO,SAAS,cAAc,QAA+C;AAC3E,SAAO,CAAC,CAAC,UAAU,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,KAAK,CAAC,oBAAoB,MAAM;AACxG;AAMO,SAAS,aAAa,QAA6C;AACxE,MAAI,oBAAoB,MAAM,EAAG,QAAO;AACxC,MAAI,cAAc,MAAM,EAAG,QAAO;AAClC,SAAO;AACT;AAuIO,IAAM,wBAAwB;AAAA,EACnC,UAAU;AAAA,EACV,cAAc,MAAM;AAAA,EACpB,eAAe,IAAI,OAAO;AAAA,EAC1B,eAAe;AAAA,EACf,kBAAkB;AACpB;AAEO,IAAM,kBAAkB;AACxB,IAAM,uBAAuB;AAG7B,SAAS,mBAAmB,MAAuB;AACxD,MAAI,CAAC,QAAQ,KAAK,SAAS,sBAAsB,cAAe,QAAO;AACvE,MAAI,CAAC,gBAAgB,KAAK,IAAI,EAAG,QAAO;AACxC,SAAO,CAAC,KAAK,MAAM,GAAG,EAAE,KAAK,CAAC,YAAY,YAAY,OAAO,YAAY,IAAI;AAC/E;AAEO,SAAS,iBAAiB,WAA4B;AAC3D,SAAO,qBAAqB,KAAK,SAAS;AAC5C;AAMA,IAAM,UAAU,IAAI,YAAY;AAEhC,SAAS,SAAuB;AAC9B,QAAM,IAAI,WAAW;AACrB,MAAI,CAAC,KAAK,CAAC,EAAE,QAAQ;AACnB,UAAM,IAAI,MAAM,wEAAwE;AAAA,EAC1F;AACA,SAAO,EAAE;AACX;AAEA,SAAS,MAAM,QAA6B;AAC1C,QAAM,QAAQ,IAAI,WAAW,MAAM;AACnC,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,WAAO,MAAM,CAAC,EAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAAA,EAC/C;AACA,SAAO;AACT;AAEA,eAAe,UAAU,OAAgC;AACvD,SAAO,MAAM,MAAM,OAAO,EAAE,OAAO,WAAW,QAAQ,OAAO,KAAK,CAAC,CAAC;AACtE;AAGO,SAAS,WAAW,SAAkC;AAC3D,SAAO,UAAU,OAAO;AAC1B;AAOA,eAAsB,YACpB,OACiB;AACjB,QAAM,SAAS,MAAM,UAAU,KAAK;AACpC,QAAM,QAAQ,OACX,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAU,EACtC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAO,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAE,EAC/C,IAAI,CAAC,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI;AAAA,EAAK,GAAG;AAAA,CAAI,EACxC,KAAK,EAAE;AACV,SAAO,UAAU,KAAK;AACxB;AAGA,eAAsB,UACpB,OAC8B;AAC9B,SAAO,QAAQ;AAAA,IACb,MAAM,IAAI,OAAO,OAAO;AAAA,MACtB,MAAM,EAAE;AAAA,MACR,SAAS,EAAE;AAAA,MACX,QAAQ,YAAY,KAAK,OAAO,EAAE,WAAW,WAAW,EAAE,SAAS,MAAM,WAAW,EAAE,OAAO;AAAA,IAC/F,EAAE;AAAA,EACJ;AACF;AAGO,SAAS,mBAAsD,UAA8B;AAClG,QAAM,OAAgC,EAAE,GAAG,SAAS;AACpD,aAAW,OAAO,oBAAqB,QAAO,KAAK,GAAG;AACtD,SAAO;AACT;;;ACtWO,SAAS,YAAY,OAAoC;AAC9D,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,QAAQ,SAAS,cAAc;AACvF;AAEO,SAAS,WAAW,OAAqC;AAC9D,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,OAAO,UAAU,WAAW,QAAQ,MAAM;AACnD;AAGO,SAAS,iBAAiB,OAAqC;AACpE,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,SAAO,MAAM;AACf;AAgBO,SAAS,YACd,OACA,OACA,SACoB;AACpB,QAAM,UAAU,WAAW,KAAK;AAChC,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,OAAO,QAAQ,SAAS,QAAQ,QAAQ,EAAE;AAChD,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,WAAW,iBAAiB,KAAK;AAEvC,MAAI,OAAO,SAAS;AAClB,WAAO,GAAG,IAAI,WAAW,MAAM,OAAO,IAAI,OAAO,IAAI,MAAM,IAAI,YAAY,WAAW;AAAA,EACxF;AACA,SAAO,GAAG,IAAI,WAAW,OAAO;AAClC;AAUO,SAAS,eAAe,SAA0B;AACvD,SAAO,CACL,OACA,OACA,WACuB,YAAY,OAAO,OAAO,EAAE,GAAG,SAAS,QAAQ,UAAU,QAAQ,OAAO,CAAC;AACrG;","names":["contentHash","error"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botlr/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.6.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": {