@tiledesk/tiledesk-server 2.20.0 → 2.20.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@
5
5
  🚀 IN PRODUCTION 🚀
6
6
  (https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
7
7
 
8
+ # 2.21.0
9
+ - Addressed and resolved a security vulnerability
10
+
11
+ # 2.20.1
12
+ - Fixed a security vulnerability on AI-related routes
13
+ - Added authentication to `/openai` and `/url-preview` routes
14
+ - Improved error response sanitization for LLM routes
15
+ - Fixed cross-tenant access checks on integration routes
16
+ - Disabled unused `/kbsettings` route
17
+
8
18
  # 2.20.0
9
19
  - Updated tybot-connector to 2.1.8
10
20
  - Improved flow attributes management
package/app.js CHANGED
@@ -639,15 +639,16 @@ app.use('/:projectid/segments',[passport.authenticate(['basic', 'jwt'], { sessio
639
639
  app.use('/:projectid/tables', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], dataTable);
640
640
 
641
641
  app.use('/:projectid/llm', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], llm);
642
- app.use('/:projectid/openai', openai);
643
- app.use('/:projectid/url-preview', urlPreview);
642
+ app.use('/:projectid/openai', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], openai);
643
+ app.use('/:projectid/url-preview', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('guest', ['bot','subscription'])], urlPreview);
644
644
  app.use('/:projectid/quotes', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('agent', ['bot','subscription'])], quotes)
645
645
 
646
646
  app.use('/:projectid/integration', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], integration )
647
647
 
648
648
  app.use('/:projectid/mcp', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], mcp);
649
649
 
650
- app.use('/:projectid/kbsettings', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('agent', ['bot','subscription'])], kbsettings);
650
+ // Disabled: superseded by /kb routes. Kept commented with gptkey stripped from responses.
651
+ // app.use('/:projectid/kbsettings', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('agent', ['bot','subscription'])], kbsettings);
651
652
  app.use('/:projectid/kb/unanswered', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], unanswered);
652
653
  app.use('/:projectid/kb/answered', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], answered);
653
654
  app.use('/:projectid/kb', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], kb);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiledesk/tiledesk-server",
3
3
  "description": "The Tiledesk server module",
4
- "version": "2.20.0",
4
+ "version": "2.20.2",
5
5
  "scripts": {
6
6
  "start": "node ./bin/www",
7
7
  "pretest": "mongodb-runner start",
@@ -49,7 +49,7 @@
49
49
  "@tiledesk/tiledesk-rasa-connector": "^1.0.10",
50
50
  "@tiledesk/tiledesk-sms-connector": "^0.1.13",
51
51
  "@tiledesk/tiledesk-telegram-connector": "^0.1.14",
52
- "@tiledesk/tiledesk-tybot-connector": "^2.1.8",
52
+ "@tiledesk/tiledesk-tybot-connector": "^2.1.10",
53
53
  "@tiledesk/tiledesk-voice-twilio-connector": "^0.3.2",
54
54
  "@tiledesk/tiledesk-vxml-connector": "^0.1.91",
55
55
  "@tiledesk/tiledesk-whatsapp-connector": "1.0.26",
@@ -5,6 +5,11 @@ let Integration = require('../models/integrations');
5
5
  const cacheEnabler = require('../services/cacheEnabler');
6
6
  var cacheUtil = require('../utils/cacheUtil');
7
7
  const integrationEvent = require('../event/integrationEvent');
8
+ const {
9
+ mergeIntegrationValue,
10
+ toPublicIntegration,
11
+ toPublicIntegrations
12
+ } = require('../utils/maskIntegrationSecrets');
8
13
 
9
14
 
10
15
  // Get all integration for a project id
@@ -24,7 +29,7 @@ router.get('/', async (req, res) => {
24
29
  winston.error("Error getting integrations: ", err);
25
30
  return res.status(500).send({ success: false, message: "Error getting integrations "});
26
31
  }
27
- res.status(200).send(integrations);
32
+ res.status(200).send(toPublicIntegrations(integrations));
28
33
  })
29
34
  // without cache
30
35
  // Integration.find({ id_project: id_project }, (err, integrations) => {
@@ -41,14 +46,18 @@ router.get('/', async (req, res) => {
41
46
  router.get('/:integration_id', async (req, res) => {
42
47
 
43
48
  let integration_id = req.params.integration_id;
49
+ let id_project = req.projectid;
44
50
  winston.debug("Get integration with id " + integration_id);
45
51
 
46
- Integration.findById(integration_id, (err, integration) => {
52
+ Integration.findOne({ _id: integration_id, id_project: id_project }, (err, integration) => {
47
53
  if (err) {
48
54
  winston.error("Error find integration by id: ", err);
49
55
  return res.status(404).send({ success: false, err: err });
50
56
  }
51
- res.status(200).send(integration);
57
+ if (!integration) {
58
+ return res.status(404).send({ success: false, message: "Integration not found" });
59
+ }
60
+ res.status(200).send(toPublicIntegration(integration));
52
61
  })
53
62
 
54
63
  })
@@ -72,7 +81,7 @@ router.get('/name/:integration_name', async (req, res) => {
72
81
  return res.status(200).send("Integration not found");
73
82
  }
74
83
 
75
- res.status(200).send(integration);
84
+ res.status(200).send(toPublicIntegration(integration));
76
85
  })
77
86
  })
78
87
 
@@ -82,13 +91,22 @@ router.post('/', async (req, res) => {
82
91
  let id_project = req.projectid;
83
92
  winston.debug("Add new integration ", req.body);
84
93
 
94
+ let existingIntegration;
95
+ try {
96
+ existingIntegration = await Integration.findOne({ id_project: id_project, name: req.body.name });
97
+ } catch (err) {
98
+ winston.error("Error finding existing integration ", err);
99
+ return res.status(500).send({ success: false, err: err });
100
+ }
85
101
 
86
102
  let newIntegration = {
87
103
  id_project: id_project,
88
104
  name: req.body.name
89
105
  }
90
- if (req.body.value) {
91
- newIntegration.value = req.body.value;
106
+ if (req.body.value !== undefined) {
107
+ newIntegration.value = existingIntegration?.value
108
+ ? mergeIntegrationValue(req.body.value, existingIntegration.value)
109
+ : req.body.value;
92
110
  }
93
111
 
94
112
  Integration.findOneAndUpdate({ id_project: id_project, name: req.body.name }, newIntegration, { new: true, upsert: true, setDefaultsOnInsert: false}, (err, savedIntegration) => {
@@ -107,7 +125,7 @@ router.post('/', async (req, res) => {
107
125
  }
108
126
  })
109
127
 
110
- res.status(200).send(savedIntegration);
128
+ res.status(200).send(toPublicIntegration(savedIntegration));
111
129
  })
112
130
 
113
131
  // let newIntegration = new Integration({
@@ -144,19 +162,35 @@ router.put('/:integration_id', async (req, res) => {
144
162
 
145
163
  let id_project = req.projectid;
146
164
  let integration_id = req.params.integration_id;
165
+
166
+ let existingIntegration;
167
+ try {
168
+ existingIntegration = await Integration.findOne({ _id: integration_id, id_project: id_project });
169
+ } catch (err) {
170
+ winston.error("Error finding integration to update ", err);
171
+ return res.status(500).send({ success: false, error: err });
172
+ }
173
+ if (!existingIntegration) {
174
+ return res.status(404).send({ success: false, message: "Integration not found" });
175
+ }
147
176
 
148
177
  let update = {};
149
178
  if (req.body.name != undefined) {
150
179
  update.name = req.body.name;
151
180
  }
152
181
  if (req.body.value != undefined) {
153
- update.value = req.body.value
182
+ update.value = existingIntegration.value
183
+ ? mergeIntegrationValue(req.body.value, existingIntegration.value)
184
+ : req.body.value;
154
185
  }
155
186
 
156
- Integration.findByIdAndUpdate(integration_id, update, { new: true, upsert: true }, (err, savedIntegration) => {
187
+ Integration.findOneAndUpdate({ _id: integration_id, id_project: id_project }, update, { new: true }, (err, savedIntegration) => {
157
188
  if (err) {
158
189
  winston.error("Error find by id and update integration: ", err);
159
- return res.status({ success: false, error: err })
190
+ return res.status(500).send({ success: false, error: err })
191
+ }
192
+ if (!savedIntegration) {
193
+ return res.status(404).send({ success: false, message: "Integration not found" });
160
194
  }
161
195
 
162
196
  Integration.find({ id_project: id_project }, (err, integrations) => {
@@ -167,7 +201,7 @@ router.put('/:integration_id', async (req, res) => {
167
201
  }
168
202
  })
169
203
 
170
- res.status(200).send(savedIntegration);
204
+ res.status(200).send(toPublicIntegration(savedIntegration));
171
205
  })
172
206
  })
173
207
 
@@ -176,10 +210,13 @@ router.delete('/:integration_id', async (req, res) => {
176
210
  let id_project = req.projectid;
177
211
  let integration_id = req.params.integration_id;
178
212
 
179
- Integration.findByIdAndDelete(integration_id, (err, result) => {
213
+ Integration.findOneAndDelete({ _id: integration_id, id_project: id_project }, (err, result) => {
180
214
  if (err) {
181
215
  winston.error("Error find by id and delete integration: ", err);
182
- return res.status({ success: false, error: err })
216
+ return res.status(500).send({ success: false, error: err })
217
+ }
218
+ if (!result) {
219
+ return res.status(404).send({ success: false, message: "Integration not found" });
183
220
  }
184
221
 
185
222
  Integration.find({ id_project: id_project }, (err, integrations) => {
@@ -1,215 +1,185 @@
1
1
  var express = require('express');
2
- var { KBSettings } = require('../models/kb_setting');
3
- // var { KB } = require('../models/kb_setting');
4
- // var KB = require('../models/kb_setting')
5
2
  var router = express.Router();
6
- var winston = require('../config/winston');
7
- const aiService = require('../services/aiService');
8
3
 
9
- router.get('/', async (req, res) => {
10
- let project_id = req.projectid;
11
-
12
- KBSettings.findOne({ id_project: project_id }, (err, kb_setting) => {
13
- if (err) {
14
- winston.error("find knoledge base settings error: ", err);
15
- return res.status(500).send({ success: false, error: err });
16
- }
17
- else if (!kb_setting) {
18
- // Automatically creates the kb settings object if it does not exist
19
- let new_settings = new KBSettings({
20
- id_project: req.projectid
21
- })
22
- new_settings.save(function (err, savedKbSettings) {
23
- if (err) {
24
- winston.error("save new kbs error: ", err);
25
- return res.status(500).send({ success: false, error: err});
26
- } else {
27
- return res.status(200).send(savedKbSettings);
28
- }
29
- })
30
- }
31
- else {
32
- return res.status(200).send(kb_setting)
33
- }
34
- })
35
- })
36
-
37
-
38
- // Never used?
39
- router.post('/', async (req, res) => {
40
-
41
- let body = req.body;
42
-
43
- let new_settings = new KBSettings({
44
- id_project: req.projectid,
45
- // gptkey: body.gptkey
46
- // others params are set with default values
47
- })
48
-
49
- new_settings.save(function (err, savedKbSettings) {
50
- if (err) {
51
- winston.error("save new kbs error: ", err);
52
- return res.status(500).send({ success: false, error: err});
53
- } else {
54
- return res.status(200).send(savedKbSettings);
55
- }
56
- })
57
- })
58
-
59
- router.put('/:settings_id', async (req, res) => {
60
-
61
- let settings_id = req.params.settings_id;
62
- let update = {};
63
-
64
- if (req.body.gptkey != undefined) {
65
- update.gptkey = req.body.gptkey
4
+ // Route disabled — see app.js (/kbsettings superseded by /kb).
5
+ // Never expose gptkey in HTTP responses.
6
+ function stripGptkey(kb_setting) {
7
+ if (!kb_setting) {
8
+ return kb_setting;
66
9
  }
67
- // check if it is eligible
68
- // if (req.body.maxKbsNumber != undefined) {
69
- // update.maxKbsNumber = req.body.maxKbsNumber
70
- // }
71
- // if (req.body.maxPagesNumber != undefined) {
72
- // update.maxPagesNumber = req.body.maxPagesNumber
73
- // }
74
-
75
- winston.debug("update: ", update);
76
- KBSettings.findByIdAndUpdate(settings_id, update, { new: true }, (err, settings) => {
77
- if (err) {
78
- winston.error("findByIdAndUpdate error: ", err);
79
- res.status(500).send({ success: false, error: err });
80
- } else {
81
- res.status(200).send(settings);
82
- }
83
- })
84
- })
85
-
86
- router.delete('/:settings_id/:kb_id', async (req, res) => {
87
-
88
- let settings_id = req.params.settings_id;
89
- let kb_id = req.params.kb_id;
90
-
91
- KBSettings.findByIdAndUpdate(settings_id, { $pull: { kbs: { _id: kb_id }}}, { new: true}, (err, settings) => {
92
- if (err) {
93
- winston.error("delete kb from list error: ", err);
94
- res.status(500).send({ success: false, error: err });
95
- } else {
96
- res.status(200).send(settings);
97
- }
98
- })
99
-
100
- })
101
-
102
-
103
- // PROXY PUGLIA AI - START
104
- router.post('/qa', async (req, res) => {
105
- let data = req.body;
106
- winston.debug("/qa data: ", data);
107
-
108
- aiService.ask(data).then((resp) => {
109
- winston.debug("qa resp: ", resp.data);
110
- res.status(200).send(resp.data);
111
- }).catch((err) => {
112
- winston.error("qa err: ", err);
113
- //let status = err.response.status;
114
- //res.status(status).send({ statusText: err.response.statusText, detail: err.response.data.detail });
115
- res.status(400).send({ success: false, error: err });
116
- })
117
- })
118
-
119
- router.post('/startscrape', async (req, res) => {
120
-
121
- let data = req.body;
122
- winston.debug("/startscrape data: ", data);
123
-
124
- aiService.startScrape(data).then((resp) => {
125
- winston.debug("startScrape resp: ", resp.data);
126
- res.status(200).send(resp.data);
127
- }).catch((err) => {
128
- winston.error("startScrape err: ", err);
129
- // let status = err.response.status;
130
- // res.status(status).send({ statusText: err.response.statusText, detail: err.response.data.detail });
131
- res.status(400).send({ success: false, error: err });
132
- })
133
- })
134
-
135
-
136
- router.post('/checkstatus', async (req, res) => {
137
-
138
- //let data = req.body;
139
- winston.debug("/checkstatus req.body: ", req.body);
140
-
141
- let full_url = req.body.full_url;
142
- let data = {
143
- url_list: [ req.body.full_url ]
144
- }
145
-
146
- aiService.checkStatus(data).then((resp) => {
147
- winston.debug("checkStatus resp: ", resp);
148
- winston.debug("checkStatus resp: ", resp.data);
149
- winston.debug("checkStatus resp: ", resp.data[full_url]);
150
-
151
- let response = resp.data[full_url];
152
-
153
- let return_data = {
154
- status_message: response.status_message
155
- }
156
-
157
- if (response.status_code === 3) {
158
- return_data.status_code = 2;
159
- }
160
-
161
- if (response.status_code === 1 || response.status_code === 2 ) {
162
- return_data.status_code = 1;
163
- }
164
-
165
- if (response.status_code === 0) {
166
- return_data.status_code = 0;
167
- }
168
-
169
- if (!response.status_code) {
170
- return_data.status_code = 0;
171
- }
172
-
173
-
174
- res.status(200).send(return_data);
175
- }).catch((err) => {
176
- //winston.error("checkstatus err: ", err);
177
-
178
- // let status = err.response?.status;
179
- // res.status(status).send({ statusText: err.response?.statusText, detail: err.response?.data?.detail });
180
- res.status(400).send({ success: false, error: err });
181
- })
182
- })
183
- // PROXY PUGLIA AI - END
184
-
185
- router.post('/:settings_id', async (req, res) => {
186
-
187
- let settings_id = req.params.settings_id;
188
- let body = req.body;
189
-
190
- KBSettings.findById(settings_id, (err, settings) => {
191
- if (err) {
192
- winston.error("find knoledge base error: ", err);
193
- return res.status(500).send({ success: false, error: err});
194
- } else {
195
-
196
- let new_kb = {
197
- name: body.name,
198
- url: body.url
199
- }
200
- settings.kbs.push(new_kb);
201
-
202
- KBSettings.findByIdAndUpdate( settings_id, settings, { new: true }, (err, savedSettings) => {
203
- if (err) {
204
- winston.err("findByIdAndUpdate error: ", err);
205
- res.status(500).send({ success: false, error: err });
206
- } else {
207
- res.status(200).send(savedSettings);
208
- }
209
- })
210
- }
211
- })
212
- })
213
-
214
-
215
- module.exports = router;
10
+ const obj = typeof kb_setting.toObject === 'function' ? kb_setting.toObject() : { ...kb_setting };
11
+ delete obj.gptkey;
12
+ return obj;
13
+ }
14
+
15
+ // var { KBSettings } = require('../models/kb_setting');
16
+ // var winston = require('../config/winston');
17
+ // const aiService = require('../services/aiService');
18
+ // const { sanitizeAxiosError, axiosErrorStatus } = require('../utils/sanitizeAxiosError');
19
+ //
20
+ // router.get('/', async (req, res) => {
21
+ // let project_id = req.projectid;
22
+ //
23
+ // KBSettings.findOne({ id_project: project_id }, (err, kb_setting) => {
24
+ // if (err) {
25
+ // winston.error("find knoledge base settings error: ", err);
26
+ // return res.status(500).send({ success: false, error: err });
27
+ // }
28
+ // else if (!kb_setting) {
29
+ // let new_settings = new KBSettings({
30
+ // id_project: req.projectid
31
+ // })
32
+ // new_settings.save(function (err, savedKbSettings) {
33
+ // if (err) {
34
+ // winston.error("save new kbs error: ", err);
35
+ // return res.status(500).send({ success: false, error: err});
36
+ // } else {
37
+ // return res.status(200).send(stripGptkey(savedKbSettings));
38
+ // }
39
+ // })
40
+ // }
41
+ // else {
42
+ // return res.status(200).send(stripGptkey(kb_setting))
43
+ // }
44
+ // })
45
+ // })
46
+ //
47
+ // router.post('/', async (req, res) => {
48
+ // let new_settings = new KBSettings({
49
+ // id_project: req.projectid
50
+ // })
51
+ //
52
+ // new_settings.save(function (err, savedKbSettings) {
53
+ // if (err) {
54
+ // winston.error("save new kbs error: ", err);
55
+ // return res.status(500).send({ success: false, error: err});
56
+ // } else {
57
+ // return res.status(200).send(stripGptkey(savedKbSettings));
58
+ // }
59
+ // })
60
+ // })
61
+ //
62
+ // router.put('/:settings_id', async (req, res) => {
63
+ // let settings_id = req.params.settings_id;
64
+ // let update = {};
65
+ //
66
+ // if (req.body.gptkey != undefined) {
67
+ // update.gptkey = req.body.gptkey
68
+ // }
69
+ //
70
+ // winston.debug("update: ", update);
71
+ // KBSettings.findByIdAndUpdate(settings_id, update, { new: true }, (err, settings) => {
72
+ // if (err) {
73
+ // winston.error("findByIdAndUpdate error: ", err);
74
+ // res.status(500).send({ success: false, error: err });
75
+ // } else {
76
+ // res.status(200).send(stripGptkey(settings));
77
+ // }
78
+ // })
79
+ // })
80
+ //
81
+ // router.delete('/:settings_id/:kb_id', async (req, res) => {
82
+ // let settings_id = req.params.settings_id;
83
+ // let kb_id = req.params.kb_id;
84
+ //
85
+ // KBSettings.findByIdAndUpdate(settings_id, { $pull: { kbs: { _id: kb_id }}}, { new: true}, (err, settings) => {
86
+ // if (err) {
87
+ // winston.error("delete kb from list error: ", err);
88
+ // res.status(500).send({ success: false, error: err });
89
+ // } else {
90
+ // res.status(200).send(stripGptkey(settings));
91
+ // }
92
+ // })
93
+ // })
94
+ //
95
+ // router.post('/qa', async (req, res) => {
96
+ // let data = req.body;
97
+ // winston.debug("/qa data: ", data);
98
+ //
99
+ // aiService.ask(data).then((resp) => {
100
+ // winston.debug("qa resp: ", resp.data);
101
+ // res.status(200).send(resp.data);
102
+ // }).catch((err) => {
103
+ // winston.error("qa err: ", sanitizeAxiosError(err));
104
+ // res.status(axiosErrorStatus(err, 400)).send({ success: false, error: sanitizeAxiosError(err) });
105
+ // })
106
+ // })
107
+ //
108
+ // router.post('/startscrape', async (req, res) => {
109
+ // let data = req.body;
110
+ // winston.debug("/startscrape data: ", data);
111
+ //
112
+ // aiService.startScrape(data).then((resp) => {
113
+ // winston.debug("startScrape resp: ", resp.data);
114
+ // res.status(200).send(resp.data);
115
+ // }).catch((err) => {
116
+ // winston.error("startScrape err: ", sanitizeAxiosError(err));
117
+ // res.status(axiosErrorStatus(err, 400)).send({ success: false, error: sanitizeAxiosError(err) });
118
+ // })
119
+ // })
120
+ //
121
+ // router.post('/checkstatus', async (req, res) => {
122
+ // winston.debug("/checkstatus req.body: ", req.body);
123
+ //
124
+ // let full_url = req.body.full_url;
125
+ // let data = {
126
+ // url_list: [ req.body.full_url ]
127
+ // }
128
+ //
129
+ // aiService.checkStatus(data).then((resp) => {
130
+ // let response = resp.data[full_url];
131
+ //
132
+ // let return_data = {
133
+ // status_message: response.status_message
134
+ // }
135
+ //
136
+ // if (response.status_code === 3) {
137
+ // return_data.status_code = 2;
138
+ // }
139
+ //
140
+ // if (response.status_code === 1 || response.status_code === 2 ) {
141
+ // return_data.status_code = 1;
142
+ // }
143
+ //
144
+ // if (response.status_code === 0) {
145
+ // return_data.status_code = 0;
146
+ // }
147
+ //
148
+ // if (!response.status_code) {
149
+ // return_data.status_code = 0;
150
+ // }
151
+ //
152
+ // res.status(200).send(return_data);
153
+ // }).catch((err) => {
154
+ // res.status(axiosErrorStatus(err, 400)).send({ success: false, error: sanitizeAxiosError(err) });
155
+ // })
156
+ // })
157
+ //
158
+ // router.post('/:settings_id', async (req, res) => {
159
+ // let settings_id = req.params.settings_id;
160
+ // let body = req.body;
161
+ //
162
+ // KBSettings.findById(settings_id, (err, settings) => {
163
+ // if (err) {
164
+ // winston.error("find knoledge base error: ", err);
165
+ // return res.status(500).send({ success: false, error: err});
166
+ // } else {
167
+ // let new_kb = {
168
+ // name: body.name,
169
+ // url: body.url
170
+ // }
171
+ // settings.kbs.push(new_kb);
172
+ //
173
+ // KBSettings.findByIdAndUpdate( settings_id, settings, { new: true }, (err, savedSettings) => {
174
+ // if (err) {
175
+ // winston.err("findByIdAndUpdate error: ", err);
176
+ // res.status(500).send({ success: false, error: err });
177
+ // } else {
178
+ // res.status(200).send(stripGptkey(savedSettings));
179
+ // }
180
+ // })
181
+ // }
182
+ // })
183
+ // })
184
+
185
+ module.exports = router;
package/routes/llm.js CHANGED
@@ -7,6 +7,7 @@ const aiManager = require('../services/aiManager');
7
7
  const multer = require('multer');
8
8
  const fileUtils = require('../utils/fileUtils');
9
9
  const { MODELS_MULTIPLIER } = require('../utils/aiUtils');
10
+ const { sanitizeAxiosError, axiosErrorStatus } = require('../utils/sanitizeAxiosError');
10
11
 
11
12
  let MAX_UPLOAD_FILE_SIZE = process.env.MAX_UPLOAD_FILE_SIZE;
12
13
  let uploadlimits = undefined;
@@ -133,9 +134,9 @@ router.post('/preview', async (req, res) => {
133
134
  } else if (err.response?.data?.detail?.answer) {
134
135
  res.status(400).send({ success: false, error: err.response.data.detail.answer, detail: err.response.data.detail });
135
136
  } else if (err.response?.data) {
136
- res.status(500).send({ success: false, error: err.response.data });
137
+ res.status(axiosErrorStatus(err)).send({ success: false, error: sanitizeAxiosError(err) });
137
138
  } else {
138
- res.status(500).send({ success: false, error: err });
139
+ res.status(axiosErrorStatus(err)).send({ success: false, error: sanitizeAxiosError(err) });
139
140
  }
140
141
  })
141
142
 
@@ -174,8 +175,8 @@ router.post('/transcription', upload.single('uploadFile'), async (req, res) => {
174
175
  winston.verbose("Transcript response: ", response.data);
175
176
  res.status(200).send({ text: response.data.text});
176
177
  }).catch((err) => {
177
- winston.error("err: ", err.response?.data)
178
- res.status(500).send({ success: false, error: err });
178
+ winston.error("transcription error: ", sanitizeAxiosError(err));
179
+ res.status(axiosErrorStatus(err)).send({ success: false, error: sanitizeAxiosError(err) });
179
180
  })
180
181
 
181
182
  })
package/routes/openai.js CHANGED
@@ -1,90 +1,92 @@
1
1
  var express = require('express');
2
2
  var router = express.Router();
3
- var { KBSettings } = require('../models/kb_setting');
4
- var aiService = require('../services/aiService');
5
- var winston = require('../config/winston');
6
- const { QuoteManager } = require('../services/QuoteManager');
7
- const { MODELS_MULTIPLIER } = require('../utils/aiUtils');
8
3
 
9
- router.post('/', async (req, res) => {
10
-
11
- let project_id = req.projectid;
12
- let body = req.body;
13
- let usePublicKey = false;
14
- let publicKey = process.env.GPTKEY;
15
- let gptkey = null;
16
- let obj = { createdAt: new Date() };
17
- let quoteManager = req.app.get('quote_manager');
18
-
19
- KBSettings.findOne({ id_project: project_id }, async (err, kbSettings) => {
20
-
21
- if (err) {
22
- usePublicKey = true;
23
- gptkey = publicKey;
24
- }
25
-
26
- if (kbSettings && kbSettings.gptkey) {
27
- gptkey = kbSettings.gptkey;
28
- } else {
29
- usePublicKey = true;
30
- gptkey = publicKey;
31
- }
32
-
33
- if (!gptkey) {
34
- return res.status(400).send({ success: false, message: "Missing gptkey parameter" });
35
- }
36
-
37
- if (usePublicKey === true) {
38
- let isAvailable = await quoteManager.checkQuote(req.project, obj, 'tokens');
39
- if (isAvailable === false) {
40
- return res.status(403).send({ success: false, message: "Tokens quota exceeded", error_code: 13001})
41
- }
42
- }
43
-
44
-
45
- let json = {
46
- model: body.model,
47
- messages: [
48
- { role: "user", content: body.question }
49
- ],
50
- max_tokens: body.max_tokens,
51
- temperature: body.temperature
52
- }
53
-
54
- let message = { role: "", content: "" };
55
- if (body.context) {
56
- message.role = "system";
57
- message.content = body.context;
58
- json.messages.unshift(message);
59
- }
60
-
61
- if (body.formatType && body.formatType !== 'none') {
62
- json.response_format = {
63
- type: body.formatType
64
- }
65
- }
66
-
67
- let multiplier = MODELS_MULTIPLIER[json.model];
68
- if (!multiplier) {
69
- multiplier = 1;
70
- winston.info("No multiplier found for AI model (openai) " + json.model)
71
- }
72
-
73
- aiService.completions(json, gptkey).then(async (response) => {
74
- let data = { createdAt: new Date(), tokens: response.data.usage.total_tokens, multiplier: multiplier }
75
- if (usePublicKey === true) {
76
- let incremented_key = await quoteManager.incrementTokenCount(req.project, data);
77
- winston.verbose("Tokens quota incremented for key " + incremented_key);
78
- }
79
-
80
- res.status(200).send(response.data);
81
-
82
- }).catch((err) => {
83
- // winston.error("completions error: ", err);
84
- res.status(500).send(err)
85
- })
86
- })
87
- })
4
+ // Disabled: POST / was unused (only Design Studio GPT Task preview, now deprecated).
5
+ // Kept commented with sanitized error handling for potential future re-enablement.
6
+ // var { KBSettings } = require('../models/kb_setting');
7
+ // var aiService = require('../services/aiService');
8
+ // const { MODELS_MULTIPLIER } = require('../utils/aiUtils');
9
+ // const { sanitizeAxiosError, axiosErrorStatus } = require('../utils/sanitizeAxiosError');
10
+ //
11
+ // router.post('/', async (req, res) => {
12
+ //
13
+ // let project_id = req.projectid;
14
+ // let body = req.body;
15
+ // let usePublicKey = false;
16
+ // let publicKey = process.env.GPTKEY;
17
+ // let gptkey = null;
18
+ // let obj = { createdAt: new Date() };
19
+ // let quoteManager = req.app.get('quote_manager');
20
+ //
21
+ // KBSettings.findOne({ id_project: project_id }, async (err, kbSettings) => {
22
+ //
23
+ // if (err) {
24
+ // usePublicKey = true;
25
+ // gptkey = publicKey;
26
+ // }
27
+ //
28
+ // if (kbSettings && kbSettings.gptkey) {
29
+ // gptkey = kbSettings.gptkey;
30
+ // } else {
31
+ // usePublicKey = true;
32
+ // gptkey = publicKey;
33
+ // }
34
+ //
35
+ // if (!gptkey) {
36
+ // return res.status(400).send({ success: false, message: "Missing gptkey parameter" });
37
+ // }
38
+ //
39
+ // if (usePublicKey === true) {
40
+ // let isAvailable = await quoteManager.checkQuote(req.project, obj, 'tokens');
41
+ // if (isAvailable === false) {
42
+ // return res.status(403).send({ success: false, message: "Tokens quota exceeded", error_code: 13001})
43
+ // }
44
+ // }
45
+ //
46
+ //
47
+ // let json = {
48
+ // model: body.model,
49
+ // messages: [
50
+ // { role: "user", content: body.question }
51
+ // ],
52
+ // max_tokens: body.max_tokens,
53
+ // temperature: body.temperature
54
+ // }
55
+ //
56
+ // let message = { role: "", content: "" };
57
+ // if (body.context) {
58
+ // message.role = "system";
59
+ // message.content = body.context;
60
+ // json.messages.unshift(message);
61
+ // }
62
+ //
63
+ // if (body.formatType && body.formatType !== 'none') {
64
+ // json.response_format = {
65
+ // type: body.formatType
66
+ // }
67
+ // }
68
+ //
69
+ // let multiplier = MODELS_MULTIPLIER[json.model];
70
+ // if (!multiplier) {
71
+ // multiplier = 1;
72
+ // winston.info("No multiplier found for AI model (openai) " + json.model)
73
+ // }
74
+ //
75
+ // aiService.completions(json, gptkey).then(async (response) => {
76
+ // let data = { createdAt: new Date(), tokens: response.data.usage.total_tokens, multiplier: multiplier }
77
+ // if (usePublicKey === true) {
78
+ // let incremented_key = await quoteManager.incrementTokenCount(req.project, data);
79
+ // winston.verbose("Tokens quota incremented for key " + incremented_key);
80
+ // }
81
+ //
82
+ // res.status(200).send(response.data);
83
+ //
84
+ // }).catch((err) => {
85
+ // winston.error("completions error: ", sanitizeAxiosError(err));
86
+ // res.status(axiosErrorStatus(err)).send({ success: false, error: sanitizeAxiosError(err) });
87
+ // })
88
+ // })
89
+ // })
88
90
 
89
91
  router.post('/quotes', async (req, res) => {
90
92
 
@@ -106,56 +108,4 @@ router.post('/quotes', async (req, res) => {
106
108
  res.status(200).send({ message: "value incremented for key " + incremented_key, key: incremented_key, currentQuote: quote });
107
109
  })
108
110
 
109
- // router.get('/', async (req, res) => {
110
-
111
- // let project_id = req.projectid;
112
-
113
- // OpenaiKbs.find({ id_project: project_id }, (err, kbs) => {
114
- // if (err) {
115
- // console.error("find all kbs error: ", err);
116
- // return res.status(500).send({ success: false, error: err });
117
- // } else {
118
- // return res.status(200).send(kbs);
119
- // }
120
- // })
121
- // })
122
-
123
- // router.post('/', async (req, res) => {
124
-
125
- // let body = req.body;
126
-
127
- // let new_kbs = new OpenaiKbs({
128
- // name: body.name,
129
- // url: body.url,
130
- // id_project: req.projectid,
131
- // gptkey: req.body.gptkey
132
- // })
133
-
134
- // new_kbs.save(function (err, savedKbs) {
135
- // if (err) {
136
- // console.error("save new kbs error: ", err);
137
- // return res.status(500).send({ success: false, error: err});
138
- // } else {
139
- // return res.status(200).send(savedKbs);
140
- // }
141
- // })
142
- // })
143
-
144
- // router.put('/', async (req, res) => {
145
- // // to be implemented
146
- // })
147
-
148
- // router.delete('/:kbs_id', async (req, res) => {
149
- // let kbs_id = req.params.kbs_id;
150
-
151
- // OpenaiKbs.findOneAndDelete( { _id: kbs_id }, (err, kbDeleted) => {
152
- // if (err) {
153
- // console.error("find one and delete kbs error: ", err);
154
- // return res.status(500).send({ success: false, error: err});
155
- // } else {
156
- // return res.status(200).send({ success: true, message: 'Knowledge Base deleted successfully', openai_kb: kbDeleted });
157
- // }
158
- // })
159
- // })
160
-
161
- module.exports = router;
111
+ module.exports = router;
@@ -17,7 +17,7 @@ function isSafeUrl(rawUrl) {
17
17
  }
18
18
  }
19
19
 
20
- // POST /:projectid/url-preview — public endpoint (used by the widget without auth)
20
+ // POST /:projectid/url-preview — requires JWT (widget sends tiledeskToken as guest)
21
21
  // Body: { "urls": ["https://example.com", "https://another.com"] }
22
22
  router.post('/', async (req, res) => {
23
23
  const urls = req.body.urls;
@@ -53,7 +53,7 @@ class WebhookService {
53
53
  let token = await this.generateChatbotToken(chatbot);
54
54
 
55
55
  let url = TILEBOT_ENDPOINT + 'block/' + webhook.id_project + "/" + chatbot_id + "/" + webhook.block_id;
56
- winston.info("Webhook chatbot URL: " + url);
56
+ winston.verbose("Webhook chatbot URL: " + url);
57
57
 
58
58
  payload.async = webhook.async;
59
59
  payload.token = token;
@@ -0,0 +1,95 @@
1
+ const { expect } = require('chai');
2
+ const {
3
+ maskApiKey,
4
+ isMaskedSecret,
5
+ maskIntegrationValue,
6
+ mergeIntegrationValue,
7
+ toPublicIntegration
8
+ } = require('../utils/maskIntegrationSecrets');
9
+
10
+ describe('maskIntegrationSecrets', () => {
11
+
12
+ it('masks api keys keeping prefix and suffix', () => {
13
+ expect(maskApiKey('sk-abc123nz2bVU')).to.equal('sk-********2bVU');
14
+ expect(maskApiKey('sk-xxxxxxxxxxx2bVU')).to.equal('sk-***********2bVU');
15
+ });
16
+
17
+ it('masks nested integration value fields', () => {
18
+ const value = {
19
+ apikey: 'sk-secretkey1234',
20
+ organization: 'MyCompany',
21
+ servers: [{
22
+ name: 'together',
23
+ url: 'https://api.together.xyz/v1/',
24
+ apikey: 'tgp_v1_abcdefgySDDG-Yw',
25
+ models: ['model-a']
26
+ }]
27
+ };
28
+
29
+ const masked = maskIntegrationValue(value);
30
+ expect(masked.organization).to.equal('MyCompany');
31
+ expect(masked.apikey).to.equal('sk-*********1234');
32
+ expect(masked.servers[0].apikey).to.equal('tgp***************G-Yw');
33
+ expect(masked.servers[0].models).to.deep.equal(['model-a']);
34
+ });
35
+
36
+ it('detects masked secrets', () => {
37
+ expect(isMaskedSecret('sk-***********2bVU')).to.be.true;
38
+ expect(isMaskedSecret('sk-real-key')).to.be.false;
39
+ });
40
+
41
+ it('preserves existing secrets when incoming value is masked', () => {
42
+ const existing = {
43
+ apikey: 'sk-real-key',
44
+ organization: 'MyCompany',
45
+ servers: [{
46
+ name: 'together',
47
+ apikey: 'tgp_v1_realkey',
48
+ url: 'https://api.together.xyz/v1/'
49
+ }]
50
+ };
51
+
52
+ const incoming = {
53
+ apikey: 'sk-***********2bVU',
54
+ organization: 'NewCompany',
55
+ servers: [{
56
+ name: 'together',
57
+ apikey: 'tgp*************D-Yw',
58
+ url: 'https://api.together.xyz/v1/'
59
+ }]
60
+ };
61
+
62
+ const merged = mergeIntegrationValue(incoming, existing);
63
+ expect(merged.apikey).to.equal('sk-real-key');
64
+ expect(merged.organization).to.equal('NewCompany');
65
+ expect(merged.servers[0].apikey).to.equal('tgp_v1_realkey');
66
+ });
67
+
68
+ it('replaces secrets when incoming value is not masked', () => {
69
+ const existing = {
70
+ apikey: 'sk-old-key'
71
+ };
72
+ const incoming = {
73
+ apikey: 'sk-new-key'
74
+ };
75
+
76
+ const merged = mergeIntegrationValue(incoming, existing);
77
+ expect(merged.apikey).to.equal('sk-new-key');
78
+ });
79
+
80
+ it('returns masked integration for public responses', () => {
81
+ const integration = {
82
+ _id: '656054000410fa00132e5dcc',
83
+ id_project: '656054000410fa00132e5dcc',
84
+ name: 'openai',
85
+ value: {
86
+ apikey: 'sk-secretkey1234',
87
+ organization: 'MyCompany'
88
+ }
89
+ };
90
+
91
+ const publicIntegration = toPublicIntegration(integration);
92
+ expect(publicIntegration.value.apikey).to.equal('sk-*********1234');
93
+ expect(publicIntegration.value.organization).to.equal('MyCompany');
94
+ });
95
+ });
@@ -38,7 +38,7 @@ function assignmentActorContext(req, defaultSource) {
38
38
  url: req.originalUrl
39
39
  });
40
40
  } else {
41
- winston.info('assignmentActorContext actor resolution', {
41
+ winston.debug('assignmentActorContext actor resolution', {
42
42
  actor: actor,
43
43
  principal_id: projectUserUpdateContextUtil.resolveUserId(req.user),
44
44
  principal_name: req.user && req.user.name,
@@ -0,0 +1,132 @@
1
+ const SECRET_KEYS = new Set(['apikey', 'api_key']);
2
+
3
+ const UNMASKED_START = 3;
4
+ const UNMASKED_END = 4;
5
+
6
+ function maskApiKey(key) {
7
+ if (typeof key !== 'string' || key.length === 0) {
8
+ return key;
9
+ }
10
+
11
+ if (key.length <= UNMASKED_START + UNMASKED_END) {
12
+ return '*'.repeat(key.length);
13
+ }
14
+
15
+ const start = key.slice(0, UNMASKED_START);
16
+ const end = key.slice(-UNMASKED_END);
17
+ const maskLength = key.length - UNMASKED_START - UNMASKED_END;
18
+ return start + '*'.repeat(maskLength) + end;
19
+ }
20
+
21
+ function isMaskedSecret(value) {
22
+ return typeof value === 'string' && value.includes('*');
23
+ }
24
+
25
+ function mergeSecretField(incoming, existing) {
26
+ if (incoming === undefined) {
27
+ return existing;
28
+ }
29
+ if (isMaskedSecret(incoming) && existing) {
30
+ return existing;
31
+ }
32
+ return incoming;
33
+ }
34
+
35
+ function maskIntegrationValue(value) {
36
+ if (value === null || value === undefined) {
37
+ return value;
38
+ }
39
+
40
+ if (Array.isArray(value)) {
41
+ return value.map(maskIntegrationValue);
42
+ }
43
+
44
+ if (typeof value !== 'object') {
45
+ return value;
46
+ }
47
+
48
+ const result = {};
49
+ for (const [key, val] of Object.entries(value)) {
50
+ if (SECRET_KEYS.has(key) && typeof val === 'string') {
51
+ result[key] = maskApiKey(val);
52
+ } else if (val !== null && typeof val === 'object') {
53
+ result[key] = maskIntegrationValue(val);
54
+ } else {
55
+ result[key] = val;
56
+ }
57
+ }
58
+ return result;
59
+ }
60
+
61
+ function mergeIntegrationValue(incoming, existing) {
62
+ if (!incoming) {
63
+ return incoming;
64
+ }
65
+ if (!existing) {
66
+ return incoming;
67
+ }
68
+
69
+ const merged = { ...existing, ...incoming };
70
+
71
+ if (incoming.apikey !== undefined) {
72
+ merged.apikey = mergeSecretField(incoming.apikey, existing.apikey);
73
+ }
74
+ if (incoming.api_key !== undefined) {
75
+ merged.api_key = mergeSecretField(incoming.api_key, existing.api_key);
76
+ }
77
+
78
+ if (Array.isArray(incoming.servers)) {
79
+ merged.servers = incoming.servers.map((server) => {
80
+ const existingServer = Array.isArray(existing.servers)
81
+ ? existing.servers.find((s) => s.name === server.name)
82
+ : undefined;
83
+
84
+ if (!existingServer) {
85
+ return server;
86
+ }
87
+
88
+ return {
89
+ ...existingServer,
90
+ ...server,
91
+ apikey: mergeSecretField(server.apikey, existingServer.apikey),
92
+ api_key: mergeSecretField(server.api_key, existingServer.api_key)
93
+ };
94
+ });
95
+ }
96
+
97
+ return merged;
98
+ }
99
+
100
+ function toIntegrationObject(integration) {
101
+ if (!integration) {
102
+ return integration;
103
+ }
104
+ return integration.toObject ? integration.toObject() : { ...integration };
105
+ }
106
+
107
+ function toPublicIntegration(integration) {
108
+ const obj = toIntegrationObject(integration);
109
+ if (!obj) {
110
+ return obj;
111
+ }
112
+ if (obj.value) {
113
+ obj.value = maskIntegrationValue(obj.value);
114
+ }
115
+ return obj;
116
+ }
117
+
118
+ function toPublicIntegrations(integrations) {
119
+ if (!Array.isArray(integrations)) {
120
+ return integrations;
121
+ }
122
+ return integrations.map(toPublicIntegration);
123
+ }
124
+
125
+ module.exports = {
126
+ maskApiKey,
127
+ isMaskedSecret,
128
+ maskIntegrationValue,
129
+ mergeIntegrationValue,
130
+ toPublicIntegration,
131
+ toPublicIntegrations
132
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Returns a safe error payload for HTTP responses.
3
+ * Never pass raw AxiosError objects to res.send/res.json — toJSON() includes config.headers.
4
+ */
5
+ function sanitizeAxiosError(err) {
6
+ if (!err) {
7
+ return { message: 'Request failed' };
8
+ }
9
+
10
+ const data = err.response?.data;
11
+
12
+ if (data?.error?.message) {
13
+ return { message: data.error.message };
14
+ }
15
+
16
+ if (typeof data?.detail === 'string') {
17
+ return { message: data.detail };
18
+ }
19
+
20
+ if (Array.isArray(data?.detail) && data.detail[0]?.msg) {
21
+ return { message: data.detail[0].msg };
22
+ }
23
+
24
+ if (data?.detail?.answer) {
25
+ return { message: data.detail.answer };
26
+ }
27
+
28
+ if (typeof data?.message === 'string') {
29
+ return { message: data.message };
30
+ }
31
+
32
+ if (typeof err.message === 'string') {
33
+ return { message: err.message };
34
+ }
35
+
36
+ return { message: 'Request failed' };
37
+ }
38
+
39
+ function axiosErrorStatus(err, defaultStatus) {
40
+ return err?.response?.status || defaultStatus || 500;
41
+ }
42
+
43
+ module.exports = {
44
+ sanitizeAxiosError,
45
+ axiosErrorStatus
46
+ };