@hexabot-ai/api 3.2.2-beta.2 → 3.2.3
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/dist/channel/channel.controller.js +1 -0
- package/dist/channel/channel.controller.js.map +1 -1
- package/dist/channel/lib/Handler.d.ts +4 -2
- package/dist/channel/lib/Handler.js +6 -2
- package/dist/channel/lib/Handler.js.map +1 -1
- package/dist/channel/services/source.service.d.ts +1 -0
- package/dist/channel/services/source.service.js +16 -6
- package/dist/channel/services/source.service.js.map +1 -1
- package/dist/config/index.js +11 -55
- package/dist/config/index.js.map +1 -1
- package/dist/extensions/actions/ai/ai-base.action.js +11 -1
- package/dist/extensions/actions/ai/ai-base.action.js.map +1 -1
- package/dist/extensions/actions/ai/model.binding.d.ts +3 -1
- package/dist/extensions/actions/ai/model.binding.js +2 -1
- package/dist/extensions/actions/ai/model.binding.js.map +1 -1
- package/dist/extensions/channels/console/i18n/en.translations.json +1 -6
- package/dist/extensions/channels/console/i18n/fr.translations.json +1 -6
- package/dist/extensions/channels/console/index.channel.js +1 -1
- package/dist/extensions/channels/console/index.channel.js.map +1 -1
- package/dist/extensions/channels/console/settings.schema.d.ts +0 -2
- package/dist/extensions/channels/console/settings.schema.js +0 -12
- package/dist/extensions/channels/console/settings.schema.js.map +1 -1
- package/dist/extensions/channels/web/base-web-channel.d.ts +2 -2
- package/dist/extensions/channels/web/base-web-channel.js +5 -7
- package/dist/extensions/channels/web/base-web-channel.js.map +1 -1
- package/dist/extensions/channels/web/services/web-session.service.js +8 -20
- package/dist/extensions/channels/web/services/web-session.service.js.map +1 -1
- package/dist/extensions/channels/web/settings.schema.d.ts +1 -1
- package/dist/extensions/channels/web/settings.schema.js +6 -1
- package/dist/extensions/channels/web/settings.schema.js.map +1 -1
- package/dist/setting/services/setting.service.js +4 -3
- package/dist/setting/services/setting.service.js.map +1 -1
- package/dist/static/assets/{browser-ponyfill-CSTKMWgS.js → browser-ponyfill-CnklMg-0.js} +1 -1
- package/dist/static/assets/{cssMode-DgelXMs5.js → cssMode-BFc1s9d3.js} +1 -1
- package/dist/static/assets/{freemarker2-B_8oADSg.js → freemarker2-QJMqt1-L.js} +1 -1
- package/dist/static/assets/{handlebars-Be0FSTSQ.js → handlebars-D3-tp3q8.js} +1 -1
- package/dist/static/assets/{html-D2fJyqE_.js → html-BIVSAe2M.js} +1 -1
- package/dist/static/assets/{htmlMode-Dwp5dG1B.js → htmlMode-D9c0cJUu.js} +1 -1
- package/dist/static/assets/{index-D-b9KTZd.css → index-BbLr1ieK.css} +1 -1
- package/dist/static/assets/{index-BZA3azmT.js → index-Wtgr3A4p.js} +2129 -2129
- package/dist/static/assets/{javascript-BLgfe7o3.js → javascript-u87PO7ih.js} +1 -1
- package/dist/static/assets/{jsonMode-UDjgnreB.js → jsonMode-0Pf5oAfj.js} +1 -1
- package/dist/static/assets/{liquid-Ac_LaddK.js → liquid-CBQoZjUj.js} +1 -1
- package/dist/static/assets/{lspLanguageFeatures-CaS0BZM-.js → lspLanguageFeatures-Bnt5qp-g.js} +1 -1
- package/dist/static/assets/{mdx-CBtS0OUk.js → mdx-BOZSKWcy.js} +1 -1
- package/dist/static/assets/{python-DrY2rXrO.js → python-CLk42i5f.js} +1 -1
- package/dist/static/assets/{razor-CF9RDHrz.js → razor-DDV6klIi.js} +1 -1
- package/dist/static/assets/{tsMode-Cnimk6sL.js → tsMode-Dgojz_8C.js} +1 -1
- package/dist/static/assets/{typescript-CrFzjBjn.js → typescript-BAUzcnL_.js} +1 -1
- package/dist/static/assets/{xml-DId6dVHR.js → xml-D5KxV0OO.js} +1 -1
- package/dist/static/assets/{yaml-DgRo3t-f.js → yaml-CSq3AYM9.js} +1 -1
- package/dist/static/index.html +2 -2
- package/dist/static/locales/en/translation.json +4 -0
- package/dist/static/locales/fr/translation.json +4 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/utils/helpers/origin.d.ts +4 -0
- package/dist/utils/helpers/origin.js +37 -0
- package/dist/utils/helpers/origin.js.map +1 -0
- package/dist/utils/helpers/parse.d.ts +4 -0
- package/dist/utils/helpers/parse.js +49 -0
- package/dist/utils/helpers/parse.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/websocket/tokens.d.ts +1 -0
- package/dist/websocket/tokens.js +5 -0
- package/dist/websocket/tokens.js.map +1 -0
- package/dist/websocket/utils/gateway-options.d.ts +10 -1
- package/dist/websocket/utils/gateway-options.js +91 -38
- package/dist/websocket/utils/gateway-options.js.map +1 -1
- package/dist/websocket/websocket.module.js +7 -2
- package/dist/websocket/websocket.module.js.map +1 -1
- package/dist/workflow/services/workflow.service.d.ts +6 -2
- package/dist/workflow/services/workflow.service.js +16 -11
- package/dist/workflow/services/workflow.service.js.map +1 -1
- package/package.json +6 -4
- package/src/channel/README.md +2 -1
- package/src/channel/channel.controller.ts +1 -0
- package/src/channel/lib/Handler.ts +6 -0
- package/src/channel/services/source.service.ts +23 -6
- package/src/config/index.ts +11 -59
- package/src/extensions/actions/ai/ai-base.action.ts +15 -2
- package/src/extensions/actions/ai/model.binding.ts +2 -1
- package/src/extensions/channels/console/i18n/en.translations.json +1 -6
- package/src/extensions/channels/console/i18n/fr.translations.json +1 -6
- package/src/extensions/channels/console/index.channel.ts +5 -1
- package/src/extensions/channels/console/settings.schema.ts +0 -12
- package/src/extensions/channels/web/base-web-channel.ts +13 -8
- package/src/extensions/channels/web/services/web-session.service.ts +10 -21
- package/src/extensions/channels/web/settings.schema.ts +10 -4
- package/src/setting/services/setting.service.ts +4 -3
- package/src/utils/helpers/origin.ts +50 -0
- package/src/utils/helpers/parse.ts +68 -0
- package/src/utils/index.ts +2 -0
- package/src/websocket/tokens.ts +7 -0
- package/src/websocket/utils/gateway-options.ts +131 -41
- package/src/websocket/websocket.module.ts +7 -2
- package/src/workflow/services/workflow.service.ts +16 -8
|
@@ -15,6 +15,7 @@ import { FindManyOptions, FindOneOptions, In, Not } from 'typeorm';
|
|
|
15
15
|
import { DeleteResult } from 'typeorm/driver/mongodb/typings';
|
|
16
16
|
import z from 'zod';
|
|
17
17
|
|
|
18
|
+
import { CONSOLE_CHANNEL_NAME } from '@/extensions/channels/console/settings.schema';
|
|
18
19
|
import { UpdateOneOptions } from '@/utils/generics/base-orm.repository';
|
|
19
20
|
import { BaseOrmService } from '@/utils/generics/base-orm.service';
|
|
20
21
|
import { WorkflowService } from '@/workflow/services/workflow.service';
|
|
@@ -113,9 +114,17 @@ export class SourceService extends BaseOrmService<SourceOrmEntity> {
|
|
|
113
114
|
return keys.length === 1 && keys[0] === 'state' && payload.state === false;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
private ensureSourceCanBeDisabled(channel: string, state?: boolean): void {
|
|
118
|
+
if (channel === CONSOLE_CHANNEL_NAME && state === false) {
|
|
119
|
+
throw new BadRequestException('Console source cannot be disabled');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
private async buildCreatePayload(
|
|
117
124
|
payload: SourceCreateDto,
|
|
118
125
|
): Promise<SourceCreateDto> {
|
|
126
|
+
this.ensureSourceCanBeDisabled(payload.channel, payload.state);
|
|
127
|
+
|
|
119
128
|
const normalizedSettings = this.normalizeSettings(
|
|
120
129
|
payload.channel,
|
|
121
130
|
payload.settings,
|
|
@@ -147,6 +156,9 @@ export class SourceService extends BaseOrmService<SourceOrmEntity> {
|
|
|
147
156
|
throw new NotFoundException('Source not found');
|
|
148
157
|
}
|
|
149
158
|
|
|
159
|
+
const channel = payload.channel ?? existing.channel;
|
|
160
|
+
this.ensureSourceCanBeDisabled(channel, payload.state);
|
|
161
|
+
|
|
150
162
|
if (!this.channelRegistry.findChannel(existing.channel)) {
|
|
151
163
|
if (this.isStateOnlyDisablePayload(payload)) {
|
|
152
164
|
return await super.updateOne(idOrOptions, { state: false }, options);
|
|
@@ -157,13 +169,16 @@ export class SourceService extends BaseOrmService<SourceOrmEntity> {
|
|
|
157
169
|
);
|
|
158
170
|
}
|
|
159
171
|
|
|
160
|
-
const channel = payload.channel ?? existing.channel;
|
|
161
172
|
const shouldResetSettings =
|
|
162
173
|
payload.channel !== undefined && payload.settings === undefined;
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
const shouldNormalizeSettings =
|
|
175
|
+
payload.settings !== undefined || shouldResetSettings;
|
|
176
|
+
const normalizedSettings = shouldNormalizeSettings
|
|
177
|
+
? this.normalizeSettings(
|
|
178
|
+
channel,
|
|
179
|
+
shouldResetSettings ? {} : payload.settings,
|
|
180
|
+
)
|
|
181
|
+
: undefined;
|
|
167
182
|
const normalizedDefaultWorkflow =
|
|
168
183
|
payload.defaultWorkflow !== undefined
|
|
169
184
|
? await this.normalizeDefaultWorkflow(payload.defaultWorkflow)
|
|
@@ -174,7 +189,9 @@ export class SourceService extends BaseOrmService<SourceOrmEntity> {
|
|
|
174
189
|
{
|
|
175
190
|
...payload,
|
|
176
191
|
channel,
|
|
177
|
-
|
|
192
|
+
...(normalizedSettings !== undefined
|
|
193
|
+
? { settings: normalizedSettings }
|
|
194
|
+
: {}),
|
|
178
195
|
...(normalizedDefaultWorkflow !== undefined
|
|
179
196
|
? { defaultWorkflow: normalizedDefaultWorkflow }
|
|
180
197
|
: {}),
|
package/src/config/index.ts
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
|
|
9
|
+
import {
|
|
10
|
+
parseAuditHeaders,
|
|
11
|
+
parseCsv,
|
|
12
|
+
parseIntWithFallback,
|
|
13
|
+
parseOptionalInt,
|
|
14
|
+
} from '@/utils/helpers/parse';
|
|
15
|
+
|
|
9
16
|
import { Config } from './types';
|
|
10
17
|
|
|
11
18
|
const isProduction = (process.env.NODE_ENV || 'development')
|
|
@@ -16,59 +23,6 @@ const shouldAutoMigrate =
|
|
|
16
23
|
(autoMigrateToggle === 'true' &&
|
|
17
24
|
(process.env.API_IS_PRIMARY_NODE || 'true') === 'true') ||
|
|
18
25
|
!isProduction;
|
|
19
|
-
const parseIntWithFallback = (
|
|
20
|
-
value: string | undefined,
|
|
21
|
-
fallback: number,
|
|
22
|
-
): number => {
|
|
23
|
-
if (!value) {
|
|
24
|
-
return fallback;
|
|
25
|
-
}
|
|
26
|
-
const parsed = Number.parseInt(value, 10);
|
|
27
|
-
|
|
28
|
-
return Number.isNaN(parsed) ? fallback : parsed;
|
|
29
|
-
};
|
|
30
|
-
const parseOptionalInt = (value: string | undefined): number | undefined => {
|
|
31
|
-
if (value === undefined) {
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
const parsed = Number.parseInt(value, 10);
|
|
35
|
-
|
|
36
|
-
return Number.isNaN(parsed) ? undefined : parsed;
|
|
37
|
-
};
|
|
38
|
-
const parseCsv = (value: string | undefined, fallback: string[]): string[] => {
|
|
39
|
-
if (!value) {
|
|
40
|
-
return fallback;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return value
|
|
44
|
-
.split(',')
|
|
45
|
-
.map((entry) => entry.trim())
|
|
46
|
-
.filter((entry) => entry.length > 0);
|
|
47
|
-
};
|
|
48
|
-
const parseAuditHeaders = (
|
|
49
|
-
value: string | undefined,
|
|
50
|
-
): Record<string, string> => {
|
|
51
|
-
if (!value) {
|
|
52
|
-
return {};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
const parsed = JSON.parse(value) as unknown;
|
|
57
|
-
|
|
58
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
59
|
-
return {};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return Object.fromEntries(
|
|
63
|
-
Object.entries(parsed).map(([key, headerValue]) => [
|
|
64
|
-
key,
|
|
65
|
-
String(headerValue),
|
|
66
|
-
]),
|
|
67
|
-
);
|
|
68
|
-
} catch {
|
|
69
|
-
return {};
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
26
|
const sessionMaxAge = parseIntWithFallback(
|
|
73
27
|
process.env.SESSION_MAX_AGE,
|
|
74
28
|
24 * 60 * 60 * 1000,
|
|
@@ -114,9 +68,7 @@ export const config: Config = {
|
|
|
114
68
|
headers:
|
|
115
69
|
'content-type,x-xsrf-token,x-csrf-token,authorization,mcp-session-id,last-event-id',
|
|
116
70
|
methods: ['GET', 'PATCH', 'POST', 'DELETE', 'OPTIONS', 'HEAD'],
|
|
117
|
-
allowOrigins: process.env.FRONTEND_ORIGIN
|
|
118
|
-
? process.env.FRONTEND_ORIGIN.split(',').map((origin) => origin.trim())
|
|
119
|
-
: ['*'],
|
|
71
|
+
allowOrigins: parseCsv(process.env.FRONTEND_ORIGIN, ['*']),
|
|
120
72
|
allowCredentials: true,
|
|
121
73
|
},
|
|
122
74
|
csrf: true,
|
|
@@ -174,9 +126,9 @@ export const config: Config = {
|
|
|
174
126
|
// that sets a cookie (this is used by the sails.io.js socket client
|
|
175
127
|
// to get access to a 3rd party cookie and to enable sessions).
|
|
176
128
|
grant3rdPartyCookie: true,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
129
|
+
// Admin/frontend origins for Socket.IO connections without a web source.
|
|
130
|
+
// Web widget origins are controlled by each web source's allowed_domains.
|
|
131
|
+
onlyAllowOrigins: parseCsv(process.env.FRONTEND_ORIGIN, []), // ['http://example.com', 'https://example.com'],
|
|
180
132
|
},
|
|
181
133
|
session: {
|
|
182
134
|
secret: process.env.SESSION_SECRET || 'changeme',
|
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
8
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
8
9
|
import { ProviderV2, ProviderV3 } from '@ai-sdk/provider';
|
|
9
10
|
import {
|
|
10
11
|
IncomingMessageType,
|
|
11
12
|
Message,
|
|
12
|
-
Thread,
|
|
13
13
|
StdIncomingMessage,
|
|
14
14
|
StdOutgoingMessage,
|
|
15
|
+
Thread,
|
|
15
16
|
} from '@hexabot-ai/types';
|
|
16
17
|
import {
|
|
17
18
|
LanguageModel,
|
|
@@ -123,7 +124,11 @@ export abstract class AiBaseAction<
|
|
|
123
124
|
// Most hosted providers need an API key; skip strict enforcement for custom/local providers.
|
|
124
125
|
const providerId = this.getProviderId(provider);
|
|
125
126
|
|
|
126
|
-
return
|
|
127
|
+
return (
|
|
128
|
+
providerId === 'openai' ||
|
|
129
|
+
providerId === 'gateway' ||
|
|
130
|
+
providerId === 'litellm'
|
|
131
|
+
);
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
protected async loadProvider(
|
|
@@ -143,6 +148,14 @@ export abstract class AiBaseAction<
|
|
|
143
148
|
return createGatewayProvider(options);
|
|
144
149
|
}
|
|
145
150
|
|
|
151
|
+
if (providerId === 'litellm') {
|
|
152
|
+
return createOpenAICompatible({
|
|
153
|
+
...options,
|
|
154
|
+
name: 'litellm',
|
|
155
|
+
baseURL: options.baseURL || '',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
146
159
|
const moduleCandidates = new Set<string>([
|
|
147
160
|
provider,
|
|
148
161
|
normalized,
|
|
@@ -36,6 +36,7 @@ export const vercelAiSdkProviders = [
|
|
|
36
36
|
'huggingface',
|
|
37
37
|
'hume',
|
|
38
38
|
'klingai',
|
|
39
|
+
'litellm',
|
|
39
40
|
'lmnt',
|
|
40
41
|
'luma',
|
|
41
42
|
'mistral',
|
|
@@ -85,7 +86,7 @@ export const aiModelBindingSchema = z.strictObject({
|
|
|
85
86
|
'ui:options': {
|
|
86
87
|
showWhen: {
|
|
87
88
|
field: 'provider',
|
|
88
|
-
|
|
89
|
+
in: ['gateway', 'litellm'],
|
|
89
90
|
},
|
|
90
91
|
},
|
|
91
92
|
}),
|
|
@@ -3,14 +3,10 @@
|
|
|
3
3
|
"Admin Chat Console": "Admin Chat Console",
|
|
4
4
|
"Allowed domains": "Allowed domains",
|
|
5
5
|
"Comma-separated list of allowed CORS origins.": "Comma-separated list of allowed CORS origins.",
|
|
6
|
-
"Show start button": "Show start button",
|
|
7
|
-
"Display the start button before chat begins.": "Display the start button before chat begins.",
|
|
8
6
|
"Disable input": "Disable input",
|
|
9
7
|
"Disable the user input field in the chat widget.": "Disable the user input field in the chat widget.",
|
|
10
8
|
"Persistent menu": "Persistent menu",
|
|
11
9
|
"Keep the menu visible in the chat interface.": "Keep the menu visible in the chat interface.",
|
|
12
|
-
"Greeting message": "Greeting message",
|
|
13
|
-
"Greeting shown to users when the conversation starts.": "Greeting shown to users when the conversation starts.",
|
|
14
10
|
"Show emoji": "Show emoji",
|
|
15
11
|
"Enable emoji picker in the chat interface.": "Enable emoji picker in the chat interface.",
|
|
16
12
|
"Show file upload": "Show file upload",
|
|
@@ -18,7 +14,6 @@
|
|
|
18
14
|
"Show location": "Show location",
|
|
19
15
|
"Enable location sharing action in the chat interface.": "Enable location sharing action in the chat interface.",
|
|
20
16
|
"Allowed upload types": "Allowed upload types",
|
|
21
|
-
"Comma-separated MIME types accepted by the upload input.": "Comma-separated MIME types accepted by the upload input."
|
|
22
|
-
"Welcome! Ready to start chatting with our chatbot?": "Welcome! Ready to start chatting with our chatbot?"
|
|
17
|
+
"Comma-separated MIME types accepted by the upload input.": "Comma-separated MIME types accepted by the upload input."
|
|
23
18
|
}
|
|
24
19
|
}
|
|
@@ -3,14 +3,10 @@
|
|
|
3
3
|
"Admin Chat Console": "Testeur Live Chat",
|
|
4
4
|
"Allowed domains": "Domaines autorisés",
|
|
5
5
|
"Comma-separated list of allowed CORS origins.": "Liste séparée par des virgules des origines CORS autorisées.",
|
|
6
|
-
"Show start button": "Afficher le bouton de démarrage",
|
|
7
|
-
"Display the start button before chat begins.": "Afficher le bouton de démarrage avant le début de la discussion.",
|
|
8
6
|
"Disable input": "Désactiver la saisie",
|
|
9
7
|
"Disable the user input field in the chat widget.": "Désactiver le champ de saisie utilisateur dans le widget de chat.",
|
|
10
8
|
"Persistent menu": "Menu persistant",
|
|
11
9
|
"Keep the menu visible in the chat interface.": "Garder le menu visible dans l'interface de chat.",
|
|
12
|
-
"Greeting message": "Message de bienvenue",
|
|
13
|
-
"Greeting shown to users when the conversation starts.": "Message affiché aux utilisateurs au début de la conversation.",
|
|
14
10
|
"Show emoji": "Afficher les emojis",
|
|
15
11
|
"Enable emoji picker in the chat interface.": "Activer le sélecteur d'emojis dans l'interface de chat.",
|
|
16
12
|
"Show file upload": "Afficher l'envoi de fichier",
|
|
@@ -18,7 +14,6 @@
|
|
|
18
14
|
"Show location": "Afficher la localisation",
|
|
19
15
|
"Enable location sharing action in the chat interface.": "Activer le partage de localisation dans l'interface de chat.",
|
|
20
16
|
"Allowed upload types": "Types de téléversement autorisés",
|
|
21
|
-
"Comma-separated MIME types accepted by the upload input.": "Types MIME acceptés par le champ d'envoi, séparés par des virgules."
|
|
22
|
-
"Welcome! Ready to start chatting with our chatbot?": "Bienvenue ! Prêt à commencer à discuter avec notre chatbot ?"
|
|
17
|
+
"Comma-separated MIME types accepted by the upload input.": "Types MIME acceptés par le champ d'envoi, séparés par des virgules."
|
|
23
18
|
}
|
|
24
19
|
}
|
|
@@ -18,6 +18,10 @@ export default class ConsoleChannelHandler extends BaseWebChannelHandler<
|
|
|
18
18
|
typeof CONSOLE_CHANNEL_NAME
|
|
19
19
|
> {
|
|
20
20
|
constructor() {
|
|
21
|
-
super(
|
|
21
|
+
super(
|
|
22
|
+
CONSOLE_CHANNEL_NAME,
|
|
23
|
+
CONSOLE_CHANNEL_SOURCE_SETTINGS_SCHEMA,
|
|
24
|
+
'system',
|
|
25
|
+
);
|
|
22
26
|
}
|
|
23
27
|
}
|
|
@@ -22,10 +22,6 @@ export const CONSOLE_CHANNEL_SOURCE_SETTINGS_SCHEMA = z
|
|
|
22
22
|
title: 'Allowed domains',
|
|
23
23
|
description: 'Comma-separated list of allowed CORS origins.',
|
|
24
24
|
}),
|
|
25
|
-
start_button: z.boolean().default(true).meta({
|
|
26
|
-
title: 'Show start button',
|
|
27
|
-
description: 'Display the start button before chat begins.',
|
|
28
|
-
}),
|
|
29
25
|
input_disabled: z.boolean().default(false).meta({
|
|
30
26
|
title: 'Disable input',
|
|
31
27
|
description: 'Disable the user input field in the chat widget.',
|
|
@@ -34,14 +30,6 @@ export const CONSOLE_CHANNEL_SOURCE_SETTINGS_SCHEMA = z
|
|
|
34
30
|
title: 'Persistent menu',
|
|
35
31
|
description: 'Keep the menu visible in the chat interface.',
|
|
36
32
|
}),
|
|
37
|
-
greeting_message: z
|
|
38
|
-
.string()
|
|
39
|
-
.default('Welcome! Ready to start chatting with our chatbot?')
|
|
40
|
-
.meta({
|
|
41
|
-
title: 'Greeting message',
|
|
42
|
-
description: 'Greeting shown to users when the conversation starts.',
|
|
43
|
-
'ui:widget': 'textarea',
|
|
44
|
-
}),
|
|
45
33
|
show_emoji: z.boolean().default(true).meta({
|
|
46
34
|
title: 'Show emoji',
|
|
47
35
|
description: 'Enable emoji picker in the chat interface.',
|
|
@@ -8,6 +8,7 @@ import { randomUUID } from 'crypto';
|
|
|
8
8
|
|
|
9
9
|
import type {
|
|
10
10
|
Attachment,
|
|
11
|
+
ChannelVisibility,
|
|
11
12
|
Source,
|
|
12
13
|
StdOutgoingMessageEnvelope,
|
|
13
14
|
Subscriber,
|
|
@@ -50,6 +51,7 @@ import { MessageService } from '@/chat/services/message.service';
|
|
|
50
51
|
import type { SubscriberChannelData } from '@/chat/types/channel';
|
|
51
52
|
import { MenuService } from '@/cms/services/menu.service';
|
|
52
53
|
import { config } from '@/config';
|
|
54
|
+
import { getAllowedDomains } from '@/utils/helpers/origin';
|
|
53
55
|
import { SocketRequest } from '@/websocket/utils/socket-request';
|
|
54
56
|
import { SocketResponse } from '@/websocket/utils/socket-response';
|
|
55
57
|
|
|
@@ -116,8 +118,12 @@ export default abstract class BaseWebChannelHandler<N extends ChannelName>
|
|
|
116
118
|
@ExtensionInject(WebHistoryService)
|
|
117
119
|
private historyService!: WebHistoryService;
|
|
118
120
|
|
|
119
|
-
constructor(
|
|
120
|
-
|
|
121
|
+
constructor(
|
|
122
|
+
name: N,
|
|
123
|
+
sourceSettingsSchema?: z.ZodTypeAny,
|
|
124
|
+
visibility?: ChannelVisibility,
|
|
125
|
+
) {
|
|
126
|
+
super(name, sourceSettingsSchema, visibility);
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
async onModuleInit() {
|
|
@@ -303,12 +309,11 @@ export default abstract class BaseWebChannelHandler<N extends ChannelName>
|
|
|
303
309
|
res: Response | SocketResponse,
|
|
304
310
|
source: Source,
|
|
305
311
|
): Promise<void> {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
await this.sessionService.validateCors(req, res, allowedDomains);
|
|
312
|
+
await this.sessionService.validateCors(
|
|
313
|
+
req,
|
|
314
|
+
res,
|
|
315
|
+
getAllowedDomains(source.settings),
|
|
316
|
+
);
|
|
312
317
|
}
|
|
313
318
|
|
|
314
319
|
protected async getOrCreateSession(
|
|
@@ -12,6 +12,7 @@ import { SubscriberCreateDto } from '@/chat/dto/subscriber.dto';
|
|
|
12
12
|
import { SubscriberService } from '@/chat/services/subscriber.service';
|
|
13
13
|
import { ThreadService } from '@/chat/services/thread.service';
|
|
14
14
|
import { LoggerService } from '@/logger/logger.service';
|
|
15
|
+
import { isAllowedOrigin, normalizeOrigin } from '@/utils/helpers/origin';
|
|
15
16
|
import { SocketRequest } from '@/websocket/utils/socket-request';
|
|
16
17
|
import { SocketResponse } from '@/websocket/utils/socket-response';
|
|
17
18
|
|
|
@@ -62,38 +63,26 @@ export class WebSessionService {
|
|
|
62
63
|
res: Response | SocketResponse,
|
|
63
64
|
allowedDomains: string,
|
|
64
65
|
): Promise<void> {
|
|
65
|
-
|
|
66
|
+
const origin = req.headers?.origin;
|
|
67
|
+
|
|
68
|
+
if (!origin) {
|
|
66
69
|
this.logger.debug('No origin ', req.headers);
|
|
67
70
|
throw new Error('CORS - No origin provided!');
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
const
|
|
71
|
-
|
|
73
|
+
const normalizedOrigin = normalizeOrigin(origin);
|
|
74
|
+
|
|
75
|
+
if (!normalizedOrigin) {
|
|
72
76
|
throw new Error('CORS - Invalid origin!');
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
const foundOrigin = origins
|
|
77
|
-
.filter((o) => o.trim() !== '*')
|
|
78
|
-
.map((o) => {
|
|
79
|
-
try {
|
|
80
|
-
return new URL(o.trim()).origin;
|
|
81
|
-
} catch {
|
|
82
|
-
this.logger.error(`Invalid URL in allowed domains: ${o}`);
|
|
83
|
-
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
})
|
|
87
|
-
.filter((o): o is string => o !== null)
|
|
88
|
-
.some((o) => o === originUrl.origin);
|
|
89
|
-
|
|
90
|
-
if (!foundOrigin && !origins.includes('*')) {
|
|
79
|
+
if (!isAllowedOrigin(normalizedOrigin, allowedDomains)) {
|
|
91
80
|
res.set('Access-Control-Allow-Origin', '');
|
|
92
|
-
this.logger.debug('No origin found ',
|
|
81
|
+
this.logger.debug('No origin found ', origin);
|
|
93
82
|
throw new Error('CORS - Domain not allowed!');
|
|
94
83
|
}
|
|
95
84
|
|
|
96
|
-
res.set('Access-Control-Allow-Origin',
|
|
85
|
+
res.set('Access-Control-Allow-Origin', normalizedOrigin);
|
|
97
86
|
res.set('Access-Control-Allow-Credentials', 'true');
|
|
98
87
|
res.set('Access-Control-Expose-Headers', '');
|
|
99
88
|
if (req.method === 'OPTIONS') {
|
|
@@ -12,6 +12,8 @@ export const WEB_CHANNEL_NAME = 'web' as const;
|
|
|
12
12
|
|
|
13
13
|
const WEB_ALLOWED_UPLOAD_TYPES =
|
|
14
14
|
'audio/mpeg,audio/x-ms-wma,audio/vnd.rn-realaudio,audio/x-wav,image/gif,image/jpeg,image/png,image/tiff,image/vnd.microsoft.icon,image/vnd.djvu,image/svg+xml,text/css,text/csv,text/html,text/plain,text/xml,video/mpeg,video/mp4,video/quicktime,video/x-ms-wmv,video/x-msvideo,video/x-flv,video/web,application/msword,application/vnd.ms-powerpoint,application/pdf,application/vnd.ms-excel,application/vnd.oasis.opendocument.presentation,application/vnd.oasis.opendocument.tex,application/vnd.oasis.opendocument.spreadsheet,application/vnd.oasis.opendocument.graphics,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
15
|
+
const emptyStringToUndefined = (value: unknown) =>
|
|
16
|
+
typeof value === 'string' && value.trim() === '' ? undefined : value;
|
|
15
17
|
|
|
16
18
|
export const WEB_CHANNEL_SOURCE_SETTINGS_SCHEMA = z
|
|
17
19
|
.strictObject({
|
|
@@ -46,10 +48,14 @@ export const WEB_CHANNEL_SOURCE_SETTINGS_SCHEMA = z
|
|
|
46
48
|
title: 'Window title',
|
|
47
49
|
description: 'Title displayed in the web widget header.',
|
|
48
50
|
}),
|
|
49
|
-
avatar_url: z
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
avatar_url: z
|
|
52
|
+
.preprocess(emptyStringToUndefined, z.url().optional())
|
|
53
|
+
.default('')
|
|
54
|
+
.optional()
|
|
55
|
+
.meta({
|
|
56
|
+
title: 'Avatar URL',
|
|
57
|
+
description: 'URL of the chatbot avatar image.',
|
|
58
|
+
}),
|
|
53
59
|
show_emoji: z.boolean().default(true).meta({
|
|
54
60
|
title: 'Show emoji',
|
|
55
61
|
description: 'Enable emoji picker in the chat interface.',
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { Cacheable } from '@/utils/decorators/cacheable.decorator';
|
|
29
29
|
import { UpdateOneOptions } from '@/utils/generics/base-orm.repository';
|
|
30
30
|
import { BaseOrmService } from '@/utils/generics/base-orm.service';
|
|
31
|
+
import { splitAllowedOrigins } from '@/utils/helpers/origin';
|
|
31
32
|
import { UpdateEntityEvent } from '@/utils/types/entity-event.types';
|
|
32
33
|
|
|
33
34
|
import { SettingCreateDto, SettingUpdateDto } from '../dto/setting.dto';
|
|
@@ -272,9 +273,9 @@ export class SettingService extends BaseOrmService<SettingOrmEntity> {
|
|
|
272
273
|
where: { label: 'allowed_domains' },
|
|
273
274
|
})) as Setting[];
|
|
274
275
|
const allowedDomains = settings.flatMap((setting) =>
|
|
275
|
-
|
|
276
|
-
.
|
|
277
|
-
|
|
276
|
+
typeof setting.value === 'string'
|
|
277
|
+
? splitAllowedOrigins(setting.value)
|
|
278
|
+
: [],
|
|
278
279
|
);
|
|
279
280
|
const uniqueOrigins = new Set([
|
|
280
281
|
...config.security.cors.allowOrigins,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2026 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { parseCsv } from './parse';
|
|
8
|
+
|
|
9
|
+
export const normalizeOrigin = (value: string | undefined): string | null => {
|
|
10
|
+
if (!value) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(value.trim());
|
|
16
|
+
|
|
17
|
+
return ['http:', 'https:'].includes(url.protocol) ? url.origin : null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const splitAllowedOrigins = (
|
|
24
|
+
allowedOrigins: string | string[],
|
|
25
|
+
): string[] => parseCsv(allowedOrigins, []);
|
|
26
|
+
|
|
27
|
+
export const getAllowedDomains = (settings: Record<string, unknown>): string =>
|
|
28
|
+
typeof settings.allowed_domains === 'string' ? settings.allowed_domains : '*';
|
|
29
|
+
|
|
30
|
+
export const isAllowedOrigin = (
|
|
31
|
+
origin: string | undefined,
|
|
32
|
+
allowedOrigins: string | string[],
|
|
33
|
+
): boolean => {
|
|
34
|
+
const normalizedOrigin = normalizeOrigin(origin);
|
|
35
|
+
const allowedOriginValues = splitAllowedOrigins(allowedOrigins);
|
|
36
|
+
|
|
37
|
+
if (!normalizedOrigin) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (allowedOriginValues.includes('*')) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const normalizedAllowedOrigins = allowedOriginValues
|
|
46
|
+
.map(normalizeOrigin)
|
|
47
|
+
.filter((allowedOrigin): allowedOrigin is string => allowedOrigin !== null);
|
|
48
|
+
|
|
49
|
+
return normalizedAllowedOrigins.includes(normalizedOrigin);
|
|
50
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2026 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const parseIntWithFallback = (
|
|
8
|
+
value: string | undefined,
|
|
9
|
+
fallback: number,
|
|
10
|
+
): number => {
|
|
11
|
+
if (!value) {
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
const parsed = Number.parseInt(value, 10);
|
|
15
|
+
|
|
16
|
+
return Number.isNaN(parsed) ? fallback : parsed;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const parseOptionalInt = (
|
|
20
|
+
value: string | undefined,
|
|
21
|
+
): number | undefined => {
|
|
22
|
+
if (value === undefined) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const parsed = Number.parseInt(value, 10);
|
|
26
|
+
|
|
27
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const parseCsv = (
|
|
31
|
+
value: string | string[] | undefined,
|
|
32
|
+
fallback: string[],
|
|
33
|
+
): string[] => {
|
|
34
|
+
if (!value) {
|
|
35
|
+
return fallback;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const values = Array.isArray(value) ? value : value.split(',');
|
|
39
|
+
|
|
40
|
+
return values
|
|
41
|
+
.map((entry) => entry.trim())
|
|
42
|
+
.filter((entry) => entry.length > 0);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const parseAuditHeaders = (
|
|
46
|
+
value: string | undefined,
|
|
47
|
+
): Record<string, string> => {
|
|
48
|
+
if (!value) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(value) as unknown;
|
|
54
|
+
|
|
55
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return Object.fromEntries(
|
|
60
|
+
Object.entries(parsed).map(([key, headerValue]) => [
|
|
61
|
+
key,
|
|
62
|
+
String(headerValue),
|
|
63
|
+
]),
|
|
64
|
+
);
|
|
65
|
+
} catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
};
|
package/src/utils/index.ts
CHANGED