wingbot 3.24.5 → 3.26.0-alpha.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/index.js CHANGED
@@ -21,6 +21,7 @@ const { asserts } = require('./src/testTools');
21
21
  const BuildRouter = require('./src/BuildRouter');
22
22
  const MockAiModel = require('./src/MockAiModel');
23
23
  const ReturnSender = require('./src/ReturnSender');
24
+ const CallbackAuditLog = require('./src/CallbackAuditLog');
24
25
  const Plugins = require('./src/Plugins');
25
26
  const NotificationsStorage = require('./src/notifications/NotificationsStorage');
26
27
  const Notifications = require('./src/notifications/Notifications');
@@ -70,6 +71,7 @@ module.exports = {
70
71
  bufferloader,
71
72
  asserts,
72
73
  MemoryStateStorage,
74
+ CallbackAuditLog,
73
75
  MemoryBotConfigStorage,
74
76
  CachedModel,
75
77
  CustomEntityDetectionModel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wingbot",
3
- "version": "3.24.5",
3
+ "version": "3.26.0-alpha.2",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/BotApp.js CHANGED
@@ -9,6 +9,7 @@ const crypto = require('crypto');
9
9
  const BotAppSender = require('./BotAppSender');
10
10
  const Processor = require('./Processor');
11
11
  const ReturnSender = require('./ReturnSender');
12
+ const headersToAuditMeta = require('./utils/headersToAuditMeta');
12
13
 
13
14
  const DEFAULT_API_URL = 'https://orchestrator-api.wingbot.ai';
14
15
 
@@ -20,6 +21,9 @@ const DEFAULT_API_URL = 'https://orchestrator-api.wingbot.ai';
20
21
  /** @typedef {import('./Responder')} Responder */
21
22
  /** @typedef {import('./Processor').Plugin} Plugin */
22
23
 
24
+ /** @typedef {import('./CallbackAuditLog')} AuditLog */
25
+ /** @typedef {import('./BotAppSender').TlsOptions} TlsOptions */
26
+
23
27
  /**
24
28
  * @typedef {object} BotAppOptions
25
29
  * @prop {string|Promise<string>} secret
@@ -28,6 +32,8 @@ const DEFAULT_API_URL = 'https://orchestrator-api.wingbot.ai';
28
32
  * @prop {Function} [fetch]
29
33
  * @prop {ChatLogStorage} [chatLogStorage]
30
34
  * @prop {boolean} [preferSynchronousResponse]
35
+ * @prop {AuditLog} [auditLog]
36
+ * @prop {TlsOptions} [tls]
31
37
  *
32
38
  * @typedef {ProcessorOptions & BotAppOptions} Options
33
39
  */
@@ -58,12 +64,16 @@ class BotApp {
58
64
  fetch = null,
59
65
  appId = null,
60
66
  preferSynchronousResponse = false,
67
+ auditLog = null,
68
+ tls = null,
61
69
  ...processorOptions
62
70
  } = options;
63
71
 
64
72
  this._secret = Promise.resolve(secret);
65
73
  this._fetch = fetch; // mock
66
74
  this._appId = appId;
75
+ this._auditLog = auditLog;
76
+ this._tls = tls;
67
77
 
68
78
  let { apiUrl } = options;
69
79
 
@@ -90,6 +100,18 @@ class BotApp {
90
100
  this._preferSynchronousResponse = preferSynchronousResponse;
91
101
  }
92
102
 
103
+ /**
104
+ * Get authorization token for wingbot orchestrator
105
+ *
106
+ * @param {string} body
107
+ * @param {string} secret
108
+ * @param {string} appId
109
+ * @returns {Promise<string>}
110
+ */
111
+ static signBody (body, secret, appId) {
112
+ return BotApp.signBody(body, secret, appId);
113
+ }
114
+
93
115
  /**
94
116
  * Returns processor plugin, which updates thread context automatically
95
117
  *
@@ -140,13 +162,44 @@ class BotApp {
140
162
  };
141
163
  }
142
164
 
143
- async _processIncommingMessage (message, senderId, pageId, appId, secret, sync = false) {
165
+ /**
166
+ * @param {ReturnSender} sender
167
+ * @param {string} senderId
168
+ * @param {string} pageId
169
+ * @param {object} headers
170
+ */
171
+ async _processSenderResponses (sender, senderId, pageId, headers) {
172
+ if (!this._auditLog) {
173
+ return;
174
+ }
175
+ await Promise.all(
176
+ sender.tracking.events.map(async (event) => {
177
+ if (!['audit', 'report'].includes(event.type)) {
178
+ return;
179
+ }
180
+
181
+ await this._auditLog.log(
182
+ event,
183
+ { senderId, pageId },
184
+ headersToAuditMeta(headers),
185
+ this._auditLog.defaultWid,
186
+ 'Info',
187
+ event.type === 'audit' ? 'Important' : 'Debug'
188
+ );
189
+ })
190
+ );
191
+ }
192
+
193
+ async _processIncommingMessage (
194
+ message, senderId, pageId, appId, secret, sync = false, headers = {}
195
+ ) {
144
196
  const { mid = null } = message;
145
197
 
146
198
  if (sync || this._preferSynchronousResponse) {
147
199
  const sender = new ReturnSender({}, senderId, message, this._senderLogger);
148
200
  sender.propagatesWaitEvent = true;
149
201
  const res = await this._processor.processMessage(message, pageId, sender, { appId });
202
+ await this._processSenderResponses(sender, senderId, pageId, headers);
150
203
 
151
204
  return {
152
205
  status: res.status,
@@ -174,11 +227,13 @@ class BotApp {
174
227
  appId,
175
228
  secret,
176
229
  mid,
177
- fetch: this._fetch
230
+ fetch: this._fetch,
231
+ tls: this._tls
178
232
  };
179
233
 
180
234
  const sender = new BotAppSender(options, senderId, message, this._senderLogger);
181
235
  const res = await this._processor.processMessage(message, pageId, sender, { appId });
236
+ await this._processSenderResponses(sender, senderId, pageId, headers);
182
237
 
183
238
  return {
184
239
  status: res.status,
@@ -280,7 +335,7 @@ class BotApp {
280
335
 
281
336
  const body = JSON.parse(rawBody);
282
337
 
283
- const entry = await this._processEntries(appId, secret, body.entry);
338
+ const entry = await this._processEntries(appId, secret, body.entry, rawHeaders);
284
339
 
285
340
  return {
286
341
  statusCode: 200,
@@ -293,7 +348,7 @@ class BotApp {
293
348
  };
294
349
  }
295
350
 
296
- async _processEntries (appId, secret, entry = []) {
351
+ async _processEntries (appId, secret, entry = [], headers = {}) {
297
352
  return Promise.all(
298
353
  entry.map(async (event) => {
299
354
  const {
@@ -308,7 +363,8 @@ class BotApp {
308
363
  ...standby.map((e) => ({ ...e, isStandby: true }))
309
364
  ];
310
365
 
311
- const responses = await this._processMessaging(events, pageId, appId, secret, sync);
366
+ const responses = await this
367
+ ._processMessaging(events, pageId, appId, secret, sync, headers);
312
368
 
313
369
  return {
314
370
  id: pageId,
@@ -318,7 +374,7 @@ class BotApp {
318
374
  );
319
375
  }
320
376
 
321
- async _processMessaging (process, pageId, appId, secret, sync) {
377
+ async _processMessaging (process, pageId, appId, secret, sync, headers) {
322
378
  const eventsBySenderId = new Map();
323
379
  process.forEach((e) => this._processMessagingArrayItem(e, eventsBySenderId));
324
380
 
@@ -329,7 +385,7 @@ class BotApp {
329
385
 
330
386
  for (const message of messaging) {
331
387
  const res = await this._processIncommingMessage(
332
- message, senderId, pageId, appId, secret, sync
388
+ message, senderId, pageId, appId, secret, sync, headers
333
389
  );
334
390
 
335
391
  responses.push(res);
@@ -6,17 +6,25 @@
6
6
  const jwt = require('jsonwebtoken');
7
7
  const { default: fetch, Headers } = require('node-fetch');
8
8
  const crypto = require('crypto');
9
+ const { Agent } = require('https');
9
10
  const { promisify } = require('util');
10
11
  const ReturnSender = require('./ReturnSender');
11
12
 
12
13
  /** @typedef {import('./ReturnSender').ReturnSenderOptions} ReturnSenderOptions */
13
14
  /** @typedef {import('./ReturnSender').ChatLogStorage} ChatLogStorage */
14
15
 
16
+ /**
17
+ * @typedef {object} TlsOptions
18
+ * @prop {string|Promise<string>} key
19
+ * @prop {string|Promise<string>} cert
20
+ */
21
+
15
22
  /**
16
23
  * @typedef {object} BotAppSenderOptions
17
24
  * @prop {string} apiUrl
18
25
  * @prop {string} pageId
19
26
  * @prop {string} appId
27
+ * @prop {TlsOptions} [tls]
20
28
  * @prop {string} [mid]
21
29
  * @prop {Function} [fetch]
22
30
  * @prop {string|Promise<string>} secret
@@ -47,6 +55,9 @@ class BotAppSender extends ReturnSender {
47
55
 
48
56
  this._secret = Promise.resolve(options.secret);
49
57
 
58
+ this._tls = options.tls;
59
+ this._agent = null;
60
+
50
61
  this._fetch = options.fetch || fetch;
51
62
  }
52
63
 
@@ -65,6 +76,21 @@ class BotAppSender extends ReturnSender {
65
76
  }, goodSecret);
66
77
  }
67
78
 
79
+ async _getAgent () {
80
+ if (!this._tls) {
81
+ return null;
82
+ }
83
+ if (!this._agent) {
84
+ const [key, cert] = await Promise.all([
85
+ Promise.resolve(this._tls.key), Promise.resolve(this._tls.cert)
86
+ ]);
87
+
88
+ this._agent = new Agent({ cert, key });
89
+ }
90
+
91
+ return this._agent;
92
+ }
93
+
68
94
  async _send (payload) {
69
95
  // attach sender
70
96
  if (typeof payload.sender === 'undefined') {
@@ -78,14 +104,19 @@ class BotAppSender extends ReturnSender {
78
104
 
79
105
  const body = JSON.stringify(payload);
80
106
 
81
- const token = await BotAppSender.signBody(body, this._secret, this._appId);
107
+ const [token, agent] = await Promise.all([
108
+ BotAppSender.signBody(body, this._secret, this._appId),
109
+ this._getAgent()
110
+ ]);
82
111
 
83
112
  const headers = new Headers();
84
113
 
85
114
  headers.set('Authorization', token);
86
115
  headers.set('Content-Type', 'application/json');
87
116
 
88
- const response = await this._fetch(this._apiUrl, { headers, body, method: 'POST' })
117
+ const response = await this._fetch(this._apiUrl, {
118
+ headers, body, agent, method: 'POST'
119
+ })
89
120
  .then((r) => r.json());
90
121
 
91
122
  const { request, errors = null } = response;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * @author wingbot.ai
3
+ */
4
+ 'use strict';
5
+
6
+ const LEVEL_CRITICAL = 'Critical';
7
+ const LEVEL_IMPORTANT = 'Important';
8
+ const LEVEL_DEBUG = 'Debug';
9
+
10
+ const TYPE_ERROR = 'Error';
11
+ const TYPE_WARN = 'Warn';
12
+ const TYPE_INFO = 'Info';
13
+
14
+ /**
15
+ * @typedef {object} TrackingEvent
16
+ * @prop {string} [type='audit']
17
+ * @prop {string} category
18
+ * @prop {string} action
19
+ * @prop {string} [label]
20
+ * @prop {object} [payload]
21
+ */
22
+
23
+ /**
24
+ * @typedef {object} User
25
+ * @prop {string} [id]
26
+ * @prop {string} [senderId]
27
+ * @prop {string} [pageId]
28
+ * @prop {string} [jwt] - jwt to check the authorship
29
+ */
30
+
31
+ /**
32
+ * @typedef {object} Meta
33
+ * @prop {string} [ip]
34
+ * @prop {string} [ua]
35
+ * @prop {string} [ro] - referrer || origin
36
+ */
37
+
38
+ /**
39
+ * @typedef {object} Logger
40
+ * @prop {Function} log
41
+ * @prop {Function} error
42
+ */
43
+
44
+ /**
45
+ * @typedef {object} AuditLogEntry
46
+ * @prop {string} date - ISO date
47
+ * @prop {string} [eventType='audit']
48
+ * @prop {string} category
49
+ * @prop {string} action
50
+ * @prop {string} [label]
51
+ * @prop {object} [payload]
52
+ * @prop {string} level - (Critical|Important|Debug)
53
+ * @prop {string} type - (Error|Warn|Info)
54
+ * @prop {User} user
55
+ * @prop {string} wid - workspace id
56
+ * @prop {Meta} meta
57
+ */
58
+
59
+ /**
60
+ * Audit Log Callback
61
+ *
62
+ * @callback AuditLogCallback
63
+ * @param {AuditLogEntry} entry
64
+ * @returns {Promise}
65
+ */
66
+
67
+ /**
68
+ * Just a simple auditlog interface
69
+ */
70
+ class CallbackAuditLog {
71
+
72
+ /**
73
+ *
74
+ * @param {AuditLogCallback} callback
75
+ * @param {Logger} log
76
+ */
77
+ constructor (callback = null, log = console) {
78
+ this._log = log;
79
+ this.defaultWid = '0';
80
+
81
+ this.LEVEL_CRITICAL = LEVEL_CRITICAL;
82
+ this.LEVEL_IMPORTANT = LEVEL_IMPORTANT;
83
+ this.LEVEL_DEBUG = LEVEL_DEBUG;
84
+
85
+ this.TYPE_ERROR = TYPE_ERROR;
86
+ this.TYPE_WARN = TYPE_WARN;
87
+ this.TYPE_INFO = TYPE_INFO;
88
+
89
+ /** @type {AuditLogCallback} */
90
+ this.callback = callback || (async (e) => {
91
+ const {
92
+ category,
93
+ action,
94
+ ...rest
95
+ } = e;
96
+ log.log(`AuditLog: ${category} - ${action}`, rest);
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Add a log
102
+ *
103
+ * @param {TrackingEvent} event
104
+ * @param {User} user
105
+ * @param {Meta} [meta]
106
+ * @param {string} [wid] - workspace ID
107
+ * @param {string} [type]
108
+ * @param {string} [level]
109
+ * @param {Date} [date]
110
+ * @returns {Promise}
111
+ */
112
+ async log (
113
+ event,
114
+ user = {},
115
+ meta = {},
116
+ wid = this.defaultWid,
117
+ type = TYPE_INFO,
118
+ level = LEVEL_IMPORTANT,
119
+ date = new Date()
120
+ ) {
121
+ const {
122
+ type: eventType = 'audit',
123
+ ...rest
124
+ } = event;
125
+ const entry = {
126
+ date: date.toISOString(),
127
+ eventType,
128
+ ...rest,
129
+ level,
130
+ meta,
131
+ type,
132
+ user,
133
+ wid
134
+ };
135
+
136
+ try {
137
+ await this.callback(entry);
138
+ } catch (e) {
139
+ this._log.error('Failed to send AuditLog', e, entry);
140
+ }
141
+ }
142
+
143
+ }
144
+
145
+ module.exports = CallbackAuditLog;
package/src/Responder.js CHANGED
@@ -190,7 +190,7 @@ class Responder {
190
190
  * Events are aggregated within ReturnSender and can be caught
191
191
  * within Processor's `interaction` event (event.tracking.events)
192
192
  *
193
- * @param {string} type
193
+ * @param {string} type - (log,report,conversation,audit,user)
194
194
  * @param {string} category
195
195
  * @param {string} [action]
196
196
  * @param {string} [label]
@@ -8,6 +8,7 @@ const { graphql, buildSchema } = require('graphql');
8
8
  const WingbotApiConnector = require('./WingbotApiConnector');
9
9
  // @ts-ignore
10
10
  const packageJson = require('../../package.json');
11
+ const headersToAuditMeta = require('../utils/headersToAuditMeta');
11
12
 
12
13
  const DEFAULT_GROUPS = ['botEditor', 'botAdmin', 'appToken'];
13
14
  const KEYS_URL = 'https://api.wingbot.ai/keys';
@@ -19,6 +20,8 @@ const DEFAULT_CACHE = 86400000; // 24 hours
19
20
  * @param {object[]} [errors]
20
21
  */
21
22
 
23
+ /** @typedef {import('../CallbackAuditLog')} AuditLog */
24
+
22
25
  /**
23
26
  * Experimental chatbot API
24
27
  */
@@ -32,6 +35,7 @@ class GraphApi {
32
35
  * @param {string} [options.appToken] - public token
33
36
  * @param {string[]} [options.groups] - list of allowed bot groups
34
37
  * @param {boolean} [options.useBundledGql] - uses library bundled graphql definition
38
+ * @param {AuditLog} [options.auditLog]
35
39
  */
36
40
  constructor (apis, options) {
37
41
  this._root = {
@@ -44,7 +48,16 @@ class GraphApi {
44
48
  token: null,
45
49
  groups: DEFAULT_GROUPS,
46
50
  keysUrl: KEYS_URL,
47
- cacheKeys: DEFAULT_CACHE
51
+ cacheKeys: DEFAULT_CACHE,
52
+ auditLog: {
53
+ async callback () {
54
+ // noop
55
+ },
56
+ defaultWid: '0',
57
+ async log () {
58
+ // noop
59
+ }
60
+ }
48
61
  };
49
62
 
50
63
  Object.assign(opts, options);
@@ -57,6 +70,10 @@ class GraphApi {
57
70
 
58
71
  this._defaultGroups = opts.groups;
59
72
 
73
+ /** @type {AuditLog} */
74
+ // @ts-ignore
75
+ this.auditLog = opts.auditLog;
76
+
60
77
  this._apiConnector = new WingbotApiConnector({
61
78
  token: opts.token,
62
79
  appToken: opts.appToken,
@@ -75,6 +92,10 @@ class GraphApi {
75
92
  * @param {object} headers
76
93
  * @param {string} [headers.Authorization]
77
94
  * @param {string} [headers.authorization]
95
+ * @param {string} [headers.Origin]
96
+ * @param {string} [headers.origin]
97
+ * @param {string} [headers.Referer]
98
+ * @param {string} [headers.referer]
78
99
  * @param {string} [wingbotToken]
79
100
  * @returns {Promise<GraphQlResponse>}
80
101
  */
@@ -83,13 +104,39 @@ class GraphApi {
83
104
  assert.equal(typeof body.query, 'string', 'GraphQL request should contain a query property');
84
105
 
85
106
  const authHeader = headers.Authorization || headers.authorization;
86
- const token = await this._apiConnector.verifyToken(authHeader, wingbotToken);
107
+ let token = {};
108
+
109
+ const audit = async (action, payload = {}, important = false, warn = false) => {
110
+ await this.auditLog.log(
111
+ {
112
+ category: 'API',
113
+ action,
114
+ payload
115
+ },
116
+ {
117
+ id: token.id,
118
+ jwt: token.id && authHeader.replace(/^bearer\s/i, '')
119
+ },
120
+ headersToAuditMeta(headers),
121
+ this.auditLog.defaultWid,
122
+ warn ? 'Warn' : 'Info',
123
+ important ? 'Important' : 'Debug'
124
+ );
125
+ };
126
+
127
+ try {
128
+ token = await this._apiConnector.verifyToken(authHeader, wingbotToken);
129
+ } catch (e) {
130
+ await audit('authorization failed', { message: e.message, authHeader }, true, true);
131
+ throw e;
132
+ }
87
133
 
88
134
  const schema = await this._schema();
89
135
 
90
136
  const ctx = {
91
137
  token,
92
- groups: this._defaultGroups
138
+ groups: this._defaultGroups,
139
+ audit
93
140
  };
94
141
 
95
142
  // @ts-ignore
@@ -42,6 +42,8 @@ function conversationTestApi (testsSource, botFactory, options, acl) {
42
42
 
43
43
  const { bot: validationRequestBody, step = null, lang = null } = args;
44
44
 
45
+ await ctx.audit('conversationTest', args);
46
+
45
47
  return test.test(validationRequestBody, step, lang);
46
48
  }
47
49
  };
@@ -131,6 +131,8 @@ function conversationsApi (
131
131
  condition
132
132
  } = args;
133
133
 
134
+ await ctx.audit('conversations', args);
135
+
134
136
  const { data, lastKey: nextKey } = await stateStorage
135
137
  .getStates(condition || {}, limit, lastKey);
136
138
 
@@ -154,6 +156,8 @@ function conversationsApi (
154
156
  senderId
155
157
  } = args;
156
158
 
159
+ await ctx.audit('conversation', args);
160
+
157
161
  const state = await stateStorage.getState(senderId, pageId);
158
162
 
159
163
  if (!state) {
@@ -35,6 +35,7 @@ function postBackApi (processor, acl) {
35
35
 
36
36
  const event = Request.postBack(args.senderId, args.action, args.data);
37
37
 
38
+ await ctx.audit('postBack', args);
38
39
  return processor.processMessage(event, args.senderId, args.pageId);
39
40
  }
40
41
  };
@@ -99,6 +99,7 @@ function validateBotApi (botFactory, postBackTest = null, textTest = null, acl =
99
99
 
100
100
  const bot = botFactory();
101
101
 
102
+ await ctx.audit('validateBot');
102
103
  return validate(bot, validationRequestBody, postBackTest, textTest);
103
104
  }
104
105
  };
@@ -179,7 +179,7 @@ function getSetState (setState = {}, req, res = null, useState = null) {
179
179
  const useValue = typeof value === 'string'
180
180
  ? handlebars.compile(value)(stateData(req, res))
181
181
  .split(/(?<!\\),/g)
182
- .map((v) => v.trim())
182
+ .map((v) => v.replace(/\\,/g, ',').trim())
183
183
  : value;
184
184
  values = toArray(useValue);
185
185
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @author wingbot.ai
3
+ */
4
+ 'use strict';
5
+
6
+ module.exports = function headersToAuditMeta (headers) {
7
+ return {
8
+ ua: headers['User-Agent'] || headers['user-agent'] || headers['User-agent'],
9
+ ro: headers.Referer || headers.referer || headers.Origin || headers.origin,
10
+ ip: headers['X-Forwarded-For'] || headers['x-forwarded-for'] || headers['X-Azure-ClientIP'] || headers['x-azure-clientip']
11
+ };
12
+ };