@openstaticfish/chattybox-config 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -2,17 +2,104 @@
2
2
 
3
3
  Typed configuration and runtime validation for ChattyBox config-as-code projects.
4
4
 
5
+ ## Availability and Usage
6
+
7
+ Public registry check on 2026-09-07: `@openstaticfish/chattybox-config@0.2.0` is available. This checkout prepares the expanded contract as `0.3.0`, but that version has not been published. The published types do not include `project`, source discovery/scheduling fields, `runtime.localeMode/defaultLocale/allowLocaleOverride`, `widget.icon`, or `widget.showBranding`. The example below targets current repository source, not the published `0.2.0` declarations. Publishing config `0.3.0` and the matching CLI release is required because the CLI validator is bundled, not replaced by upgrading a config dependency.
8
+
9
+ Install the published baseline with `bun add -d @openstaticfish/chattybox-config`. Use ESM imports; the package exports `import` and `types` entrypoints with no CommonJS `require` entrypoint. It declares no minimum Node/Bun version or browser support matrix. The implementation uses the standard `URL` API and has no network or filesystem side effects. Published-consumer CI exercises Node 20, 22, and 24 on Linux; the separate CLI declares Node `>=20`.
10
+
5
11
  ```ts
6
12
  import { defineConfig } from "@openstaticfish/chattybox-config";
7
13
 
8
14
  export default defineConfig({
9
15
  schemaVersion: "1",
10
- assistant: { name: "Support" },
16
+ project: { description: "Managed from Git" },
17
+ assistant: { name: "Support", promptProfile: "support" },
11
18
  knowledge: {
12
- sources: [{ type: "website", url: "https://docs.example.com" }],
19
+ sources: [{
20
+ type: "website",
21
+ url: "https://docs.example.com",
22
+ mode: "sitemap",
23
+ sitemapUrl: "https://docs.example.com/sitemap.xml",
24
+ autoRescrape: true,
25
+ rescrapeInterval: "weekly",
26
+ }],
13
27
  },
14
- widget: { enabled: true },
28
+ runtime: { localeMode: "auto", defaultLocale: "en", allowLocaleOverride: true },
29
+ widget: { enabled: true, icon: { type: "emoji", emoji: "💬" } },
15
30
  });
16
31
  ```
17
32
 
18
- Version 1 supports exactly one website source per project when `knowledge` is provided. Production deployments require that source so the runtime can replace and re-index the project corpus deterministically.
33
+ For a published-`0.2.0`-compatible typed baseline, use `defineConfig({ schemaVersion: "1", assistant: { name: "Support" }, knowledge: { sources: [{ type: "website", url: "https://docs.example.com" }] }, widget: { enabled: true } })`.
34
+
35
+ ## Exports and Validation
36
+
37
+ The package exports `defineConfig(config)` and `validateConfig(config)`, both with the signature `(config: ChattyboxConfig) => ChattyboxConfig`. Both validate synchronously, throw an ordinary `Error` at the first failure, and return the same object. They do not clone, freeze, trim stored strings, coerce types, apply defaults, or perform network requests. Their parameter is typed as `ChattyboxConfig`, not `unknown`; handle untrusted input and validation exceptions in your tooling.
38
+
39
+ Type-only exports are `ChattyboxConfig`, `WebsiteSource`, `ConfigMode` (`collaborative | config_locked`), and `DeploymentEnvironment` (`development | preview | production`). The mode/environment types are not fields you can add to config. This package does not export a JSON Schema document, `$schema` URL, file loader, deployment client, or environment overlay/merge utility. `schemaVersion: "1"` selects the config contract, not the package version.
40
+
41
+ The CLI only reads JSON. A TypeScript default export as above is useful in your own build tooling, but is not directly loadable by the CLI. `defineConfig` validates immediately; it does not deploy anything or make the object immutable. Backend version records, rather than local objects, are the immutable snapshots.
42
+
43
+ ## Current Source Contract
44
+
45
+ Only `schemaVersion` and `assistant` are required at the top level. Optional means omitted/`undefined`, not `null`. Local validation rejects unknown properties at every documented object boundary, matching the backend's strict config objects. TypeScript checks and backend deployment/entitlement validation remain separate boundaries.
46
+
47
+ | Field | Validation and meaning |
48
+ |---|---|
49
+ | `schemaVersion` | Exactly the string `"1"`. |
50
+ | `project.description` | Optional non-empty string. `project` itself is optional. |
51
+ | `assistant.name` | Required non-empty string; also becomes the project's dashboard name. |
52
+ | `assistant.promptProfile` | `default`, `support`, `sales`, `sarcastic`, or `custom`. Custom requires a non-empty `systemPrompt`. |
53
+ | `assistant.systemPrompt`, `assistant.fallbackMessage` | Optional non-empty strings. An explicit system prompt wins over a built-in profile. |
54
+ | `knowledge.sources` | Exactly one website source when `knowledge` is provided. No empty or multi-source list. May be omitted locally/development; required for preview/production. |
55
+ | `runtime` | Optional locale settings described below. |
56
+ | `widget.enabled` | Required boolean whenever `widget` is provided. |
57
+ | `widget.position` | `bottom-right`, `bottom-left`, `top-right`, or `top-left`. |
58
+ | `widget.headerTitle`, `widget.welcomeMessage` | Optional non-empty strings. |
59
+ | `widget.showBranding` | Optional boolean. Local validation does not check entitlement. |
60
+ | `widget.theme` | Optional `primaryColor`, `backgroundColor`, `textColor`, each a six-digit `#RRGGBB` string (case-insensitive). No shorthand, names, or alpha. |
61
+ | `widget.icon` | Required `type` when present: `default`, `emoji`, or `url`. Optional `size`: `small`, `medium`, `large`. Emoji type requires a non-empty `emoji`; URL type requires an HTTP(S) `url`. |
62
+
63
+ Non-empty means the string contains non-whitespace text; the original string is returned unchanged. Icon validation does not count emoji characters, fetch an image, or check MIME type. Only the selected icon payload is required/validated by local runtime checks; inactive `emoji`/`url` fields are not a discriminated union in the type. Uploaded storage IDs are not config fields.
64
+
65
+ ### Website Sources
66
+
67
+ | Field | Validation and runtime use |
68
+ |---|---|
69
+ | `type`, `url` | Type must be `website`; URL must parse as HTTP(S). No local reachability, same-origin, crawl-permission, or public-host check. |
70
+ | `mode` | Optional `homepage`, `sitemap`, `manual`, or `crawl`; runtime defaults to crawl. |
71
+ | `sitemapUrl` | Optional HTTP(S) URL; only used for explicit sitemap mode. An explicit sitemap yielding no URLs fails; omission tries `/sitemap.xml`, then `/sitemap_index.xml`, then homepage. |
72
+ | `manualUrls` | Optional array of HTTP(S) URLs; non-empty in manual mode. Manual discovery selects these URLs. |
73
+ | `include`, `exclude` | Optional arrays of non-empty strings. Case-sensitive URL substring matching, not glob/regex. Any include match admits, any exclude match rejects. Empty lists do not filter. Applied to all discovery modes. |
74
+ | `maxPages` | Optional positive integer; the matching `0.3.0` backend stores the request without clamping it to an entitlement. Each run limits outbound discovery and selection to the smaller of the request and the larger of current provider capacity or known existing project pages. |
75
+ | `maxDepth` | Optional integer from 0 through 20; link-crawl fallback depth, not sitemap depth. When `mode` is omitted it still selects crawl and preserves an explicit depth, including zero. |
76
+ | `autoRescrape` | Optional boolean; defaults to false at runtime. |
77
+ | `rescrapeInterval` | `daily`, `weekly`, or `monthly`; required when `autoRescrape` is true. Interval alone does not enable refresh. Monthly is 30 days; scheduling is not an exact execution-time guarantee. |
78
+
79
+ Homepage selects only `url` for the next refresh. Crawl first tries conventional sitemaps at the source origin, then follows links from `url` if neither sitemap yields URLs. Selecting fewer URLs does not delete existing corpus pages.
80
+
81
+ ### Locale Settings
82
+
83
+ Supported codes: `en`, `de`, `fr`, `es`, `it`, `nl`, `pl`, `pt`, `sv`, `fi`, `et`, `cs`, `cy`, `id`.
84
+
85
+ `runtime.localeMode` is `auto` or `fixed`. Fixed mode requires `defaultLocale`; `allowLocaleOverride` is an optional boolean independent of mode. Omission of runtime settings resets to auto, default `en`, and overrides allowed when applied.
86
+
87
+ `runtime.locale` is shorthand for fixed mode with overrides disabled. Current local validation forbids combining it with `localeMode`, `defaultLocale`, or `allowLocaleOverride: true`; an explicit false is accepted. Published `0.2.0` types expose only this shorthand, not the expanded controls.
88
+
89
+ ## Backend Application
90
+
91
+ **Environment labels do not isolate runtimes.** Development only records a version/pointer. Preview and production require one website source and both overwrite the same project's runtime/corpus used by its public keys; the latest runtime promotion wins. A preview-only token is not protection for production traffic in the same project. Use distinct projects/tokens/keys for isolation.
92
+
93
+ Config application is full replacement of config-owned settings, not a partial update. Omitted description, prompt/profile/fallback, and widget copy/theme/position overrides clear so runtime defaults apply. Omitted widget enablement becomes true; icon becomes default/medium and an uploaded icon is unlinked. Source mode defaults to crawl, filters clear when omitted, and automatic refresh defaults to off; mode-specific fields apply only to their corresponding mode.
94
+
95
+ **Branding is the replacement and lock exception:** omitted `widget.showBranding` preserves its stored value, otherwise defaults to true. Explicit values overwrite it on the next runtime promotion. The dashboard branding-only control remains available even under config lock, subject to entitlement. Hiding branding and automatic daily refresh require Starter or higher; Free accepts weekly/monthly refresh and still enforces branding in public runtime output.
96
+
97
+ With the matching backend for the prepared `0.3.0` release, the immutable version keeps the requested `maxPages` value without clamping it to an entitlement. Each run limits outbound discovery and selection to the smaller of that request and the larger of current provider page capacity or known existing project pages. At zero or denied capacity, only known existing pages may refresh; with none, it makes no unknown-page outbound discovery. Omission uses the included-plan request default: Free 10, Starter 150, Pro 5,000, Business 25,000. Persistent page capacity is shared across the account's projects; this changes no owner-pool or plan entitlements. The worker rechecks page/PAYG permission and reserves new pages against that pool. Entitlement checks apply on preview/production promotion and rollback, not local validation/development recording.
98
+
99
+ Promotion reads the server-cached project/workspace plan rather than querying the billing provider live. Recent billing changes therefore also require verification of plan synchronization and the worker's actual allowance.
100
+
101
+ Runtime promotion applies configuration and queues an asynchronous in-place scrape. It does not wait for indexing, prune old URLs omitted by the new source/filter, or atomically swap a historical corpus. Changed content triggers embedding work. Failed refreshes do not revert config and can leave partial page updates. Jobs record a version ID but also read mutable scrape settings, so avoid overlapping deployments when consistency matters. Rollback reapplies a stored version under current entitlements and refreshes again; it is not historical content restoration. Config versions stay immutable, while environment pointers/promotion metadata are overwritten rather than appended to a promotion event log.
102
+
103
+ Config lock is enabled separately, not via `ConfigMode` in the JSON. It is **irreversible through the current dashboard, CLI, and public API**. It blocks config-owned dashboard settings and manual corpus writes, not the branding-only control, token/public-key management, scheduled work, or project deletion. Keep a verified deployment-token workflow before locking. Tokens belong in server/CI secrets, never in config or browser code.
104
+
105
+ The [CLI README](../cli/README.md) and [deployment guide](https://chattybox.ai/docs/cli/) cover tokens, flags, errors, and CI. Current-source local validation and backend deployment validation both reject unknown config keys, blank include/exclude entries and widget copy, and incompatible locale shorthand settings. Confirm the deployed backend and newly released package artifacts before relying on expanded fields in production.
package/dist/index.d.ts CHANGED
@@ -3,12 +3,21 @@ export type DeploymentEnvironment = "development" | "preview" | "production";
3
3
  export type WebsiteSource = {
4
4
  type: "website";
5
5
  url: string;
6
+ mode?: "homepage" | "sitemap" | "manual" | "crawl";
7
+ sitemapUrl?: string;
8
+ manualUrls?: Array<string>;
6
9
  include?: Array<string>;
7
10
  exclude?: Array<string>;
8
11
  maxPages?: number;
12
+ maxDepth?: number;
13
+ autoRescrape?: boolean;
14
+ rescrapeInterval?: "daily" | "weekly" | "monthly";
9
15
  };
10
16
  export type ChattyboxConfig = {
11
17
  schemaVersion: "1";
18
+ project?: {
19
+ description?: string;
20
+ };
12
21
  assistant: {
13
22
  name: string;
14
23
  systemPrompt?: string;
@@ -19,6 +28,10 @@ export type ChattyboxConfig = {
19
28
  sources: Array<WebsiteSource>;
20
29
  };
21
30
  runtime?: {
31
+ localeMode?: "auto" | "fixed";
32
+ defaultLocale?: string;
33
+ allowLocaleOverride?: boolean;
34
+ /** Shorthand for fixed mode with locale overrides disabled. */
22
35
  locale?: string;
23
36
  };
24
37
  widget?: {
@@ -26,6 +39,14 @@ export type ChattyboxConfig = {
26
39
  position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
27
40
  headerTitle?: string;
28
41
  welcomeMessage?: string;
42
+ /** Starter and higher plans may hide the maintained widget attribution. */
43
+ showBranding?: boolean;
44
+ icon?: {
45
+ type: "default" | "emoji" | "url";
46
+ size?: "small" | "medium" | "large";
47
+ emoji?: string;
48
+ url?: string;
49
+ };
29
50
  theme?: {
30
51
  primaryColor?: string;
31
52
  backgroundColor?: string;
package/dist/index.js CHANGED
@@ -3,6 +3,29 @@ var HEX_COLOR = /^#[0-9a-f]{6}$/i;
3
3
  var PROMPT_PROFILES = new Set(["default", "support", "sales", "sarcastic", "custom"]);
4
4
  var WIDGET_POSITIONS = new Set(["bottom-right", "bottom-left", "top-right", "top-left"]);
5
5
  var WIDGET_LOCALES = new Set(["en", "de", "fr", "es", "it", "nl", "pl", "pt", "sv", "fi", "et", "cs", "cy", "id"]);
6
+ var SOURCE_MODES = new Set(["homepage", "sitemap", "manual", "crawl"]);
7
+ var RESCRAPE_INTERVALS = new Set(["daily", "weekly", "monthly"]);
8
+ var ICON_TYPES = new Set(["default", "emoji", "url"]);
9
+ var ICON_SIZES = new Set(["small", "medium", "large"]);
10
+ function isRecord(value) {
11
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12
+ }
13
+ function requireRecord(value, path, keys) {
14
+ if (!isRecord(value))
15
+ throw new Error(`${path} must be an object`);
16
+ if (keys) {
17
+ for (const key of Object.keys(value)) {
18
+ if (!keys.includes(key))
19
+ throw new Error(`${path}.${key} is not supported`);
20
+ }
21
+ }
22
+ }
23
+ function requireKnownKeys(value, path, keys) {
24
+ for (const key of Object.keys(value)) {
25
+ if (!keys.has(key))
26
+ throw new Error(`${path}.${key} is not supported`);
27
+ }
28
+ }
6
29
  function requireNonEmptyString(value, path) {
7
30
  if (typeof value !== "string" || value.trim().length === 0) {
8
31
  throw new Error(`${path} must be a non-empty string`);
@@ -13,50 +36,186 @@ function validateOptionalColor(value, path) {
13
36
  throw new Error(`${path} must be a six-digit hex color`);
14
37
  }
15
38
  }
39
+ function validateOptionalStringArray(value, path) {
40
+ if (value === undefined)
41
+ return;
42
+ if (!Array.isArray(value))
43
+ throw new Error(`${path} must be an array of strings`);
44
+ value.forEach((entry, index) => requireNonEmptyString(entry, `${path}[${index}]`));
45
+ }
46
+ function validateHttpUrl(value, path) {
47
+ requireNonEmptyString(value, path);
48
+ try {
49
+ const url = new URL(value);
50
+ if (url.protocol !== "http:" && url.protocol !== "https:")
51
+ throw new Error;
52
+ } catch {
53
+ throw new Error(`${path} must be an HTTP(S) URL`);
54
+ }
55
+ }
16
56
  function validateConfig(config) {
17
- if (!config || typeof config !== "object")
57
+ if (!isRecord(config))
18
58
  throw new Error("Config must be an object");
59
+ requireRecord(config, "Config", ["schemaVersion", "project", "assistant", "knowledge", "runtime", "widget"]);
60
+ requireKnownKeys(config, "config", new Set(["schemaVersion", "project", "assistant", "knowledge", "runtime", "widget"]));
19
61
  if (config.schemaVersion !== "1")
20
62
  throw new Error('schemaVersion must be "1"');
21
- requireNonEmptyString(config.assistant?.name, "assistant.name");
63
+ requireRecord(config.assistant, "assistant", ["name", "systemPrompt", "promptProfile", "fallbackMessage"]);
64
+ requireKnownKeys(config.assistant, "assistant", new Set(["name", "systemPrompt", "promptProfile", "fallbackMessage"]));
65
+ requireNonEmptyString(config.assistant.name, "assistant.name");
66
+ if (config.project !== undefined) {
67
+ requireRecord(config.project, "project", ["description"]);
68
+ requireKnownKeys(config.project, "project", new Set(["description"]));
69
+ if (config.project.description !== undefined) {
70
+ requireNonEmptyString(config.project.description, "project.description");
71
+ }
72
+ }
22
73
  if (config.assistant.systemPrompt !== undefined) {
23
74
  requireNonEmptyString(config.assistant.systemPrompt, "assistant.systemPrompt");
24
75
  }
25
- if (config.assistant.promptProfile !== undefined && !PROMPT_PROFILES.has(config.assistant.promptProfile)) {
76
+ if (config.assistant.promptProfile === "custom" && !config.assistant.systemPrompt) {
77
+ throw new Error("assistant.systemPrompt is required for the custom prompt profile");
78
+ }
79
+ if (config.assistant.promptProfile !== undefined && (typeof config.assistant.promptProfile !== "string" || !PROMPT_PROFILES.has(config.assistant.promptProfile))) {
26
80
  throw new Error("assistant.promptProfile is not supported");
27
81
  }
28
82
  if (config.assistant.fallbackMessage !== undefined) {
29
83
  requireNonEmptyString(config.assistant.fallbackMessage, "assistant.fallbackMessage");
30
84
  }
31
- if (config.knowledge !== undefined && config.knowledge.sources.length !== 1) {
32
- throw new Error("knowledge.sources must contain exactly one website source when provided");
85
+ let sources = [];
86
+ if (config.knowledge !== undefined) {
87
+ requireRecord(config.knowledge, "knowledge", ["sources"]);
88
+ requireKnownKeys(config.knowledge, "knowledge", new Set(["sources"]));
89
+ if (!Array.isArray(config.knowledge.sources)) {
90
+ throw new Error("knowledge.sources must be an array");
91
+ }
92
+ if (config.knowledge.sources.length !== 1) {
93
+ throw new Error("knowledge.sources must contain exactly one website source when provided");
94
+ }
95
+ sources = config.knowledge.sources;
33
96
  }
34
- for (const [index, source] of (config.knowledge?.sources ?? []).entries()) {
97
+ for (const [index, sourceValue] of sources.entries()) {
98
+ const sourcePath = `knowledge.sources[${index}]`;
99
+ requireRecord(sourceValue, sourcePath, ["type", "url", "mode", "sitemapUrl", "manualUrls", "include", "exclude", "maxPages", "maxDepth", "autoRescrape", "rescrapeInterval"]);
100
+ const source = sourceValue;
101
+ requireKnownKeys(source, sourcePath, new Set([
102
+ "type",
103
+ "url",
104
+ "mode",
105
+ "sitemapUrl",
106
+ "manualUrls",
107
+ "include",
108
+ "exclude",
109
+ "maxPages",
110
+ "maxDepth",
111
+ "autoRescrape",
112
+ "rescrapeInterval"
113
+ ]));
35
114
  if (source.type !== "website") {
36
- throw new Error(`knowledge.sources[${index}].type is not supported`);
115
+ throw new Error(`${sourcePath}.type is not supported`);
37
116
  }
38
- requireNonEmptyString(source.url, `knowledge.sources[${index}].url`);
39
- try {
40
- const url = new URL(source.url);
41
- if (url.protocol !== "http:" && url.protocol !== "https:")
42
- throw new Error;
43
- } catch {
44
- throw new Error(`knowledge.sources[${index}].url must be an HTTP(S) URL`);
117
+ validateHttpUrl(source.url, `${sourcePath}.url`);
118
+ const maxPages = source.maxPages;
119
+ if (maxPages !== undefined && (typeof maxPages !== "number" || !Number.isInteger(maxPages) || maxPages < 1)) {
120
+ throw new Error(`${sourcePath}.maxPages must be a positive integer`);
45
121
  }
46
- if (source.maxPages !== undefined && (!Number.isInteger(source.maxPages) || source.maxPages < 1)) {
47
- throw new Error(`knowledge.sources[${index}].maxPages must be a positive integer`);
122
+ if (source.mode !== undefined && (typeof source.mode !== "string" || !SOURCE_MODES.has(source.mode))) {
123
+ throw new Error(`${sourcePath}.mode is not supported`);
48
124
  }
125
+ if (source.sitemapUrl !== undefined)
126
+ validateHttpUrl(source.sitemapUrl, `${sourcePath}.sitemapUrl`);
127
+ const manualUrls = source.manualUrls;
128
+ validateOptionalStringArray(manualUrls, `${sourcePath}.manualUrls`);
129
+ for (const [urlIndex, manualUrl] of (manualUrls ?? []).entries()) {
130
+ validateHttpUrl(manualUrl, `${sourcePath}.manualUrls[${urlIndex}]`);
131
+ }
132
+ if (source.mode === "manual" && (!manualUrls || manualUrls.length === 0)) {
133
+ throw new Error(`${sourcePath}.manualUrls must contain at least one URL in manual mode`);
134
+ }
135
+ const maxDepth = source.maxDepth;
136
+ if (maxDepth !== undefined && (typeof maxDepth !== "number" || !Number.isInteger(maxDepth) || maxDepth < 0 || maxDepth > 20)) {
137
+ throw new Error(`${sourcePath}.maxDepth must be an integer between 0 and 20`);
138
+ }
139
+ if (source.autoRescrape !== undefined && typeof source.autoRescrape !== "boolean") {
140
+ throw new Error(`${sourcePath}.autoRescrape must be a boolean`);
141
+ }
142
+ if (source.rescrapeInterval !== undefined && (typeof source.rescrapeInterval !== "string" || !RESCRAPE_INTERVALS.has(source.rescrapeInterval))) {
143
+ throw new Error(`${sourcePath}.rescrapeInterval is not supported`);
144
+ }
145
+ if (source.autoRescrape && !source.rescrapeInterval) {
146
+ throw new Error(`${sourcePath}.rescrapeInterval is required when autoRescrape is enabled`);
147
+ }
148
+ validateOptionalStringArray(source.include, `${sourcePath}.include`);
149
+ validateOptionalStringArray(source.exclude, `${sourcePath}.exclude`);
49
150
  }
50
- if (config.runtime?.locale !== undefined && !WIDGET_LOCALES.has(config.runtime.locale)) {
51
- throw new Error("runtime.locale is not supported");
151
+ if (config.runtime !== undefined) {
152
+ requireRecord(config.runtime, "runtime", ["locale", "localeMode", "defaultLocale", "allowLocaleOverride"]);
153
+ requireKnownKeys(config.runtime, "runtime", new Set(["locale", "localeMode", "defaultLocale", "allowLocaleOverride"]));
154
+ for (const [path, locale] of [["runtime.locale", config.runtime.locale], ["runtime.defaultLocale", config.runtime.defaultLocale]]) {
155
+ if (locale !== undefined && (typeof locale !== "string" || !WIDGET_LOCALES.has(locale))) {
156
+ throw new Error(`${path} is not supported`);
157
+ }
158
+ }
159
+ if (config.runtime.locale !== undefined && (config.runtime.localeMode !== undefined || config.runtime.defaultLocale !== undefined || config.runtime.allowLocaleOverride === true)) {
160
+ throw new Error("runtime.locale cannot be combined with localeMode, defaultLocale, or an enabled locale override");
161
+ }
162
+ if (config.runtime.localeMode === "fixed" && !config.runtime.defaultLocale) {
163
+ throw new Error("runtime.defaultLocale is required in fixed locale mode");
164
+ }
165
+ if (config.runtime.localeMode !== undefined && config.runtime.localeMode !== "auto" && config.runtime.localeMode !== "fixed") {
166
+ throw new Error("runtime.localeMode is not supported");
167
+ }
168
+ if (config.runtime.allowLocaleOverride !== undefined && typeof config.runtime.allowLocaleOverride !== "boolean") {
169
+ throw new Error("runtime.allowLocaleOverride must be a boolean");
170
+ }
52
171
  }
53
172
  if (config.widget !== undefined) {
173
+ requireRecord(config.widget, "widget", ["enabled", "position", "headerTitle", "welcomeMessage", "showBranding", "icon", "theme"]);
174
+ requireKnownKeys(config.widget, "widget", new Set([
175
+ "enabled",
176
+ "position",
177
+ "headerTitle",
178
+ "welcomeMessage",
179
+ "showBranding",
180
+ "icon",
181
+ "theme"
182
+ ]));
54
183
  if (typeof config.widget.enabled !== "boolean") {
55
184
  throw new Error("widget.enabled must be a boolean");
56
185
  }
57
- if (config.widget.position !== undefined && !WIDGET_POSITIONS.has(config.widget.position)) {
186
+ if (config.widget.showBranding !== undefined && typeof config.widget.showBranding !== "boolean") {
187
+ throw new Error("widget.showBranding must be a boolean");
188
+ }
189
+ if (config.widget.position !== undefined && (typeof config.widget.position !== "string" || !WIDGET_POSITIONS.has(config.widget.position))) {
58
190
  throw new Error("widget.position is not supported");
59
191
  }
192
+ const icon = config.widget.icon;
193
+ if (icon !== undefined)
194
+ requireRecord(icon, "widget.icon", ["type", "size", "emoji", "url"]);
195
+ if (icon !== undefined)
196
+ requireKnownKeys(icon, "widget.icon", new Set(["type", "size", "emoji", "url"]));
197
+ if (icon && !ICON_TYPES.has(icon.type))
198
+ throw new Error("widget.icon.type is not supported");
199
+ if (icon?.size !== undefined && (typeof icon.size !== "string" || !ICON_SIZES.has(icon.size))) {
200
+ throw new Error("widget.icon.size is not supported");
201
+ }
202
+ if (icon?.type === "emoji")
203
+ requireNonEmptyString(icon.emoji, "widget.icon.emoji");
204
+ if (icon?.type === "url")
205
+ validateHttpUrl(icon.url, "widget.icon.url");
206
+ for (const field of ["emoji", "url"]) {
207
+ if (icon?.[field] !== undefined && typeof icon[field] !== "string") {
208
+ throw new Error(`widget.icon.${field} must be a string`);
209
+ }
210
+ }
211
+ if (config.widget.headerTitle !== undefined)
212
+ requireNonEmptyString(config.widget.headerTitle, "widget.headerTitle");
213
+ if (config.widget.welcomeMessage !== undefined)
214
+ requireNonEmptyString(config.widget.welcomeMessage, "widget.welcomeMessage");
215
+ if (config.widget.theme !== undefined)
216
+ requireRecord(config.widget.theme, "widget.theme", ["primaryColor", "backgroundColor", "textColor"]);
217
+ if (config.widget.theme !== undefined)
218
+ requireKnownKeys(config.widget.theme, "widget.theme", new Set(["primaryColor", "backgroundColor", "textColor"]));
60
219
  }
61
220
  validateOptionalColor(config.widget?.theme?.primaryColor, "widget.theme.primaryColor");
62
221
  validateOptionalColor(config.widget?.theme?.backgroundColor, "widget.theme.backgroundColor");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openstaticfish/chattybox-config",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",