@openstaticfish/chattybox-config 0.3.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.
Files changed (3) hide show
  1. package/README.md +78 -4
  2. package/dist/index.js +37 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,12 @@
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
 
@@ -16,7 +22,7 @@ export default defineConfig({
16
22
  mode: "sitemap",
17
23
  sitemapUrl: "https://docs.example.com/sitemap.xml",
18
24
  autoRescrape: true,
19
- rescrapeInterval: "daily",
25
+ rescrapeInterval: "weekly",
20
26
  }],
21
27
  },
22
28
  runtime: { localeMode: "auto", defaultLocale: "en", allowLocaleOverride: true },
@@ -24,8 +30,76 @@ export default defineConfig({
24
30
  });
25
31
  ```
26
32
 
27
- Schema version 1 supports exactly one website source per project when `knowledge` is provided. Preview and production deployments require that source for an in-place, non-destructive refresh. Discovery modes are `homepage`, `sitemap`, `manual`, and `crawl`; omitting mode means crawl and preserves an explicit `maxDepth`, including zero. Source changes and include/exclude filters govern discovery, not deletion of existing indexed pages. Existing content consumes page capacity, so check refresh outcomes when changing sources.
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.
28
102
 
29
- Unknown fields and empty include/exclude entries are rejected. Preview and production are promotion labels for one shared project runtime, not isolated environments. Use separate projects for independent runtimes.
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.
30
104
 
31
- This contract requires config and CLI 0.3.0 or later. Version 0.2.0 lacks several fields in this example; use compatible config and CLI versions together.
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.js CHANGED
@@ -20,6 +20,12 @@ function requireRecord(value, path, keys) {
20
20
  }
21
21
  }
22
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
+ }
23
29
  function requireNonEmptyString(value, path) {
24
30
  if (typeof value !== "string" || value.trim().length === 0) {
25
31
  throw new Error(`${path} must be a non-empty string`);
@@ -51,12 +57,15 @@ function validateConfig(config) {
51
57
  if (!isRecord(config))
52
58
  throw new Error("Config must be an object");
53
59
  requireRecord(config, "Config", ["schemaVersion", "project", "assistant", "knowledge", "runtime", "widget"]);
60
+ requireKnownKeys(config, "config", new Set(["schemaVersion", "project", "assistant", "knowledge", "runtime", "widget"]));
54
61
  if (config.schemaVersion !== "1")
55
62
  throw new Error('schemaVersion must be "1"');
56
63
  requireRecord(config.assistant, "assistant", ["name", "systemPrompt", "promptProfile", "fallbackMessage"]);
64
+ requireKnownKeys(config.assistant, "assistant", new Set(["name", "systemPrompt", "promptProfile", "fallbackMessage"]));
57
65
  requireNonEmptyString(config.assistant.name, "assistant.name");
58
66
  if (config.project !== undefined) {
59
67
  requireRecord(config.project, "project", ["description"]);
68
+ requireKnownKeys(config.project, "project", new Set(["description"]));
60
69
  if (config.project.description !== undefined) {
61
70
  requireNonEmptyString(config.project.description, "project.description");
62
71
  }
@@ -76,6 +85,7 @@ function validateConfig(config) {
76
85
  let sources = [];
77
86
  if (config.knowledge !== undefined) {
78
87
  requireRecord(config.knowledge, "knowledge", ["sources"]);
88
+ requireKnownKeys(config.knowledge, "knowledge", new Set(["sources"]));
79
89
  if (!Array.isArray(config.knowledge.sources)) {
80
90
  throw new Error("knowledge.sources must be an array");
81
91
  }
@@ -88,6 +98,19 @@ function validateConfig(config) {
88
98
  const sourcePath = `knowledge.sources[${index}]`;
89
99
  requireRecord(sourceValue, sourcePath, ["type", "url", "mode", "sitemapUrl", "manualUrls", "include", "exclude", "maxPages", "maxDepth", "autoRescrape", "rescrapeInterval"]);
90
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
+ ]));
91
114
  if (source.type !== "website") {
92
115
  throw new Error(`${sourcePath}.type is not supported`);
93
116
  }
@@ -127,6 +150,7 @@ function validateConfig(config) {
127
150
  }
128
151
  if (config.runtime !== undefined) {
129
152
  requireRecord(config.runtime, "runtime", ["locale", "localeMode", "defaultLocale", "allowLocaleOverride"]);
153
+ requireKnownKeys(config.runtime, "runtime", new Set(["locale", "localeMode", "defaultLocale", "allowLocaleOverride"]));
130
154
  for (const [path, locale] of [["runtime.locale", config.runtime.locale], ["runtime.defaultLocale", config.runtime.defaultLocale]]) {
131
155
  if (locale !== undefined && (typeof locale !== "string" || !WIDGET_LOCALES.has(locale))) {
132
156
  throw new Error(`${path} is not supported`);
@@ -147,6 +171,15 @@ function validateConfig(config) {
147
171
  }
148
172
  if (config.widget !== undefined) {
149
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
+ ]));
150
183
  if (typeof config.widget.enabled !== "boolean") {
151
184
  throw new Error("widget.enabled must be a boolean");
152
185
  }
@@ -159,6 +192,8 @@ function validateConfig(config) {
159
192
  const icon = config.widget.icon;
160
193
  if (icon !== undefined)
161
194
  requireRecord(icon, "widget.icon", ["type", "size", "emoji", "url"]);
195
+ if (icon !== undefined)
196
+ requireKnownKeys(icon, "widget.icon", new Set(["type", "size", "emoji", "url"]));
162
197
  if (icon && !ICON_TYPES.has(icon.type))
163
198
  throw new Error("widget.icon.type is not supported");
164
199
  if (icon?.size !== undefined && (typeof icon.size !== "string" || !ICON_SIZES.has(icon.size))) {
@@ -179,6 +214,8 @@ function validateConfig(config) {
179
214
  requireNonEmptyString(config.widget.welcomeMessage, "widget.welcomeMessage");
180
215
  if (config.widget.theme !== undefined)
181
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"]));
182
219
  }
183
220
  validateOptionalColor(config.widget?.theme?.primaryColor, "widget.theme.primaryColor");
184
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.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",