wingbot 3.24.4 → 3.26.0-alpha.1

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.4",
3
+ "version": "3.26.0-alpha.1",
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
 
@@ -140,13 +150,44 @@ class BotApp {
140
150
  };
141
151
  }
142
152
 
143
- async _processIncommingMessage (message, senderId, pageId, appId, secret, sync = false) {
153
+ /**
154
+ * @param {ReturnSender} sender
155
+ * @param {string} senderId
156
+ * @param {string} pageId
157
+ * @param {object} headers
158
+ */
159
+ async _processSenderResponses (sender, senderId, pageId, headers) {
160
+ if (!this._auditLog) {
161
+ return;
162
+ }
163
+ await Promise.all(
164
+ sender.tracking.events.map(async (event) => {
165
+ if (!['audit', 'report'].includes(event.type)) {
166
+ return;
167
+ }
168
+
169
+ await this._auditLog.log(
170
+ event,
171
+ { senderId, pageId },
172
+ headersToAuditMeta(headers),
173
+ this._auditLog.defaultWid,
174
+ 'Info',
175
+ event.type === 'audit' ? 'Important' : 'Debug'
176
+ );
177
+ })
178
+ );
179
+ }
180
+
181
+ async _processIncommingMessage (
182
+ message, senderId, pageId, appId, secret, sync = false, headers = {}
183
+ ) {
144
184
  const { mid = null } = message;
145
185
 
146
186
  if (sync || this._preferSynchronousResponse) {
147
187
  const sender = new ReturnSender({}, senderId, message, this._senderLogger);
148
188
  sender.propagatesWaitEvent = true;
149
189
  const res = await this._processor.processMessage(message, pageId, sender, { appId });
190
+ await this._processSenderResponses(sender, senderId, pageId, headers);
150
191
 
151
192
  return {
152
193
  status: res.status,
@@ -174,11 +215,13 @@ class BotApp {
174
215
  appId,
175
216
  secret,
176
217
  mid,
177
- fetch: this._fetch
218
+ fetch: this._fetch,
219
+ tls: this._tls
178
220
  };
179
221
 
180
222
  const sender = new BotAppSender(options, senderId, message, this._senderLogger);
181
223
  const res = await this._processor.processMessage(message, pageId, sender, { appId });
224
+ await this._processSenderResponses(sender, senderId, pageId, headers);
182
225
 
183
226
  return {
184
227
  status: res.status,
@@ -280,7 +323,7 @@ class BotApp {
280
323
 
281
324
  const body = JSON.parse(rawBody);
282
325
 
283
- const entry = await this._processEntries(appId, secret, body.entry);
326
+ const entry = await this._processEntries(appId, secret, body.entry, rawHeaders);
284
327
 
285
328
  return {
286
329
  statusCode: 200,
@@ -293,7 +336,7 @@ class BotApp {
293
336
  };
294
337
  }
295
338
 
296
- async _processEntries (appId, secret, entry = []) {
339
+ async _processEntries (appId, secret, entry = [], headers = {}) {
297
340
  return Promise.all(
298
341
  entry.map(async (event) => {
299
342
  const {
@@ -308,7 +351,8 @@ class BotApp {
308
351
  ...standby.map((e) => ({ ...e, isStandby: true }))
309
352
  ];
310
353
 
311
- const responses = await this._processMessaging(events, pageId, appId, secret, sync);
354
+ const responses = await this
355
+ ._processMessaging(events, pageId, appId, secret, sync, headers);
312
356
 
313
357
  return {
314
358
  id: pageId,
@@ -318,7 +362,7 @@ class BotApp {
318
362
  );
319
363
  }
320
364
 
321
- async _processMessaging (process, pageId, appId, secret, sync) {
365
+ async _processMessaging (process, pageId, appId, secret, sync, headers) {
322
366
  const eventsBySenderId = new Map();
323
367
  process.forEach((e) => this._processMessagingArrayItem(e, eventsBySenderId));
324
368
 
@@ -329,7 +373,7 @@ class BotApp {
329
373
 
330
374
  for (const message of messaging) {
331
375
  const res = await this._processIncommingMessage(
332
- message, senderId, pageId, appId, secret, sync
376
+ message, senderId, pageId, appId, secret, sync, headers
333
377
  );
334
378
 
335
379
  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
  };
@@ -168,6 +168,9 @@ function getSetState (setState = {}, req, res = null, useState = null) {
168
168
  if (entity && (!rear || req.entities.some((e) => e.entity === entity))) {
169
169
  values = req.entities.filter((e) => e.entity === entity)
170
170
  .map((e) => e.value);
171
+ if (values.length === 0 && useState && useState[`@${entity}`]) {
172
+ values = [useState[`@${entity}`]];
173
+ }
171
174
  } else if (variable) {
172
175
  values = toArray(getValue(variable, state));
173
176
  } else if (value === '' || value === null || value === undefined) {
@@ -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
+ };