@botfabrik/engine-webclient 4.109.11 → 4.109.12-alpha.0
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/client/index.html
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<meta name="theme-color" content="#000000" />
|
|
14
14
|
<meta name="google" content="notranslate" />
|
|
15
15
|
<title>Bubble Chat Client</title>
|
|
16
|
-
<script type="module" crossorigin src="./assets/index-
|
|
16
|
+
<script type="module" crossorigin src="./assets/index-DfW2mfd6.js"></script>
|
|
17
17
|
<link rel="stylesheet" crossorigin href="./assets/index-BKtBdibb.css">
|
|
18
18
|
</head>
|
|
19
19
|
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Actions, ActionTypes, BotUser, TextMessage, } from '@botfabrik/engine-domain';
|
|
2
2
|
import { getPdf } from '@botfabrik/engine-transcript-export';
|
|
3
|
-
import
|
|
3
|
+
import cors from 'cors';
|
|
4
|
+
import { Router, static as serveStatic, } from 'express';
|
|
5
|
+
import helmet from 'helmet';
|
|
4
6
|
import { dirname } from 'node:path';
|
|
5
7
|
import { fileURLToPath } from 'node:url';
|
|
6
8
|
import { setUpSamlAuth, storeLoginRequestToken, verifyLoginToken, } from './auth/index.js';
|
|
@@ -21,8 +23,95 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
21
23
|
const __dirname = dirname(__filename);
|
|
22
24
|
export default (clientName, environment, props) => async (bot) => {
|
|
23
25
|
const logger = bot.logger.child({ clientType: CLIENT_TYPE, clientName });
|
|
26
|
+
const router = Router();
|
|
27
|
+
// Security: CORS
|
|
28
|
+
// When allowedOrigins is undefined, all origins are permitted (backward-compatible default).
|
|
29
|
+
// When it is set (even to an empty array), only the listed origins are allowed.
|
|
30
|
+
const allowedOrigins = props.allowedOrigins;
|
|
31
|
+
// When trustedResourceOrigins is undefined, the webclient may load resources from all origins.
|
|
32
|
+
// When it is set, only those origins are trusted as resource sources (font-src, img-src, …).
|
|
33
|
+
const trustedResourceOrigins = props.trustedResourceOrigins;
|
|
34
|
+
router.use(cors({
|
|
35
|
+
// Access-Control-Allow-Origin
|
|
36
|
+
origin: allowedOrigins === undefined
|
|
37
|
+
? true // all origins allowed
|
|
38
|
+
: (origin, callback) => {
|
|
39
|
+
if (!origin || allowedOrigins.includes(origin)) {
|
|
40
|
+
callback(null, true);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
callback(new Error(`CORS: Origin '${origin}' is not allowed`));
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
// Access-Control-Allow-Methods
|
|
47
|
+
methods: ['GET', 'POST', 'OPTIONS'],
|
|
48
|
+
// Access-Control-Allow-Headers
|
|
49
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
50
|
+
credentials: true,
|
|
51
|
+
}));
|
|
52
|
+
// Security: Helmet (CSP + security headers)
|
|
53
|
+
router.use(helmet({
|
|
54
|
+
contentSecurityPolicy: {
|
|
55
|
+
directives: {
|
|
56
|
+
defaultSrc: ["'self'"],
|
|
57
|
+
scriptSrc: ["'self'"],
|
|
58
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
59
|
+
// images: restrict to trusted resource origins when defined, otherwise allow all HTTPS
|
|
60
|
+
imgSrc: trustedResourceOrigins === undefined
|
|
61
|
+
? ["'self'", 'data:', 'https:']
|
|
62
|
+
: ["'self'", 'data:', ...trustedResourceOrigins],
|
|
63
|
+
connectSrc: [
|
|
64
|
+
"'self'",
|
|
65
|
+
// allow websocket connections back to the same server
|
|
66
|
+
bot.webserver.baseUrl.replace(/^http/, 'ws'),
|
|
67
|
+
bot.webserver.baseUrl.replace(/^http/, 'wss'),
|
|
68
|
+
],
|
|
69
|
+
// fonts may be loaded from trusted resource origins (e.g. Google Fonts);
|
|
70
|
+
// when no trustedResourceOrigins are configured, allow fonts from all origins
|
|
71
|
+
fontSrc: trustedResourceOrigins === undefined
|
|
72
|
+
? ['*']
|
|
73
|
+
: ["'self'", ...trustedResourceOrigins],
|
|
74
|
+
// when no allowedOrigins are configured, allow framing from all origins
|
|
75
|
+
frameAncestors: allowedOrigins === undefined
|
|
76
|
+
? ['*']
|
|
77
|
+
: ["'self'", ...allowedOrigins],
|
|
78
|
+
objectSrc: ["'none'"], // disable Flash/plugins
|
|
79
|
+
baseUri: ["'self'"], // prevent <base>-tag injection
|
|
80
|
+
// When SAML auth is configured, the browser POSTs a form to the external IdP.
|
|
81
|
+
// The IdP URL must therefore be explicitly allowed alongside 'self'.
|
|
82
|
+
formAction: props.auth?.saml.entryPoint
|
|
83
|
+
? ["'self'", props.auth.saml.entryPoint]
|
|
84
|
+
: ["'self'"],
|
|
85
|
+
// upgrade-insecure-requests is intentionally omitted:
|
|
86
|
+
// the webclient is embedded via iframe on external pages which may themselves be HTTP,
|
|
87
|
+
// and the directive would also affect those sub-resource loads in some browsers.
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
// Disable X-Frame-Options: we control framing exclusively via CSP frame-ancestors above.
|
|
91
|
+
// X-Frame-Options: SAMEORIGIN (helmet default) would block cross-origin iframe embedding.
|
|
92
|
+
frameguard: false,
|
|
93
|
+
// Send origin only, without path – sufficient for analytics while avoiding leaking URLs
|
|
94
|
+
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
|
|
95
|
+
// resources served by the webclient must be loadable cross-origin (e.g. embed script)
|
|
96
|
+
crossOriginResourcePolicy: { policy: 'cross-origin' },
|
|
97
|
+
}));
|
|
98
|
+
// Permissions-Policy: restrict browser feature access to the minimum required.
|
|
99
|
+
// Microphone is only permitted when speech-to-text is configured.
|
|
100
|
+
router.use((_req, res, next) => {
|
|
101
|
+
const microphonePolicy = props.speech
|
|
102
|
+
? 'microphone=(self)'
|
|
103
|
+
: 'microphone=()';
|
|
104
|
+
res.setHeader('Permissions-Policy', [
|
|
105
|
+
microphonePolicy,
|
|
106
|
+
'camera=()',
|
|
107
|
+
'geolocation=()',
|
|
108
|
+
'payment=()',
|
|
109
|
+
'usb=()',
|
|
110
|
+
].join(', '));
|
|
111
|
+
next();
|
|
112
|
+
});
|
|
24
113
|
// serve transcript pdf
|
|
25
|
-
|
|
114
|
+
router.use('/transcript-pdf/:sessionId', async (req, res) => {
|
|
26
115
|
const sessionId = req.params['sessionId'];
|
|
27
116
|
if (sessionId?.length) {
|
|
28
117
|
const session = await bot.createSession(sessionId);
|
|
@@ -36,7 +125,7 @@ export default (clientName, environment, props) => async (bot) => {
|
|
|
36
125
|
}
|
|
37
126
|
});
|
|
38
127
|
// serve embed resources
|
|
39
|
-
|
|
128
|
+
router.get('/embed', (req, res) => {
|
|
40
129
|
const server = `${bot.webserver.baseUrl}/${clientName}`;
|
|
41
130
|
const serverUrl = new URL(server);
|
|
42
131
|
// Pass all query parameters to the chatbot URL
|
|
@@ -53,7 +142,7 @@ export default (clientName, environment, props) => async (bot) => {
|
|
|
53
142
|
res.end();
|
|
54
143
|
});
|
|
55
144
|
if (fabVisibilityIsConditional(props)) {
|
|
56
|
-
|
|
145
|
+
router.get('/embed/bundle.js', (req, res) => {
|
|
57
146
|
const caller = req.query['caller'];
|
|
58
147
|
const referer = req.get('referer')?.toLowerCase();
|
|
59
148
|
const visibleUrls = Array.isArray(props.fabVisible)
|
|
@@ -72,18 +161,20 @@ export default (clientName, environment, props) => async (bot) => {
|
|
|
72
161
|
}
|
|
73
162
|
});
|
|
74
163
|
}
|
|
75
|
-
|
|
76
|
-
|
|
164
|
+
router.use('/embed', serveStatic(__dirname + '/embed', serveStaticOptions));
|
|
165
|
+
router.get('/logo.svg', (_req, res) => {
|
|
77
166
|
res.redirect(`/cms/chatbot/design/logo.svg?client=${clientName}`);
|
|
78
167
|
});
|
|
79
|
-
|
|
168
|
+
router.get('/bot.svg', (_req, res) => {
|
|
80
169
|
res.redirect(`/cms/chatbot/design/bot.svg?client=${clientName}`);
|
|
81
170
|
});
|
|
82
|
-
|
|
171
|
+
router.get('/fab.svg', (_req, res) => {
|
|
83
172
|
res.redirect(`/cms/chatbot/design/fab.svg?client=${clientName}`);
|
|
84
173
|
});
|
|
85
174
|
// serve chat client resources
|
|
86
|
-
|
|
175
|
+
router.use(serveStatic(__dirname + '/client', serveStaticOptions));
|
|
176
|
+
// mount router on the main app
|
|
177
|
+
bot.webserver.express.use(`/${clientName}`, router);
|
|
87
178
|
logger.info(`Webclient will be available on route: /${clientName}`);
|
|
88
179
|
const nsp = bot.webserver.socket.of(`/${clientName}/chat`);
|
|
89
180
|
nsp.on('connection', async (socket) => {
|
|
@@ -222,6 +313,7 @@ const sendConfigurationToClient = (socket, props, bot, clientName) => {
|
|
|
222
313
|
enableSpeechSupport: !!props.speech,
|
|
223
314
|
commands: props.commands || [],
|
|
224
315
|
menuItems: props.menuItems || [],
|
|
316
|
+
allowedOrigins: props.allowedOrigins,
|
|
225
317
|
};
|
|
226
318
|
socket.emit('set-settings', settings);
|
|
227
319
|
if (props.expandChatWindowAtStart &&
|
package/dist/types.d.ts
CHANGED
|
@@ -183,6 +183,35 @@ export interface WebClientProps {
|
|
|
183
183
|
* @default []
|
|
184
184
|
*/
|
|
185
185
|
menuItems?: MenuItem[];
|
|
186
|
+
/**
|
|
187
|
+
* List of allowed origins for CORS and CSP `frame-ancestors`.
|
|
188
|
+
*
|
|
189
|
+
* Controls **who may use the webclient**:
|
|
190
|
+
* - embed the chatbot in an iframe (`frame-ancestors` directive of the Content Security Policy)
|
|
191
|
+
* - make cross-origin requests to webclient endpoints (CORS `Access-Control-Allow-Origin`)
|
|
192
|
+
*
|
|
193
|
+
* - **Not set (default):** all origins are allowed – backward-compatible behaviour.
|
|
194
|
+
* - **Set to an array:** only the listed origins are permitted; all others are blocked.
|
|
195
|
+
* An empty array `[]` blocks all cross-origin access.
|
|
196
|
+
*
|
|
197
|
+
* @default undefined (all origins allowed)
|
|
198
|
+
*/
|
|
199
|
+
allowedOrigins?: string[];
|
|
200
|
+
/**
|
|
201
|
+
* List of trusted origins the webclient itself may load resources from.
|
|
202
|
+
*
|
|
203
|
+
* Controls **what external sources the webclient (iframe) may use**, e.g.:
|
|
204
|
+
* - fonts (`font-src`), e.g. `https://fonts.gstatic.com` for Google Fonts
|
|
205
|
+
* - images (`img-src`)
|
|
206
|
+
*
|
|
207
|
+
* Typical entries: `https://fonts.googleapis.com`, `https://fonts.gstatic.com`
|
|
208
|
+
*
|
|
209
|
+
* - **Not set (default):** all origins are trusted – backward-compatible behaviour.
|
|
210
|
+
* - **Set to an array:** only the listed origins are permitted as resource sources.
|
|
211
|
+
*
|
|
212
|
+
* @default undefined (all origins trusted)
|
|
213
|
+
*/
|
|
214
|
+
trustedResourceOrigins?: string[];
|
|
186
215
|
}
|
|
187
216
|
export declare enum Devices {
|
|
188
217
|
All = "all",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botfabrik/engine-webclient",
|
|
3
|
-
"version": "4.109.
|
|
3
|
+
"version": "4.109.12-alpha.0",
|
|
4
4
|
"description": "Webclient for Botfabriks Bot Engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -36,8 +36,11 @@
|
|
|
36
36
|
"@botfabrik/engine-utils": "^4.109.11",
|
|
37
37
|
"@google-cloud/speech": "^7.3.0",
|
|
38
38
|
"@node-saml/passport-saml": "^5.1.0",
|
|
39
|
+
"@types/cors": "^2.8.19",
|
|
40
|
+
"cors": "^2.8.6",
|
|
39
41
|
"express": "^5.2.1",
|
|
40
42
|
"flat": "^6.0.1",
|
|
43
|
+
"helmet": "^8.1.0",
|
|
41
44
|
"jose": "^6.1.3",
|
|
42
45
|
"passport": "^0.7.0"
|
|
43
46
|
},
|
|
@@ -50,5 +53,5 @@
|
|
|
50
53
|
"tsx": "^4.21.0",
|
|
51
54
|
"typescript": "5.9.3"
|
|
52
55
|
},
|
|
53
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "49afbc154f2bdd31c5cb2a8c9c6d605959f538bf"
|
|
54
57
|
}
|