wingbot 3.24.5 → 3.25.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/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.25.0",
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,8 @@ 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
+
23
26
  /**
24
27
  * @typedef {object} BotAppOptions
25
28
  * @prop {string|Promise<string>} secret
@@ -28,6 +31,7 @@ const DEFAULT_API_URL = 'https://orchestrator-api.wingbot.ai';
28
31
  * @prop {Function} [fetch]
29
32
  * @prop {ChatLogStorage} [chatLogStorage]
30
33
  * @prop {boolean} [preferSynchronousResponse]
34
+ * @prop {AuditLog} [auditLog]
31
35
  *
32
36
  * @typedef {ProcessorOptions & BotAppOptions} Options
33
37
  */
@@ -58,12 +62,14 @@ class BotApp {
58
62
  fetch = null,
59
63
  appId = null,
60
64
  preferSynchronousResponse = false,
65
+ auditLog = null,
61
66
  ...processorOptions
62
67
  } = options;
63
68
 
64
69
  this._secret = Promise.resolve(secret);
65
70
  this._fetch = fetch; // mock
66
71
  this._appId = appId;
72
+ this._auditLog = auditLog;
67
73
 
68
74
  let { apiUrl } = options;
69
75
 
@@ -140,13 +146,44 @@ class BotApp {
140
146
  };
141
147
  }
142
148
 
143
- async _processIncommingMessage (message, senderId, pageId, appId, secret, sync = false) {
149
+ /**
150
+ * @param {ReturnSender} sender
151
+ * @param {string} senderId
152
+ * @param {string} pageId
153
+ * @param {object} headers
154
+ */
155
+ async _processSenderResponses (sender, senderId, pageId, headers) {
156
+ if (!this._auditLog) {
157
+ return;
158
+ }
159
+ await Promise.all(
160
+ sender.tracking.events.map(async (event) => {
161
+ if (!['audit', 'report'].includes(event.type)) {
162
+ return;
163
+ }
164
+
165
+ await this._auditLog.log(
166
+ event,
167
+ { senderId, pageId },
168
+ headersToAuditMeta(headers),
169
+ this._auditLog.defaultWid,
170
+ 'Info',
171
+ event.type === 'audit' ? 'Important' : 'Debug'
172
+ );
173
+ })
174
+ );
175
+ }
176
+
177
+ async _processIncommingMessage (
178
+ message, senderId, pageId, appId, secret, sync = false, headers = {}
179
+ ) {
144
180
  const { mid = null } = message;
145
181
 
146
182
  if (sync || this._preferSynchronousResponse) {
147
183
  const sender = new ReturnSender({}, senderId, message, this._senderLogger);
148
184
  sender.propagatesWaitEvent = true;
149
185
  const res = await this._processor.processMessage(message, pageId, sender, { appId });
186
+ await this._processSenderResponses(sender, senderId, pageId, headers);
150
187
 
151
188
  return {
152
189
  status: res.status,
@@ -179,6 +216,7 @@ class BotApp {
179
216
 
180
217
  const sender = new BotAppSender(options, senderId, message, this._senderLogger);
181
218
  const res = await this._processor.processMessage(message, pageId, sender, { appId });
219
+ await this._processSenderResponses(sender, senderId, pageId, headers);
182
220
 
183
221
  return {
184
222
  status: res.status,
@@ -280,7 +318,7 @@ class BotApp {
280
318
 
281
319
  const body = JSON.parse(rawBody);
282
320
 
283
- const entry = await this._processEntries(appId, secret, body.entry);
321
+ const entry = await this._processEntries(appId, secret, body.entry, rawHeaders);
284
322
 
285
323
  return {
286
324
  statusCode: 200,
@@ -293,7 +331,7 @@ class BotApp {
293
331
  };
294
332
  }
295
333
 
296
- async _processEntries (appId, secret, entry = []) {
334
+ async _processEntries (appId, secret, entry = [], headers = {}) {
297
335
  return Promise.all(
298
336
  entry.map(async (event) => {
299
337
  const {
@@ -308,7 +346,8 @@ class BotApp {
308
346
  ...standby.map((e) => ({ ...e, isStandby: true }))
309
347
  ];
310
348
 
311
- const responses = await this._processMessaging(events, pageId, appId, secret, sync);
349
+ const responses = await this
350
+ ._processMessaging(events, pageId, appId, secret, sync, headers);
312
351
 
313
352
  return {
314
353
  id: pageId,
@@ -318,7 +357,7 @@ class BotApp {
318
357
  );
319
358
  }
320
359
 
321
- async _processMessaging (process, pageId, appId, secret, sync) {
360
+ async _processMessaging (process, pageId, appId, secret, sync, headers) {
322
361
  const eventsBySenderId = new Map();
323
362
  process.forEach((e) => this._processMessagingArrayItem(e, eventsBySenderId));
324
363
 
@@ -329,7 +368,7 @@ class BotApp {
329
368
 
330
369
  for (const message of messaging) {
331
370
  const res = await this._processIncommingMessage(
332
- message, senderId, pageId, appId, secret, sync
371
+ message, senderId, pageId, appId, secret, sync, headers
333
372
  );
334
373
 
335
374
  responses.push(res);
@@ -0,0 +1,150 @@
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
+ /**
79
+ * @type {Logger}
80
+ * @ignore
81
+ * @private
82
+ */
83
+ this._log = log;
84
+ this.defaultWid = '0';
85
+
86
+ this.LEVEL_CRITICAL = LEVEL_CRITICAL;
87
+ this.LEVEL_IMPORTANT = LEVEL_IMPORTANT;
88
+ this.LEVEL_DEBUG = LEVEL_DEBUG;
89
+
90
+ this.TYPE_ERROR = TYPE_ERROR;
91
+ this.TYPE_WARN = TYPE_WARN;
92
+ this.TYPE_INFO = TYPE_INFO;
93
+
94
+ /** @type {AuditLogCallback} */
95
+ this.callback = callback || (async (e) => {
96
+ const {
97
+ category,
98
+ action,
99
+ ...rest
100
+ } = e;
101
+ log.log(`AuditLog: ${category} - ${action}`, rest);
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Add a log
107
+ *
108
+ * @param {TrackingEvent} event
109
+ * @param {User} user
110
+ * @param {Meta} [meta]
111
+ * @param {string} [wid] - workspace ID
112
+ * @param {string} [type]
113
+ * @param {string} [level]
114
+ * @param {Date} [date]
115
+ * @returns {Promise}
116
+ */
117
+ async log (
118
+ event,
119
+ user = {},
120
+ meta = {},
121
+ wid = this.defaultWid,
122
+ type = TYPE_INFO,
123
+ level = LEVEL_IMPORTANT,
124
+ date = new Date()
125
+ ) {
126
+ const {
127
+ type: eventType = 'audit',
128
+ ...rest
129
+ } = event;
130
+ const entry = {
131
+ date: date.toISOString(),
132
+ eventType,
133
+ ...rest,
134
+ level,
135
+ meta,
136
+ type,
137
+ user,
138
+ wid
139
+ };
140
+
141
+ try {
142
+ await this.callback(entry);
143
+ } catch (e) {
144
+ this._log.error('Failed to send AuditLog', e, entry);
145
+ }
146
+ }
147
+
148
+ }
149
+
150
+ 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', args);
102
103
  return validate(bot, validationRequestBody, postBackTest, textTest);
103
104
  }
104
105
  };
@@ -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
+ };