@botfabrik/engine-webclient 4.93.2 → 4.93.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.
@@ -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-DBZImkhZ.js"></script>
16
+ <script type="module" crossorigin src="./assets/index-BwGiWG8Z.js"></script>
17
17
  <link rel="stylesheet" crossorigin href="./assets/index-CQifi_K_.css">
18
18
  </head>
19
19
 
@@ -6,9 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const globals_1 = require("@jest/globals");
7
7
  const createSessionInfo_1 = __importDefault(require("./createSessionInfo"));
8
8
  (0, globals_1.describe)('create session info', () => {
9
- const props = {
10
- sessionTokenSecret: 'some-secret',
11
- };
12
9
  const querystrings = {};
13
10
  const headers = {
14
11
  host: 'fluance-chatbot.scapp.io',
@@ -46,7 +43,7 @@ const createSessionInfo_1 = __importDefault(require("./createSessionInfo"));
46
43
  environment: 'PROD',
47
44
  };
48
45
  (0, globals_1.it)('without user payload', async () => {
49
- const sessionInfo = await (0, createSessionInfo_1.default)(socket, 'my-client', 'TEST', defaultSessionInfo, 'my-user-id', 'de_DE', { email: 'hans@example.com' }, props)();
46
+ const sessionInfo = await (0, createSessionInfo_1.default)(socket, 'my-client', 'TEST', defaultSessionInfo, 'my-user-id', 'de_DE', { email: 'hans@example.com' }, {})();
50
47
  // client
51
48
  (0, globals_1.expect)(sessionInfo.client.name).toBe('my-client');
52
49
  (0, globals_1.expect)(sessionInfo.client.type).toBe('webclient');
@@ -73,7 +70,6 @@ const createSessionInfo_1 = __importDefault(require("./createSessionInfo"));
73
70
  };
74
71
  const props = {
75
72
  requestUserInfos,
76
- sessionTokenSecret: 'some-secret',
77
73
  };
78
74
  const sessionInfo = await (0, createSessionInfo_1.default)(socket, 'my-client', 'TEST', defaultSessionInfo, 'my-user-id', 'de_DE', {}, props)();
79
75
  (0, globals_1.expect)(sessionInfo.user.id).toBe('hans.muster@PRIMARY');
@@ -88,7 +84,6 @@ const createSessionInfo_1 = __importDefault(require("./createSessionInfo"));
88
84
  };
89
85
  const props = {
90
86
  requestUserInfos,
91
- sessionTokenSecret: 'some-secret',
92
87
  };
93
88
  const sessionInfo = await (0, createSessionInfo_1.default)(socket, 'my-client', 'TEST', defaultSessionInfo, 'my-user-id', 'de_DE', {}, props)();
94
89
  (0, globals_1.expect)(sessionInfo.user.id).toBe('my-user-id');
package/dist/index.d.ts CHANGED
@@ -13,12 +13,6 @@ export interface WebClientProps {
13
13
  speech?: SpeechToTextProps | undefined;
14
14
  expandChatWindowAtStart?: Devices;
15
15
  fabVisible?: boolean;
16
- /**
17
- * A secret key used to generate and verify session tokens (e.g. HMAC).
18
- * Should be long, random, and stored securely (e.g. via environment variable).
19
- * Used for signing tokens that identify sessions over insecure channels like WebSocket.
20
- */
21
- sessionTokenSecret: string;
22
16
  /**
23
17
  * Displays a start screen before a new chat begins, useful for user opt-in (e.g., terms acceptance).
24
18
  * Contains a text (`start-screen.intro` in Markdown), form fields, and a start button (`start-screen.start-chat`).
package/dist/index.js CHANGED
@@ -6,9 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.CLIENT_TYPE = exports.Devices = void 0;
7
7
  const engine_domain_1 = require("@botfabrik/engine-domain");
8
8
  const engine_transcript_export_1 = require("@botfabrik/engine-transcript-export");
9
- const cookie_parser_1 = __importDefault(require("cookie-parser"));
10
- const crypto_1 = require("crypto");
11
9
  const express_1 = require("express");
10
+ const node_crypto_1 = require("node:crypto");
12
11
  const package_json_1 = require("../package.json");
13
12
  const createSessionInfo_1 = __importDefault(require("./createSessionInfo"));
14
13
  const extractLocale_1 = __importDefault(require("./extractLocale"));
@@ -19,6 +18,8 @@ const requestSessionData_1 = __importDefault(require("./requestSessionData"));
19
18
  const setTranslations_1 = __importDefault(require("./setTranslations"));
20
19
  const speechToText_1 = __importDefault(require("./speechToText"));
21
20
  const views_1 = __importDefault(require("./views"));
21
+ const SESSION_ID_COOKIE_NAME = 'session-id';
22
+ const USER_ID_COOKIE_NAME = 'user-id';
22
23
  var Devices;
23
24
  (function (Devices) {
24
25
  Devices["All"] = "all";
@@ -26,15 +27,11 @@ var Devices;
26
27
  Devices["Desktop"] = "desktop";
27
28
  Devices["None"] = "none";
28
29
  })(Devices || (exports.Devices = Devices = {}));
29
- const SESSION_ID_COOKIE_NAME = 'session-id';
30
- const USER_ID_COOKIE_NAME = 'user-id';
31
30
  exports.default = (clientName, environment, props) => async (bot) => {
32
31
  const logger = bot.logger.child({ clientType: exports.CLIENT_TYPE, clientName });
33
- bot.webserver.express.use((0, cookie_parser_1.default)());
34
32
  // serve transcript pdf
35
- bot.webserver.express.get(`/${clientName}/transcript-pdf`, async (req, res) => {
36
- const cookies = req.cookies || {};
37
- const sessionId = cookies[SESSION_ID_COOKIE_NAME];
33
+ bot.webserver.express.use('/transcript-pdf/:sessionId', async (req, res) => {
34
+ const sessionId = req.params['sessionId'];
38
35
  if (sessionId?.length) {
39
36
  const session = await bot.createSession(sessionId);
40
37
  const pdf = await (0, engine_transcript_export_1.getPdf)(session);
@@ -67,25 +64,19 @@ exports.default = (clientName, environment, props) => async (bot) => {
67
64
  });
68
65
  }
69
66
  bot.webserver.express.use(`/${clientName}/embed`, (0, express_1.static)(__dirname + '/embed', serveStaticOptions));
67
+ // TODO: remove in future versions
70
68
  bot.webserver.express.get(`/${clientName}/me/session`, (req, res) => {
69
+ logger.info('Old client access get token');
71
70
  const cookies = req.cookies || {};
72
- const sessionId = cookies[SESSION_ID_COOKIE_NAME] || (0, crypto_1.randomUUID)();
71
+ const sessionId = cookies[SESSION_ID_COOKIE_NAME] || (0, node_crypto_1.randomUUID)();
73
72
  const userId = cookies[USER_ID_COOKIE_NAME] ||
74
73
  cookies['bubble-chat-user-id'] || // TODO: remove in future versions
75
- (0, crypto_1.randomUUID)();
76
- const sessionIdToken = generateSignedToken(sessionId, props.sessionTokenSecret);
77
- const userIdToken = generateSignedToken(userId, props.sessionTokenSecret);
78
- res.cookie(SESSION_ID_COOKIE_NAME, sessionId, {
79
- ...buildCookieOptions(clientName, bot),
80
- maxAge: 24 * 60 * 60 * 1000, // 1 day
81
- });
82
- if (!cookies[USER_ID_COOKIE_NAME]) {
83
- res.cookie(USER_ID_COOKIE_NAME, userId, buildCookieOptions(clientName, bot));
84
- }
85
- res.status(201).json({ sessionIdToken, userIdToken });
74
+ (0, node_crypto_1.randomUUID)();
75
+ res.status(201).json({ sessionIdToken: sessionId, userIdToken: userId });
86
76
  });
77
+ // TODO: remove in future versions
87
78
  bot.webserver.express.delete(`/${clientName}/me/session`, (_req, res) => {
88
- res.clearCookie(SESSION_ID_COOKIE_NAME, buildCookieOptions(clientName, bot));
79
+ logger.info('Old client access delete token');
89
80
  res.sendStatus(204);
90
81
  });
91
82
  bot.webserver.express.get(`/${clientName}/logo.svg`, (_req, res) => {
@@ -143,29 +134,13 @@ const onTerminateSession = (socket, bot) => async ({ sessionId, // passed if the
143
134
  socket.leave(sessionId);
144
135
  await session.dispatch(engine_domain_1.Actions.guestDisconnected(sessionId));
145
136
  };
146
- const onStartChat = (socket, props, bot, clientName, environment, logger) => async ({ sessionIdToken, sessionId: oldSessionId, // optional, used for old clients
147
- userIdToken, querystrings, }) => {
137
+ const onStartChat = (socket, props, bot, clientName, environment, logger) => async ({ sessionId: sessionIdFromClient, userId: defaultUserId, querystrings, sessionIdToken, userIdToken, }) => {
148
138
  const locale = (0, extractLocale_1.default)(querystrings, socket.request.headers['accept-language']);
149
- let sessionId = verifySignedToken(sessionIdToken, props.sessionTokenSecret);
150
- let userId = verifySignedToken(userIdToken, props.sessionTokenSecret);
151
- // TODO: remove temporary code in future versions
152
- // this code is relevant for old clients
153
- if (!sessionId && oldSessionId) {
154
- logger.info(`Received an old session ID from client: ${oldSessionId}. It will be replaced with a new one.`);
155
- sessionId = oldSessionId;
156
- }
157
- if (!userId) {
158
- userId = (0, crypto_1.randomUUID)();
159
- }
160
- if (!sessionId || !userId) {
161
- logger.error(`Invalid session ID token received from client: ${sessionIdToken}`);
162
- return;
163
- }
164
139
  const sessionsCollection = bot.store.db.collection('sessions');
165
- const { sessionInfo: defaultSessionInfo, isNew } = await (0, requestSessionData_1.default)(sessionId, querystrings, sessionsCollection, clientName, props);
140
+ const { sessionId, sessionInfo: defaultSessionInfo, isNew, } = await (0, requestSessionData_1.default)(sessionIdFromClient || sessionIdToken, querystrings, sessionsCollection, clientName, props);
166
141
  // create a channel for each session
167
142
  socket.join(sessionId);
168
- const sessionInfo = await (0, createSessionInfo_1.default)(socket, clientName, environment, defaultSessionInfo, userId, locale, querystrings, props)();
143
+ const sessionInfo = await (0, createSessionInfo_1.default)(socket, clientName, environment, defaultSessionInfo, defaultUserId || userIdToken, locale, querystrings, props)();
169
144
  const session = await bot.createSession(sessionId, sessionInfo);
170
145
  sendConfigurationToClient(socket, props, bot, clientName);
171
146
  // sending persisted state to client
@@ -242,44 +217,4 @@ const sendConfigurationToClient = (socket, props, bot, clientName) => {
242
217
  });
243
218
  }
244
219
  };
245
- const buildCookieOptions = (clientName, bot) => {
246
- const isLocalhost = bot.webserver.baseUrl === 'http://localhost:3000';
247
- return {
248
- httpOnly: true, // 🔒 Not accessible via JS
249
- secure: !isLocalhost, // ✅ true only for HTTPS
250
- sameSite: 'none', // Important, if chat-widget runs under a different domain
251
- path: `/${clientName}`, // Path for the cookie
252
- };
253
- };
254
- /**
255
- * Generates a signed token.
256
- *
257
- * @param payload
258
- * @param secret
259
- * @returns token in the format "payload.signature"
260
- */
261
- const generateSignedToken = (payload, secret) => {
262
- const signature = (0, crypto_1.createHmac)('sha256', secret).update(payload).digest('hex');
263
- return `${payload}.${signature}`;
264
- };
265
- /**
266
- * Verifies a signed token.
267
- *
268
- * @param token in the format "payload.signature"
269
- * @param secret
270
- * @returns payload if the token is valid, otherwise null.
271
- */
272
- const verifySignedToken = (token, secret) => {
273
- if (!token)
274
- return null;
275
- const [payload, signature] = token.split('.');
276
- if (!payload || !signature)
277
- return null;
278
- const expectedSignature = (0, crypto_1.createHmac)('sha256', secret)
279
- .update(payload)
280
- .digest('hex');
281
- if (signature !== expectedSignature)
282
- return null;
283
- return payload;
284
- };
285
220
  exports.CLIENT_TYPE = 'webclient';
@@ -2,6 +2,7 @@ import type { SessionInfo } from '@botfabrik/engine-domain';
2
2
  import { type WebClientProps } from './index';
3
3
  import type { SessionInfoClientPayload, SessionInfoUserPayload } from './types';
4
4
  interface SessionData {
5
+ sessionId: string;
5
6
  sessionInfo: SessionInfo<SessionInfoClientPayload, SessionInfoUserPayload>;
6
7
  isNew: boolean;
7
8
  }
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const crypto_1 = require("crypto");
3
4
  const index_1 = require("./index");
4
5
  const requestSessionData = async (sessionId, querystrings, sessionsCollection, clientName, props) => {
5
6
  const baseQuery = {
@@ -21,13 +22,16 @@ const requestSessionData = async (sessionId, querystrings, sessionsCollection, c
21
22
  };
22
23
  }
23
24
  const sessionRecord = await sessionsCollection.findOne(findSessionRecordQuery, { _id: 1, sessionInfo: 1 });
25
+ let sesId;
24
26
  let sessionInfo;
25
27
  let isNew;
26
28
  if (sessionRecord) {
29
+ sesId = sessionRecord._id;
27
30
  sessionInfo = sessionRecord.sessionInfo;
28
31
  isNew = false;
29
32
  }
30
33
  else {
34
+ sesId = (0, crypto_1.randomUUID)();
31
35
  sessionInfo = {
32
36
  client: {},
33
37
  user: {},
@@ -36,6 +40,7 @@ const requestSessionData = async (sessionId, querystrings, sessionsCollection, c
36
40
  isNew = true;
37
41
  }
38
42
  return {
43
+ sessionId: sesId,
39
44
  sessionInfo,
40
45
  isNew,
41
46
  };
@@ -5,13 +5,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const globals_1 = require("@jest/globals");
7
7
  const requestSessionData_1 = __importDefault(require("./requestSessionData"));
8
+ globals_1.jest.mock('crypto', () => {
9
+ const original = globals_1.jest.requireActual('crypto');
10
+ return {
11
+ v4: globals_1.jest.fn(() => 'uuid'),
12
+ ...original,
13
+ randomUUID: globals_1.jest.fn(() => 'some-uuid'),
14
+ };
15
+ });
8
16
  (0, globals_1.describe)('request session id', () => {
9
17
  (0, globals_1.beforeEach)(() => {
10
18
  globals_1.jest.clearAllMocks();
11
19
  });
12
- const props = {
13
- sessionTokenSecret: 'some-secret',
14
- };
15
20
  const querystrings = {
16
21
  accessToken: 'access-token',
17
22
  };
@@ -21,13 +26,14 @@ const requestSessionData_1 = __importDefault(require("./requestSessionData"));
21
26
  const sessionsCollection = {
22
27
  findOne,
23
28
  };
24
- const { isNew } = await (0, requestSessionData_1.default)('session-id', querystrings, sessionsCollection, 'test-bot', props);
29
+ const { sessionId, isNew } = await (0, requestSessionData_1.default)('session-id', querystrings, sessionsCollection, 'test-bot', {});
25
30
  (0, globals_1.expect)(findOne).toHaveBeenCalledTimes(1);
26
31
  (0, globals_1.expect)(findOne).toHaveBeenCalledWith({
27
32
  _id: 'session-id',
28
33
  'sessionInfo.client.name': 'test-bot',
29
34
  'sessionInfo.client.type': 'webclient',
30
35
  }, { _id: 1, sessionInfo: 1 });
36
+ (0, globals_1.expect)(sessionId).toBe('some-uuid');
31
37
  (0, globals_1.expect)(isNew).toBe(true);
32
38
  });
33
39
  (0, globals_1.it)('when sessionId has been passed by query param and exists in db', async () => {
@@ -36,13 +42,14 @@ const requestSessionData_1 = __importDefault(require("./requestSessionData"));
36
42
  const sessionsCollection = {
37
43
  findOne,
38
44
  };
39
- const { isNew } = await (0, requestSessionData_1.default)('session-id', querystrings, sessionsCollection, 'test-bot', props);
45
+ const { sessionId, isNew } = await (0, requestSessionData_1.default)('session-id', querystrings, sessionsCollection, 'test-bot', {});
40
46
  (0, globals_1.expect)(findOne).toHaveBeenCalledTimes(1);
41
47
  (0, globals_1.expect)(findOne).toHaveBeenCalledWith({
42
48
  _id: 'session-id',
43
49
  'sessionInfo.client.name': 'test-bot',
44
50
  'sessionInfo.client.type': 'webclient',
45
51
  }, { _id: 1, sessionInfo: 1 });
52
+ (0, globals_1.expect)(sessionId).toBe('session-id');
46
53
  (0, globals_1.expect)(isNew).toBe(false);
47
54
  });
48
55
  (0, globals_1.it)('when requestSessionRecordQuery has been passed as webclient property but no such session exists in db', async () => {
@@ -54,10 +61,9 @@ const requestSessionData_1 = __importDefault(require("./requestSessionData"));
54
61
  const requestSessionRecordQuery = globals_1.jest.fn();
55
62
  const props = {
56
63
  requestSessionRecordQuery,
57
- sessionTokenSecret: 'some-secret',
58
64
  };
59
65
  requestSessionRecordQuery.mockReturnValueOnce(Promise.resolve({ 'sessionInfo.user.id': 'hans@apptiva.ch' }));
60
- const { isNew } = await (0, requestSessionData_1.default)('session-id', querystrings, sessionsCollection, 'test-bot', props);
66
+ const { sessionId, isNew } = await (0, requestSessionData_1.default)('session-id', querystrings, sessionsCollection, 'test-bot', props);
61
67
  (0, globals_1.expect)(requestSessionRecordQuery).toHaveBeenCalledTimes(1);
62
68
  (0, globals_1.expect)(requestSessionRecordQuery).toHaveBeenCalledWith({
63
69
  querystrings,
@@ -69,6 +75,7 @@ const requestSessionData_1 = __importDefault(require("./requestSessionData"));
69
75
  'sessionInfo.client.type': 'webclient',
70
76
  'sessionInfo.user.id': 'hans@apptiva.ch',
71
77
  }, { _id: 1, sessionInfo: 1 });
78
+ (0, globals_1.expect)(sessionId).toBe('some-uuid');
72
79
  (0, globals_1.expect)(isNew).toBe(true);
73
80
  });
74
81
  (0, globals_1.it)('when requestSessionRecordQuery has been passed as webclient property and session exists in db', async () => {
@@ -80,10 +87,9 @@ const requestSessionData_1 = __importDefault(require("./requestSessionData"));
80
87
  const requestSessionRecordQuery = globals_1.jest.fn();
81
88
  const props = {
82
89
  requestSessionRecordQuery,
83
- sessionTokenSecret: 'some-secret',
84
90
  };
85
91
  requestSessionRecordQuery.mockReturnValueOnce(Promise.resolve({ 'sessionInfo.user.id': 'hans@apptiva.ch' }));
86
- const { isNew } = await (0, requestSessionData_1.default)('session-id', querystrings, sessionsCollection, 'test-bot', props);
92
+ const { sessionId, isNew } = await (0, requestSessionData_1.default)('session-id', querystrings, sessionsCollection, 'test-bot', props);
87
93
  (0, globals_1.expect)(requestSessionRecordQuery).toHaveBeenCalledTimes(1);
88
94
  (0, globals_1.expect)(requestSessionRecordQuery).toHaveBeenCalledWith({
89
95
  querystrings,
@@ -95,6 +101,7 @@ const requestSessionData_1 = __importDefault(require("./requestSessionData"));
95
101
  'sessionInfo.client.type': 'webclient',
96
102
  'sessionInfo.user.id': 'hans@apptiva.ch',
97
103
  }, { _id: 1, sessionInfo: 1 });
104
+ (0, globals_1.expect)(sessionId).toBe('session-id');
98
105
  (0, globals_1.expect)(isNew).toBe(false);
99
106
  });
100
107
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botfabrik/engine-webclient",
3
- "version": "4.93.2",
3
+ "version": "4.93.3",
4
4
  "description": "Webclient for Botfabriks Bot Engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -25,13 +25,11 @@
25
25
  "@botfabrik/engine-utils": "^4.90.6",
26
26
  "@google-cloud/speech": "^7.1.0",
27
27
  "accept-language-parser": "^1.5.0",
28
- "cookie-parser": "^1.4.7",
29
28
  "express": "^5.1.0",
30
29
  "uuid": "^11.1.0"
31
30
  },
32
31
  "devDependencies": {
33
32
  "@types/accept-language-parser": "^1.5.8",
34
- "@types/cookie-parser": "^1.4.9",
35
33
  "@types/express": "^5.0.3",
36
34
  "@types/serve-static": "^1.15.8",
37
35
  "@types/ua-parser-js": "^0.7.39",
@@ -41,5 +39,5 @@
41
39
  "tsx": "^4.20.1",
42
40
  "typescript": "5.8.3"
43
41
  },
44
- "gitHead": "f148c84858fbaa4b025af8c3cc536069fd8e3ead"
42
+ "gitHead": "0a885ea70adab1a92106546b0cbca1ec86d7c368"
45
43
  }