@eshal-bot/chat-widget 0.1.62 → 0.1.63

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.
@@ -8011,14 +8011,18 @@
8011
8011
  * @param {object} request - Chat request object
8012
8012
  * @param {string} apiBaseUrl - Base URL for the API
8013
8013
  */
8014
- const sendStreamingRequest = async (request, apiBaseUrl, organizationId) => {
8014
+ const sendStreamingRequest = async (request, apiBaseUrl, organizationId, secretKey) => {
8015
8015
  const headers = {
8016
8016
  'Content-Type': 'application/json',
8017
8017
  Accept: 'text/event-stream'
8018
8018
  };
8019
8019
 
8020
- // Add organization ID header if provided
8021
- if (organizationId) {
8020
+ // Mode A — secret key (direct embed): key replaces orgId in the header
8021
+ // Mode B — proxy (@eshal-bot/server): no header from widget; proxy injects x-eshal-sig + x-eshal-org
8022
+ // Mode C — legacy: orgId sent directly
8023
+ if (secretKey) {
8024
+ headers['x-eshal-key'] = secretKey;
8025
+ } else if (organizationId) {
8022
8026
  headers['x-eshal-org'] = organizationId;
8023
8027
  }
8024
8028
  const response = await fetch("".concat(apiBaseUrl.replace(/\/$/, ''), "/api/v1/support"), {
@@ -9129,6 +9133,7 @@
9129
9133
  apiBaseUrl,
9130
9134
  apiKey,
9131
9135
  organizationId,
9136
+ secretKey,
9132
9137
  autoOpen,
9133
9138
  openDelay,
9134
9139
  darkMode,
@@ -9765,7 +9770,7 @@
9765
9770
  if (apiType === 'userName' || apiType === 'email' || apiType === 'phone') {
9766
9771
  // For onboarding fields, just consume the stream without processing.
9767
9772
  // Single attempt — no retry, no user-visible partial state to clean up.
9768
- const stream = await sendStreamingRequest(chatRequest, apiBaseUrl, organizationId);
9773
+ const stream = await sendStreamingRequest(chatRequest, apiBaseUrl, organizationId, secretKey);
9769
9774
  const reader = stream.getReader();
9770
9775
  const decoder = new TextDecoder();
9771
9776
  while (true) {
@@ -9784,7 +9789,7 @@
9784
9789
  let lastErr;
9785
9790
  for (let attempt = 0; attempt <= STREAM_RETRY_DELAYS_MS.length; attempt += 1) {
9786
9791
  try {
9787
- const stream = await sendStreamingRequest(chatRequest, apiBaseUrl, organizationId);
9792
+ const stream = await sendStreamingRequest(chatRequest, apiBaseUrl, organizationId, secretKey);
9788
9793
  await processStream(stream, setMessages);
9789
9794
  updateActivity(organizationId);
9790
9795
  return {
@@ -91865,6 +91870,9 @@
91865
91870
  fontSize: 11,
91866
91871
  fill: labelColor
91867
91872
  },
91873
+ angle: 0,
91874
+ textAnchor: "middle",
91875
+ interval: 0,
91868
91876
  label: props.xAxisLabel ? {
91869
91877
  value: props.xAxisLabel,
91870
91878
  position: 'insideBottom',
@@ -92143,6 +92151,9 @@
92143
92151
  fontSize: 11,
92144
92152
  fill: labelColor
92145
92153
  },
92154
+ angle: 0,
92155
+ textAnchor: "middle",
92156
+ interval: 0,
92146
92157
  label: props.xAxisLabel ? {
92147
92158
  value: props.xAxisLabel,
92148
92159
  position: 'insideBottom',
@@ -92713,10 +92724,12 @@
92713
92724
 
92714
92725
  // ISO date / datetime prefix — matches the typical SQL output the
92715
92726
  // agent's tools produce when GROUP BY-ing by month
92716
- // (`DATE_TRUNC('month', col)` → `YYYY-MM-DD`). Used by formatAxisTick
92717
- // below so a chart of monthly expenses shows "Jan '26" / "Feb '26"
92718
- // horizontally instead of "2026 01 01" slanted (EV-1187 follow-up).
92719
- const ISO_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})(?:T|$)/;
92727
+ // (`DATE_TRUNC('month', col)` → `YYYY-MM-DD`, or `to_char(..,'YYYY-MM')`
92728
+ // → `YYYY-MM`). Used by formatAxisTick below so a chart of monthly
92729
+ // expenses shows "Jan '26" / "Feb '26" horizontally instead of
92730
+ // "2026 01 01" or "2026 01" slanted (EV-1187 follow-up + 2026-06-24
92731
+ // day-less variant fix).
92732
+ const ISO_DATE_RE = /^(\d{4})-(\d{2})(?:-\d{2})?(?:[T\s]|$)/;
92720
92733
  function formatIsoDateAsMonthTick(value) {
92721
92734
  if (typeof value !== 'string') return null;
92722
92735
  const m = value.match(ISO_DATE_RE);
@@ -96219,7 +96232,18 @@
96219
96232
  copyChat: "Copy Chat",
96220
96233
  startChat: "Start Chat",
96221
96234
  startingVoiceSession: "Starting voice session...",
96222
- allowMicAccess: "Allow microphone access"
96235
+ allowMicAccess: "Allow microphone access",
96236
+ // Onboarding-form validation messages. These are the English fallback for
96237
+ // every locale (translate() falls back to EN[key]); without them the form
96238
+ // rendered the raw key strings ("emailInvalid", etc.). `fieldRequiredSuffix`
96239
+ // is appended after a field label: `${label} ${fieldRequiredSuffix}`.
96240
+ emailRequired: "Please enter your email address",
96241
+ nameRequired: "Please enter your name",
96242
+ phoneRequired: "Please enter your phone number",
96243
+ fieldRequiredSuffix: "is required",
96244
+ emailInvalid: "Please enter a valid email address",
96245
+ nameInvalid: "Please enter a valid name",
96246
+ phoneInvalid: "Please enter a valid phone number"
96223
96247
  };
96224
96248
  const DICTIONARIES = {
96225
96249
  en: EN,
@@ -99609,6 +99633,7 @@
99609
99633
  apiBaseUrl,
99610
99634
  apiKey,
99611
99635
  organizationId,
99636
+ secretKey,
99612
99637
  conversationId,
99613
99638
  conciergeId,
99614
99639
  onboardingQuestions = [],
@@ -100405,6 +100430,7 @@
100405
100430
  /**
100406
100431
  * @param {string|undefined|null} apiBaseUrl
100407
100432
  * @param {string|undefined|null} orgId
100433
+ * @param {string|undefined|null} secretKey — Mode A/B: orgId resolved server-side
100408
100434
  * @returns {{
100409
100435
  * categories: Array<{id:string,label:string,count:number,tags:string[]}>,
100410
100436
  * loading: boolean,
@@ -100412,14 +100438,17 @@
100412
100438
  * hasFilters: boolean,
100413
100439
  * }}
100414
100440
  */
100415
- function useFilterCatalog(apiBaseUrl, orgId) {
100441
+ function useFilterCatalog(apiBaseUrl, orgId, secretKey) {
100442
+ // Mode A: secretKey present, orgId null → key-based endpoint
100443
+ // Mode C: orgId present (legacy) → orgId-in-path endpoint
100444
+ const hasAuth = !!(apiBaseUrl && (orgId || secretKey));
100416
100445
  const [state, setState] = reactExports.useState({
100417
100446
  categories: [],
100418
- loading: !!(apiBaseUrl && orgId),
100447
+ loading: hasAuth,
100419
100448
  error: null
100420
100449
  });
100421
100450
  reactExports.useEffect(() => {
100422
- if (!apiBaseUrl || !orgId) {
100451
+ if (!apiBaseUrl || !orgId && !secretKey) {
100423
100452
  setState({
100424
100453
  categories: [],
100425
100454
  loading: false,
@@ -100427,8 +100456,8 @@
100427
100456
  });
100428
100457
  return;
100429
100458
  }
100430
- const key = "".concat(apiBaseUrl, "|").concat(orgId);
100431
- const cached = _PROCESS_CACHE.get(key);
100459
+ const cacheKey = secretKey ? "".concat(apiBaseUrl, "|__key__") : "".concat(apiBaseUrl, "|").concat(orgId);
100460
+ const cached = _PROCESS_CACHE.get(cacheKey);
100432
100461
  if (cached) {
100433
100462
  setState({
100434
100463
  categories: cached,
@@ -100442,12 +100471,15 @@
100442
100471
  loading: true,
100443
100472
  error: null
100444
100473
  }));
100445
- const url = "".concat(apiBaseUrl.replace(/\/$/, ""), "/api/v1/widget/filters/").concat(encodeURIComponent(orgId));
100474
+ const base = apiBaseUrl.replace(/\/$/, "");
100475
+ const url = secretKey ? "".concat(base, "/api/v1/widget/filters") : "".concat(base, "/api/v1/widget/filters/").concat(encodeURIComponent(orgId));
100476
+ const headers = {
100477
+ Accept: "application/json"
100478
+ };
100479
+ if (secretKey) headers["x-eshal-key"] = secretKey;
100446
100480
  fetch(url, {
100447
100481
  method: "GET",
100448
- headers: {
100449
- Accept: "application/json"
100450
- }
100482
+ headers
100451
100483
  }).then(async res => {
100452
100484
  if (!res.ok) {
100453
100485
  throw new Error("filter catalog HTTP ".concat(res.status));
@@ -100456,7 +100488,7 @@
100456
100488
  return Array.isArray(body === null || body === void 0 ? void 0 : body.categories) ? body.categories : [];
100457
100489
  }).then(categories => {
100458
100490
  if (cancelled) return;
100459
- _PROCESS_CACHE.set(key, categories);
100491
+ _PROCESS_CACHE.set(cacheKey, categories);
100460
100492
  setState({
100461
100493
  categories,
100462
100494
  loading: false,
@@ -100478,7 +100510,7 @@
100478
100510
  return () => {
100479
100511
  cancelled = true;
100480
100512
  };
100481
- }, [apiBaseUrl, orgId]);
100513
+ }, [apiBaseUrl, orgId, secretKey]);
100482
100514
  const hasFilters = reactExports.useMemo(() => Array.isArray(state.categories) && state.categories.length > 0, [state.categories]);
100483
100515
  return _objectSpread2(_objectSpread2({}, state), {}, {
100484
100516
  hasFilters
@@ -100560,17 +100592,25 @@
100560
100592
  * @param {string} orgId - Organization ID
100561
100593
  * @returns {Promise<object>} Agent configuration object
100562
100594
  */
100563
- const fetchAgentConfig = async (apiBaseUrl, orgId) => {
100564
- if (!apiBaseUrl || !orgId) {
100565
- throw new Error("apiBaseUrl and orgId are required");
100595
+ const fetchAgentConfig = async (apiBaseUrl, orgId, secretKey) => {
100596
+ if (!apiBaseUrl) {
100597
+ throw new Error("apiBaseUrl is required");
100598
+ }
100599
+ if (!secretKey && !orgId) {
100600
+ throw new Error("secretKey or orgId is required");
100566
100601
  }
100567
100602
  try {
100568
- const url = "".concat(apiBaseUrl.replace(/\/$/, ''), "/api/v1/deploy-agent/").concat(orgId);
100603
+ const base = apiBaseUrl.replace(/\/$/, '');
100604
+ // Mode A/B: no orgId in URL — backend resolves org from x-eshal-key or HMAC sig
100605
+ // Mode C legacy: orgId in URL path, no header needed
100606
+ const url = secretKey ? "".concat(base, "/api/v1/deploy-agent") : "".concat(base, "/api/v1/deploy-agent/").concat(orgId);
100607
+ const headers = {
100608
+ "Content-Type": "application/json"
100609
+ };
100610
+ if (secretKey) headers['x-eshal-key'] = secretKey;
100569
100611
  const response = await fetch(url, {
100570
100612
  method: "GET",
100571
- headers: {
100572
- "Content-Type": "application/json"
100573
- },
100613
+ headers,
100574
100614
  credentials: "include"
100575
100615
  });
100576
100616
  if (!response.ok) {
@@ -100587,8 +100627,11 @@
100587
100627
  const ChatWidget = _ref => {
100588
100628
  var _agentConfig$concierg;
100589
100629
  let {
100590
- // Required props for dynamic config
100630
+ // Auth — at least one of secretKey (Mode A/B) or orgId (Mode C legacy) is needed.
100631
+ secretKey,
100632
+ // sk_live_... from ChatWidget.init({ secretKey, apiBaseUrl })
100591
100633
  orgId,
100634
+ // legacy — kept for backward compatibility
100592
100635
  apiBaseUrl,
100593
100636
  // Optional override props (will override fetched config if provided)
100594
100637
  darkMode,
@@ -100639,14 +100682,21 @@
100639
100682
  // Fetch agent configuration on mount
100640
100683
  reactExports.useEffect(() => {
100641
100684
  const loadAgentConfig = async () => {
100642
- if (!orgId || !apiBaseUrl) {
100643
- setConfigError("orgId and apiBaseUrl are required");
100685
+ if (!apiBaseUrl) {
100686
+ setConfigError("apiBaseUrl is required");
100687
+ setIsLoadingConfig(false);
100688
+ return;
100689
+ }
100690
+ if (!secretKey && !orgId) {
100691
+ setConfigError("secretKey or orgId is required");
100644
100692
  setIsLoadingConfig(false);
100645
100693
  return;
100646
100694
  }
100647
100695
  try {
100648
100696
  setIsLoadingConfig(true);
100649
- const config = await fetchAgentConfig(apiBaseUrl, orgId);
100697
+ // Pass secretKey for Mode A/B — fetchAgentConfig will send it as a
100698
+ // header and omit the orgId path segment. Pass orgId for Mode C legacy.
100699
+ const config = await fetchAgentConfig(apiBaseUrl, orgId, secretKey);
100650
100700
  setAgentConfig(config);
100651
100701
  setConfigError(null);
100652
100702
  } catch (error) {
@@ -100657,7 +100707,7 @@
100657
100707
  }
100658
100708
  };
100659
100709
  loadAgentConfig();
100660
- }, [orgId, apiBaseUrl]);
100710
+ }, [orgId, secretKey, apiBaseUrl]);
100661
100711
 
100662
100712
  // Map API response to widget props (use props as overrides if provided)
100663
100713
  const widgetConfig = reactExports.useMemo(() => {
@@ -100835,6 +100885,7 @@
100835
100885
  // knowledge-api LoadBalancer.
100836
100886
  apiKey,
100837
100887
  organizationId: widgetConfig.organizationId,
100888
+ secretKey,
100838
100889
  autoOpen,
100839
100890
  openDelay,
100840
100891
  darkMode: widgetConfig.darkMode,
@@ -100861,14 +100912,15 @@
100861
100912
  // navigation (but not a page refresh — feedback is not written to
100862
100913
  // localStorage so it resets on reload as intended).
100863
100914
  updateMessageFeedback(messageContent, feedback);
100864
- if (!orgId || !conversationId || !apiBaseUrl) return;
100915
+ if (!orgId && !secretKey || !conversationId || !apiBaseUrl) return;
100865
100916
  const base = apiBaseUrl.replace(/\/$/, "");
100917
+ const feedbackHeaders = {
100918
+ "Content-Type": "application/json"
100919
+ };
100920
+ if (secretKey) feedbackHeaders['x-eshal-key'] = secretKey;else if (orgId) feedbackHeaders['x-eshal-org'] = orgId;
100866
100921
  fetch("".concat(base, "/api/v1/support/feedback"), {
100867
100922
  method: "POST",
100868
- headers: {
100869
- "Content-Type": "application/json",
100870
- "x-eshal-org": orgId
100871
- },
100923
+ headers: feedbackHeaders,
100872
100924
  credentials: "include",
100873
100925
  body: JSON.stringify({
100874
100926
  messageContent,
@@ -100894,7 +100946,7 @@
100894
100946
  const {
100895
100947
  categories: filterCategories,
100896
100948
  hasFilters: showFilterStep
100897
- } = useFilterCatalog(apiBaseUrl, orgId);
100949
+ } = useFilterCatalog(apiBaseUrl, orgId, secretKey);
100898
100950
 
100899
100951
  // Copy chat handler — copy all messages to clipboard
100900
100952
  const handleCopyChat = reactExports.useCallback(() => {
@@ -101000,6 +101052,7 @@
101000
101052
  apiBaseUrl: apiBaseUrl,
101001
101053
  apiKey: apiKey,
101002
101054
  organizationId: widgetConfig.organizationId,
101055
+ secretKey: secretKey,
101003
101056
  conversationId: conversationId,
101004
101057
  conciergeId: (_agentConfig$concierg = agentConfig === null || agentConfig === void 0 ? void 0 : agentConfig.conciergeId) !== null && _agentConfig$concierg !== void 0 ? _agentConfig$concierg : agentConfig === null || agentConfig === void 0 ? void 0 : agentConfig.id,
101005
101058
  onboardingQuestions: widgetConfig.onboardingQuestions,
@@ -101147,11 +101200,14 @@
101147
101200
  }
101148
101201
 
101149
101202
  // Render component
101150
- // If orgId and apiBaseUrl are provided, use dynamic config
101151
- // Otherwise, use the old static config approach for backward compatibility
101152
- const widgetProps = config.orgId && config.apiBaseUrl ? _objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2({
101153
- // Required props
101154
- orgId: config.orgId,
101203
+ // Mode A/B: secretKey present — orgId derived server-side from the key
101204
+ // Mode C legacy: orgId in config — backward compatible
101205
+ // Both: apiBaseUrl required
101206
+ const hasNewAuth = (config.secretKey || config.orgId) && config.apiBaseUrl;
101207
+ const widgetProps = hasNewAuth ? _objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2({
101208
+ // Auth props — secretKey replaces orgId in Mode A/B
101209
+ secretKey: config.secretKey || null,
101210
+ orgId: config.orgId || null,
101155
101211
  apiBaseUrl: config.apiBaseUrl
101156
101212
  }, options.wsBaseUrl !== undefined && {
101157
101213
  wsBaseUrl: config.wsBaseUrl