@marketrix.ai/widget 4.0.134 → 4.0.136

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
@@ -71,14 +71,15 @@ All modes also accept the common options below.
71
71
 
72
72
  These apply to every mode (script attribute → config key):
73
73
 
74
- | Config key | Script attribute | Type | Description |
75
- | ------------------------- | --------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
76
- | `mtxApiHost` | `mtx-api-host` | string | API server URL, e.g. `https://api.marketrix.ai`. The widget has no baked-in API host — you must supply it. |
77
- | `container` | — | `HTMLElement` | Element to mount inside (programmatic only). Defaults to a container appended to `<body>`. |
78
- | `userId` | — | number | Associates widget activity with one of your users. |
79
- | `widget_position_z_index` | — | number | `z-index` floor for the launcher and panel. Raised to the widget's own layer token if you pass a lower value. |
80
- | `show_widget` | — | boolean | When `false`, the widget initializes fully but its UI stays hidden. Default `true`. |
81
- | `use_screenshare` | `mtx-use-screenshare` | boolean | When `false`, screen-share requests are auto-denied and the Share Screen button is hidden. Default `true`. Disable via `mtx-use-screenshare="false"`. |
74
+ | Config key | Script attribute | Type | Description |
75
+ | ------------------------- | --------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
76
+ | `mtxApiHost` | `mtx-api-host` | string | API server URL, e.g. `https://api.marketrix.ai`. The widget has no baked-in API host — you must supply it. |
77
+ | `container` | — | `HTMLElement` | Element to mount inside (programmatic only). Defaults to a container appended to `<body>`. |
78
+ | `userId` | — | number | Associates widget activity with one of your users. |
79
+ | `widget_position_z_index` | — | number | `z-index` floor for the launcher and panel. Raised to the widget's own layer token if you pass a lower value. |
80
+ | `show_widget` | — | boolean | When `false`, the widget initializes fully but its UI stays hidden. Default `true`. |
81
+ | `use_screenshare` | `mtx-use-screenshare` | boolean | When `false`, screen-share requests are auto-denied and the Share Screen button is hidden. Default `true`. Disable via `mtx-use-screenshare="false"`. |
82
+ | `styleNonce` | `mtx-style-nonce` | string | A CSP nonce applied to the widget's injected `<style>` element. Required only if your `style-src` policy has no `'unsafe-inline'` — see [Content Security Policy](#content-security-policy). |
82
83
 
83
84
  Widget **appearance and behavior** (position, colors, sizing, border radius, animation, enabled Tell/Show/Do features, visibility, greeting toast, optional session recording, header/body/greeting text, and quick-action chips) are configured in the Marketrix dashboard and fetched from the API at init. A hidden widget stays visible in the dashboard preview.
84
85
 
@@ -216,7 +217,7 @@ TypeScript types are bundled with the package:
216
217
 
217
218
  - `MarketrixConfig` — full config for `initWidget` / `updateMarketrixConfig` (`mtxId`, `mtxKey`, `mtxApiHost`, `userId`, `show_widget`, `use_screenshare`, plus all widget appearance settings, optional).
218
219
  - `AddWidgetConfig` — discriminated config for `mountWidget` (production / preview variants + common options).
219
- - `ClientOwnedConfig` — the host-supplied options the API never sends (`mtxApiHost`, `userId`, `widget_position_z_index`, `show_widget`, `use_screenshare`).
220
+ - `ClientOwnedConfig` — the host-supplied options the API never sends (`mtxApiHost`, `userId`, `widget_position_z_index`, `show_widget`, `use_screenshare`, `styleNonce`).
220
221
  - `MarketrixWidgetPreviewProps` — props for the `MarketrixWidgetPreview` component.
221
222
  - `ChatMessage`, `WidgetState`, `InstructionType` (`'tell' | 'show' | 'do'`).
222
223
 
@@ -230,6 +231,24 @@ TypeScript types are bundled with the package:
230
231
 
231
232
  ---
232
233
 
234
+ ## Content Security Policy
235
+
236
+ The widget mounts into a closed Shadow DOM and injects its own stylesheet as an inline `<style>`
237
+ element inside it (there is no external stylesheet to point a `<link>` at). A host page enforcing a
238
+ `style-src` policy with no `'unsafe-inline'` blocks that element, leaving the widget mounted but
239
+ entirely unstyled. If your policy is that strict, either:
240
+
241
+ - add `'unsafe-inline'` to `style-src` (simplest, and scoped to styles only), or
242
+ - generate a per-request nonce, add it to your `style-src` policy (`style-src 'nonce-<value>'`), and
243
+ pass the same value as `styleNonce` (programmatic) or `mtx-style-nonce` (script tag) — the widget
244
+ applies it to its injected `<style>` element.
245
+
246
+ The widget makes network requests only to the configured `mtxApiHost`, with credentials explicitly
247
+ omitted on every request (it authenticates via `mtxId`/`mtxKey`, never a cookie) — no `connect-src`
248
+ entry beyond your own API host is required, and no third-party origin is ever contacted.
249
+
250
+ ---
251
+
233
252
  ## License
234
253
 
235
254
  Apache License 2.0 — see the `LICENSE` file.
@@ -6,7 +6,11 @@
6
6
  * shell's toolbar button fills with a share toggle, and the composer textarea ref. `MODES` pairs the
7
7
  * mode chips' display order with the tenant setting enabling each, so the composer offers only what
8
8
  * the workspace turned on. `ChatView` owns the draft text; `handleSendMessage` posts the turn and
9
- * `handleModeChange` announces a mode switch in the transcript before flipping state.
9
+ * `handleModeChange` announces a mode switch in the transcript before flipping state. A POST failure
10
+ * restores the composed text to the (now-cleared) composer — `messageDispatch`'s resolved `false` —
11
+ * unless the visitor already started typing a new message in the meantime, so a resend is one tap on
12
+ * Send rather than a retype, and the failed turn's own bubble (added optimistically, unconditionally)
13
+ * still shows what was said either way.
10
14
  *
11
15
  * The composer is locked both while a reply is outstanding and while a screen-access request is open,
12
16
  * since a second turn queued behind an unanswered permission card has nowhere to land. `use_screenshare`
@@ -8,15 +8,30 @@
8
8
  * defers updaters and loses the effects captured in them, so tool calls never execute. `messageDispatch`
9
9
  * POSTs fire-and-forget: the reply arrives over SSE, so only a POST failure resolves the placeholder
10
10
  * locally, and a stale-reply watchdog keyed on id AND part count re-arms on every progress line.
11
+ * `messageDispatch` resolves `false` on that same POST failure (`true` otherwise, including preview
12
+ * mode) so a caller holding the composed text — `ChatView`'s composer — can restore it instead of the
13
+ * visitor having to retype a message the widget already dropped from the input box.
11
14
  *
12
15
  * The stream effect subscribes to the `StreamClient` singleton: `handleMessage` does the bookkeeping the
13
- * pure reducer cannot hold (`tool_call_id` dedupe in a bounded set, cleared on a terminal status), then
14
- * each effect runs its browser tool, stamps progress, replies `tool/response` and only then fires
15
- * `afterResponseAttempt`. `handleError` converts only a `StreamGaveUpError` into a transport failure, as a
16
- * retriable blip settles when the reply lands on the reconnected stream. `stopTask` sends `chat/stop`,
17
- * which carries no task id. Every failure on the tool and stop paths reaches `uiActions.setError` as well
18
- * as the console: an undelivered `tool/response` leaves the agent waiting on a reply that never comes, so
19
- * the run stalls with nothing on screen unless the visitor is told, and `do` mode may still be clicking.
16
+ * pure reducer cannot hold (`tool_call_id` dedupe in a bounded set, cleared on a terminal status; the
17
+ * mirror-shaped `respondedRequestIds` below), then each effect runs its browser tool, stamps progress,
18
+ * replies `tool/response` and only then fires `afterResponseAttempt`. `handleError` converts only a
19
+ * `StreamGaveUpError` into a transport failure, as a retriable blip settles when the reply lands on the
20
+ * reconnected stream. `stopTask` sends `chat/stop`, which carries no task id. Every failure on the tool
21
+ * and stop paths reaches `uiActions.setError` as well as the console: an undelivered `tool/response`
22
+ * leaves the agent waiting on a reply that never comes, so the run stalls with nothing on screen unless
23
+ * the visitor is told, and `do` mode may still be clicking.
24
+ *
25
+ * `respondedRequestIds` closes the other half of the reconnect-replay contract `StreamClient.ts`
26
+ * documents (the api replays a chat_id's whole turn history, not just the tail a client missed):
27
+ * `reduceText`'s own exact-repeat guard (`sseReducer.ts`) only catches an identical FINAL
28
+ * `chat/response` replayed after the answer already closed, because it only ever compares against the
29
+ * message's LAST part. A replayed `chat/delta` fails that same-string check (it's a partial fragment,
30
+ * not the full closed text) and would otherwise be appended as a brand-new, visibly duplicated text
31
+ * segment ahead of the closing response that then only overwrites the one it just added — leaving two
32
+ * copies of the same answer on screen. Scoped by `request_id`, not globally, so an unrelated later
33
+ * turn's genuinely new `chat/delta`/`chat/response` is untouched; marked closed only once, on the
34
+ * request's own terminal `chat/response`, since a delta is by definition still open.
20
35
  *
21
36
  * `lastStreamErrorRef`/`currentErrorRef` close the loop `handleError` alone leaves open: `StreamClient`
22
37
  * calls `onError` on every failed dial, not only a terminal give-up, so the visitor sees "Stream
@@ -43,7 +58,7 @@ interface ChatActions {
43
58
  removeMessage: (messageId: string) => void;
44
59
  setMessages: (messages: ChatMessage[]) => void;
45
60
  clearMessages: () => void;
46
- messageDispatch: (content: string, mode?: InstructionType, skipUserMessage?: boolean) => Promise<void>;
61
+ messageDispatch: (content: string, mode?: InstructionType, skipUserMessage?: boolean) => Promise<boolean>;
47
62
  }
48
63
  interface TaskActions {
49
64
  resetTask: () => void;
@@ -21,7 +21,7 @@ export declare const useWidget: () => {
21
21
  removeMessage: (messageId: string) => void;
22
22
  setMessages: (messages: import("..").ChatMessage[]) => void;
23
23
  clearMessages: () => void;
24
- messageDispatch: (content: string, mode?: import("..").InstructionType, skipUserMessage?: boolean) => Promise<void>;
24
+ messageDispatch: (content: string, mode?: import("..").InstructionType, skipUserMessage?: boolean) => Promise<boolean>;
25
25
  resetTask: () => void;
26
26
  stopTask: () => Promise<void>;
27
27
  setActiveView: (view: import("../types").WidgetView) => void;
@@ -135,6 +135,12 @@ export declare const WorkspaceSummarySchema: z.ZodObject<{
135
135
  notify_all_members_on_question: z.ZodBoolean;
136
136
  }, z.core.$strip>;
137
137
  export type WorkspaceSummary = z.infer<typeof WorkspaceSummarySchema>;
138
+ export declare const ApplicationSkillDistillationStatusSchema: z.ZodEnum<{
139
+ failed: "failed";
140
+ idle: "idle";
141
+ pending: "pending";
142
+ }>;
143
+ export type ApplicationSkillDistillationStatus = z.infer<typeof ApplicationSkillDistillationStatusSchema>;
138
144
  export declare const ApplicationEntitySchema: z.ZodObject<{
139
145
  id: z.ZodNumber;
140
146
  created_at: z.ZodCoercedDate<unknown>;
@@ -150,6 +156,12 @@ export declare const ApplicationEntitySchema: z.ZodObject<{
150
156
  username: z.ZodNullable<z.ZodString>;
151
157
  password: z.ZodNullable<z.ZodString>;
152
158
  allowed_domains: z.ZodArray<z.ZodString>;
159
+ skill_distillation_status: z.ZodEnum<{
160
+ failed: "failed";
161
+ idle: "idle";
162
+ pending: "pending";
163
+ }>;
164
+ skill_distillation_error: z.ZodNullable<z.ZodString>;
153
165
  }, z.core.$strip>;
154
166
  export type ApplicationData = z.infer<typeof ApplicationEntitySchema>;
155
167
  /** Used for all API responses; password is write-only and never returned to clients. */
@@ -167,6 +179,12 @@ export declare const ApplicationReadSchema: z.ZodObject<{
167
179
  workspace_id: z.ZodNumber;
168
180
  username: z.ZodNullable<z.ZodString>;
169
181
  allowed_domains: z.ZodArray<z.ZodString>;
182
+ skill_distillation_status: z.ZodEnum<{
183
+ failed: "failed";
184
+ idle: "idle";
185
+ pending: "pending";
186
+ }>;
187
+ skill_distillation_error: z.ZodNullable<z.ZodString>;
170
188
  }, z.core.$strip>;
171
189
  export type ApplicationReadData = z.infer<typeof ApplicationReadSchema>;
172
190
  export declare const WidgetChipSchema: z.ZodObject<{
@@ -5,7 +5,12 @@
5
5
  * browser-local key shares via `scopedKey`, so the chat-context, drag-position and resize keys all
6
6
  * partition by tenant identically. `readLocal`/`writeLocal` are the only `localStorage` access in `src/`;
7
7
  * a host page can deny storage outright (third-party cookies off, sandboxed iframe), so both degrade to
8
- * a warn and the widget keeps working unpersisted.
8
+ * memory and the widget keeps working unpersisted. `updateContext` calls `writeLocal` on every UI-state
9
+ * change (a drag, a resize, every chat message), so without `warnOnce` a denied host page would spam one
10
+ * console line per write for the visitor's whole session; a single warn on first denial says everything
11
+ * a customer's console needs. `warnOnce` is keyed by read vs. write since `readLocal` also fires once on
12
+ * its own (`loadContext` at module init, before any write) and both should still surface if a page
13
+ * somehow denies one and not the other.
9
14
  *
10
15
  * `loadContext` merges one parsed key over `DEFAULT_CONTEXT`, so an older widget version's payload reads
11
16
  * as incomplete rather than corrupt, and discards anything past `CONTEXT_EXPIRY_MS` (7 days).
@@ -14,8 +19,13 @@
14
19
  * from it, since without that two applications on one origin would leak one tenant's transcript into
15
20
  * another's.
16
21
  *
17
- * The chat snapshot is `{messages, currentMode, isOpen}` — chat_id, config and timestamp are deliberately
18
- * excluded. `StoredMessage` is the ONE place `content` still exists as a field: a live `ChatMessage` has
22
+ * `writeChatSnapshot`'s own parameter is `{messages, currentMode, isOpen}` — chat_id, config and
23
+ * timestamp are deliberately excluded from THAT function's input, though `config` (via `setConfig`)
24
+ * and `chat_id`/`timestamp` (via `updateContext`) are still part of the one persisted record on disk;
25
+ * `config` there is the tenant's own `mtxId`/`mtxKey`/`mtxApiHost`/`userId`/rendered settings — no
26
+ * broader PII, and `mtxId`/`mtxKey` are already fully public in the host page's own script-tag markup,
27
+ * not a secret this adds exposure to. `StoredMessage` is the ONE place `content` still exists as a
28
+ * field: a live `ChatMessage` has
19
29
  * none (every reader derives `messageText(parts)` instead), but a transcript written before `parts`
20
30
  * existed has only `content` on disk, so `readChatSnapshot` backfills a text part from it and
21
31
  * `writeChatSnapshot` derives `content` back from `parts` on the way out, keeping the persisted shape
@@ -50,6 +60,10 @@ type MarketrixChatContext = Omit<ChatSnapshot, 'messages'> & {
50
60
  };
51
61
  export declare function tenantScope(config: MarketrixConfig): string;
52
62
  export declare function scopedKey(name: string, config: MarketrixConfig): string;
63
+ /** Test-only: `warned` is module-level so a "denies storage, keeps working" proof isn't the last test in
64
+ * the file to touch a denied `Storage.prototype`, and a later test in the same `bun test` process (one
65
+ * process per file, not per test) would otherwise see zero warns instead of one. Never called from `src/`. */
66
+ export declare function resetStorageWarningsForTests(): void;
53
67
  export declare function readLocal(key: string): string | null;
54
68
  export declare function writeLocal(key: string, value: string): void;
55
69
  declare class StorageService {
@@ -6,7 +6,10 @@
6
6
  *
7
7
  * `open` is the transport, `registered` is the chat: only the latter can carry a reply, so `isConnected` reads
8
8
  * `registered` and nothing waits on `open`. Backoff counters reset only on `registered` — resetting at `open` would
9
- * defeat the max-attempts cap if registration never lands and the stream flaps open→closed. Tabs share the
9
+ * defeat the max-attempts cap if registration never lands and the stream flaps open→closed. `scheduleReconnect`'s
10
+ * EQUAL JITTER (the doubling delay's own second half, chosen uniformly) keeps every dial within the documented
11
+ * schedule's bound while stopping every tab across every open customer page from redialing on the exact same
12
+ * clock tick after a shared outage — a thundering herd the deterministic schedule alone cannot prevent. Tabs share the
10
13
  * localStorage chat id, so the server keys SSE by (chat_id, tab_id) and `tabId` stops tabs evicting each other's
11
14
  * stream. Credentials are read at connect time, not captured at init, so a reconnect after `updateMarketrixConfig`
12
15
  * dials with the current ones.
@@ -4,7 +4,9 @@
4
4
  *
5
5
  * `ClientOwnedConfig` holds host-supplied knobs the api never sends (`show_widget: false` still
6
6
  * initializes the widget fully and only hides its UI; `use_screenshare: false` auto-denies
7
- * screen-access requests and hides the Share Screen button; both default true). `MarketrixConfig` is
7
+ * screen-access requests and hides the Share Screen button; both default true; `styleNonce` reaches
8
+ * the injected shadow-root `<style>` element for a host running a strict `style-src` CSP with no
9
+ * `'unsafe-inline'`). `MarketrixConfig` is
8
10
  * deliberately FLAT so api settings spread straight in — `mtxId`+`mtxKey` is the credential, while
9
11
  * `mtxApp` is stamped internally after validation and never an input, since an application id is
10
12
  * guessable and authenticates nothing. `ValidWidgetConfig` is a `MarketrixConfig` run through
@@ -29,6 +31,7 @@ export interface ClientOwnedConfig {
29
31
  widget_position_z_index?: number;
30
32
  show_widget?: boolean;
31
33
  use_screenshare?: boolean;
34
+ styleNonce?: string;
32
35
  }
33
36
  export type MarketrixConfig = Partial<WidgetRenderedSettings> & ClientOwnedConfig & {
34
37
  mtxId?: string;
@@ -11,7 +11,7 @@ interface WidgetMount {
11
11
  export declare const widgetState: {
12
12
  mount: WidgetMount | null;
13
13
  };
14
- export declare const createWidgetContainer: (parentContainer?: HTMLElement) => {
14
+ export declare const createWidgetContainer: (parentContainer?: HTMLElement, styleNonce?: string) => {
15
15
  container: HTMLElement;
16
16
  shadowRoot: ShadowRoot;
17
17
  mountEl: HTMLElement;