postman-runtime 7.30.0 → 7.31.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.
@@ -8,10 +8,11 @@ var _ = require('lodash'),
8
8
  * @constructs AuthInterface
9
9
  * @param {RequestAuth} auth -
10
10
  * @param {Object} protocolProfileBehavior - Protocol profile behaviors
11
+ * @param {Object} [options] - authorizer options
11
12
  * @return {AuthInterface}
12
13
  * @throws {Error}
13
14
  */
14
- createAuthInterface = function (auth, protocolProfileBehavior) {
15
+ createAuthInterface = function (auth, protocolProfileBehavior, options) {
15
16
  if (!(auth && auth.parameters && auth.parameters())) {
16
17
  throw new Error('runtime~createAuthInterface: invalid auth');
17
18
  }
@@ -34,12 +35,25 @@ createAuthInterface = function (auth, protocolProfileBehavior) {
34
35
  var paramVariable;
35
36
 
36
37
  if (_.isString(keys)) {
38
+ // This hacky handling here localizes the impact for refresh token in runtime, we want to avoid
39
+ // changing the auth handler interface in general and localizing this here allows us to refactor
40
+ // easily if we decide to move the oauth2 token generation flow into runtime
41
+ if (keys === 'refreshOAuth2Token') {
42
+ return options && options.refreshOAuth2Token;
43
+ }
44
+
37
45
  paramVariable = auth.parameters().one(keys);
38
46
 
39
47
  return paramVariable && paramVariable.get();
40
48
  }
41
49
  if (_.isArray(keys)) {
42
50
  return _.transform(keys, function (paramObject, key) {
51
+ if (key === 'refreshOAuth2Token') {
52
+ paramObject[key] = options && options.refreshOAuth2Token;
53
+
54
+ return paramObject;
55
+ }
56
+
43
57
  paramVariable = auth.parameters().one(key);
44
58
  paramVariable && (paramObject[key] = paramVariable.get());
45
59
 
@@ -53,13 +67,14 @@ createAuthInterface = function (auth, protocolProfileBehavior) {
53
67
  /**
54
68
  * @param {String|Object} key -
55
69
  * @param {*} [value] -
70
+ * @param {Boolean} [system] - Explicitly allow setting a param as a system param
56
71
  * @return {AuthInterface}
57
72
  * @example
58
73
  * set('foo', 'bar')
59
74
  * set({foo: 'bar', 'alpha': 'beta'})
60
75
  * @throws {Error}
61
76
  */
62
- set: function (key, value) {
77
+ set: function (key, value, system) {
63
78
  var modifiedParams = {},
64
79
  parameters;
65
80
 
@@ -77,6 +92,12 @@ createAuthInterface = function (auth, protocolProfileBehavior) {
77
92
  _.forEach(modifiedParams, function (value, key) {
78
93
  var param = parameters.one(key);
79
94
 
95
+ // If we are updating a user param inside runtime, it is now a system param for this execution and
96
+ // should be marked as such
97
+ if (system) {
98
+ param.system = true;
99
+ }
100
+
80
101
  if (!param) {
81
102
  return parameters.add({ key: key, value: value, system: true });
82
103
  }
@@ -88,7 +88,8 @@ _.forEach({
88
88
  oauth2: require('./oauth2'),
89
89
  ntlm: require('./ntlm'),
90
90
  apikey: require('./apikey'),
91
- edgegrid: require('./edgegrid')
91
+ edgegrid: require('./edgegrid'),
92
+ jwt: require('./jwt')
92
93
  }, AuthLoader.addHandler);
93
94
 
94
95
  /**
@@ -0,0 +1,251 @@
1
+ const _ = require('lodash'),
2
+ jose = require('jose'),
3
+
4
+ // jwt key constants
5
+ BEARER_AUTH_PREFIX = 'Bearer',
6
+ QUERY_KEY = 'token',
7
+ AUTH_KEYS = {
8
+ ALGORITHM: 'algorithm',
9
+ HEADER: 'header',
10
+ HEADER_ALGORITHM: 'alg',
11
+ PAYLOAD: 'payload',
12
+ SECRET: 'secret',
13
+ IS_SECRET_BASE_64_ENCODED: 'isSecretBase64Encoded',
14
+ PRIVATE_KEY: 'privateKey',
15
+ ADD_TOKEN_TO: 'addTokenTo',
16
+ HEADER_PREFIX: 'headerPrefix',
17
+ QUERY_PARAM_KEY: 'queryParamKey'
18
+ },
19
+ ADD_TOKEN_TO_TARGETS = {
20
+ HEADER: 'header',
21
+ QUERY_PARAM: 'queryParam'
22
+ },
23
+ AUTHORIZATION = 'Authorization',
24
+ BASE64 = 'base64',
25
+ ASCII = 'ascii',
26
+ SPACE = ' ',
27
+
28
+ // HS Algorithms
29
+ HS_ALGORITHMS = {
30
+ HS256: 'HS256',
31
+ HS384: 'HS384',
32
+ HS512: 'HS512'
33
+ },
34
+
35
+ // algorithms supported
36
+ ALGORITHMS_SUPPORTED = {
37
+ ...HS_ALGORITHMS,
38
+ RS256: 'RS256',
39
+ RS384: 'RS384',
40
+ RS512: 'RS512',
41
+ PS256: 'PS256',
42
+ PS384: 'PS384',
43
+ PS512: 'PS512',
44
+ ES256: 'ES256',
45
+ ES384: 'ES384',
46
+ ES512: 'ES512'
47
+ };
48
+
49
+ /**
50
+ * add the JWT Token to the request in auth header or query param
51
+ *
52
+ * @param {AuthInterface} auth - auth
53
+ * @param {Request} request - request
54
+ * @param {string} jwtToken - base64encoded jwt token
55
+ */
56
+ function addTokenToRequest (auth, request, jwtToken) {
57
+ const addTokenTo = auth.get(AUTH_KEYS.ADD_TOKEN_TO) || ADD_TOKEN_TO_TARGETS.HEADER,
58
+ queryParamKey = auth.get(AUTH_KEYS.QUERY_PARAM_KEY) || QUERY_KEY,
59
+ headerPrefix = auth.get(AUTH_KEYS.HEADER_PREFIX) || BEARER_AUTH_PREFIX;
60
+
61
+ if (addTokenTo === ADD_TOKEN_TO_TARGETS.HEADER) {
62
+ request.removeHeader(AUTHORIZATION, { ignoreCase: true });
63
+
64
+ request.addHeader({
65
+ key: AUTHORIZATION,
66
+ value: headerPrefix + SPACE + jwtToken,
67
+ system: true
68
+ });
69
+ }
70
+ else if (addTokenTo === ADD_TOKEN_TO_TARGETS.QUERY_PARAM) {
71
+ request.url.query.remove(function (query) {
72
+ return query && query.key === queryParamKey;
73
+ });
74
+
75
+ request.url.query.add({
76
+ key: queryParamKey,
77
+ value: jwtToken,
78
+ system: true
79
+ });
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Request auth payload structure
85
+ * request:{
86
+ * auth:{
87
+ * type:'jwt',
88
+ * jwt:{
89
+ * algorithm: <string> - ALGORITHMS_SUPPORTED,
90
+ * header: <JSON string> | JSON Object
91
+ * payload: <JSON string> | JSON Object
92
+ * secret: <string> - secret for HS algorithms
93
+ * isSecretBase64Encoded: <boolean> - optional property used when <secret> for HS algorithms
94
+ * is encoded in base64 format
95
+ * privateKey: <string> - PEM format private key for RS, PS, ES algorithms
96
+ * addTokenTo: <string> - possible values - header | queryParam,
97
+ * headerPrefix: <string> - prefix added before jwt token in header - Default Bearer
98
+ * queryParamKey: <string> - optional property added when <addTokenTo> set to [queryParam],
99
+ * }
100
+ * }
101
+ * }
102
+ */
103
+ /**
104
+ * @implements {AuthHandlerInterface}
105
+ */
106
+ module.exports = {
107
+ /**
108
+ * @property {AuthHandlerInterface~AuthManifest}
109
+ */
110
+ manifest: {
111
+ info: {
112
+ name: 'jwt',
113
+ version: '1.0.0'
114
+ },
115
+ updates: [
116
+ {
117
+ property: 'Authorization',
118
+ type: 'header'
119
+ },
120
+ {
121
+ property: '*',
122
+ type: 'url.param'
123
+ }
124
+ ]
125
+ },
126
+
127
+ /**
128
+ * Initializes an item (extracts parameters from intermediate requests if any, etc)
129
+ * before the actual authorization step
130
+ *
131
+ * @param {AuthInterface} auth -
132
+ * @param {Response} response -
133
+ * @param {AuthHandlerInterface~authInitHookCallback} done -
134
+ */
135
+ init: function (auth, response, done) {
136
+ done();
137
+ },
138
+
139
+ /**
140
+ * Checks the item, and fetches any parameters that are not already provided.
141
+ *
142
+ * @param {AuthInterface} auth -
143
+ * @param {AuthHandlerInterface~authPreHookCallback} done -
144
+ */
145
+ pre: function (auth, done) {
146
+ return done(null, true);
147
+ },
148
+
149
+ /**
150
+ * Verifies whether the auth succeeded
151
+ *
152
+ * @param {AuthInterface} auth -
153
+ * @param {Response} response -
154
+ * @param {AuthHandlerInterface~authPostHookCallback} done -
155
+ */
156
+ post: function (auth, response, done) {
157
+ done(null, true);
158
+ },
159
+
160
+ /**
161
+ * Signs the request
162
+ *
163
+ * @param {AuthInterface} auth -
164
+ * @param {Request} request -
165
+ * @param {AuthHandlerInterface~authSignHookCallback} done -
166
+ */
167
+ sign: function (auth, request, done) {
168
+ const algorithm = auth.get(AUTH_KEYS.ALGORITHM),
169
+ secret = auth.get(AUTH_KEYS.SECRET),
170
+ privateKey = auth.get(AUTH_KEYS.PRIVATE_KEY),
171
+ isHsAlgorithm = HS_ALGORITHMS[algorithm];
172
+
173
+ // bail out - invalid algorithm
174
+ if (!ALGORITHMS_SUPPORTED[algorithm]) {
175
+ return done(new Error('invalid algorithm'));
176
+ }
177
+
178
+ // bail out - secret not a valid string
179
+ if (isHsAlgorithm && (!secret || !_.isString(secret))) {
180
+ return done(new Error('Invalid secret key. Enter a valid key.'));
181
+ }
182
+
183
+ // bail out - private key not a valid string
184
+ if (!isHsAlgorithm && (!privateKey || !_.isString(privateKey))) {
185
+ return done(new Error('Invalid private key. Enter a valid key.'));
186
+ }
187
+
188
+ try {
189
+ const rawHeader = auth.get(AUTH_KEYS.HEADER),
190
+ rawPayload = auth.get(AUTH_KEYS.PAYLOAD);
191
+
192
+ let header = rawHeader,
193
+ payload = rawPayload;
194
+
195
+ if (typeof rawHeader === 'string') {
196
+ const trimmedHeader = rawHeader.trim();
197
+
198
+ header = trimmedHeader && JSON.parse(trimmedHeader);
199
+ }
200
+
201
+ // treat root level alg as source of truth.
202
+ // If header is not set, use empty object.
203
+ header = header ? { ...header, alg: algorithm } : { alg: algorithm };
204
+
205
+ if (typeof rawPayload === 'string') {
206
+ const trimmedPayload = rawPayload.trim();
207
+
208
+ payload = trimmedPayload ? JSON.parse(trimmedPayload) : {};
209
+ }
210
+
211
+ // HS Algorithms use secret for token generation
212
+ if (isHsAlgorithm) {
213
+ const isBase64 = auth.get(AUTH_KEYS.IS_SECRET_BASE_64_ENCODED),
214
+ rawSecret = isBase64 ? Buffer.from(secret, BASE64).toString(ASCII) : secret;
215
+
216
+ new jose.SignJWT(payload)
217
+ .setProtectedHeader(header)
218
+ .sign(new TextEncoder().encode(rawSecret))
219
+ .then((token) => {
220
+ addTokenToRequest(auth, request, token);
221
+
222
+ return done();
223
+ })
224
+ .catch((err) => {
225
+ done(err);
226
+ });
227
+ }
228
+
229
+ // RS,PS,ES Algorithms use private key for token generation
230
+ else {
231
+ jose.importPKCS8(privateKey, algorithm)
232
+ .then((signKey) => {
233
+ return new jose.SignJWT(payload)
234
+ .setProtectedHeader(header)
235
+ .sign(signKey);
236
+ })
237
+ .then((token) => {
238
+ addTokenToRequest(auth, request, token);
239
+
240
+ return done();
241
+ })
242
+ .catch((err) => {
243
+ done(err);
244
+ });
245
+ }
246
+ }
247
+ catch (err) {
248
+ done(err);
249
+ }
250
+ }
251
+ };
@@ -58,7 +58,19 @@ module.exports = {
58
58
  * @param {AuthHandlerInterface~authPreHookCallback} done -
59
59
  */
60
60
  pre: function (auth, done) {
61
- done(null, Boolean(auth.get('accessToken')));
61
+ const id = auth.get('id'),
62
+ refreshOAuth2Token = auth.get('refreshOAuth2Token');
63
+
64
+ if (!(id && _.isFunction(refreshOAuth2Token))) {
65
+ return done(null, Boolean(auth.get('accessToken')));
66
+ }
67
+
68
+ refreshOAuth2Token(id, (_, accessToken) => {
69
+ accessToken && auth.set('accessToken', accessToken, true);
70
+
71
+ // fallback to existing token
72
+ done(null, Boolean(auth.get('accessToken')));
73
+ });
62
74
  },
63
75
 
64
76
  /**
@@ -90,7 +90,7 @@ function forEachAsync (items, fn, cb) {
90
90
  // request
91
91
  //
92
92
 
93
- function request(originalRequest, options, onStart, callback) {
93
+ function request(originalRequest, options, onStart, onData, callback) {
94
94
  var options_onResponse = options.onResponse; // Save this for later.
95
95
  var XHR = _.get(options, ['agents', options.url && options.url.protocol.slice(0, -1), 'agentClass']) || XMLHttpRequest;
96
96
 
@@ -115,6 +115,7 @@ function request(originalRequest, options, onStart, callback) {
115
115
  return callback(new Error("options.uri must be a string"));
116
116
 
117
117
  options.onStart = onStart
118
+ options.onData = onData // NOTE: not implemented
118
119
  options.callback = callback
119
120
  options.method = options.method || 'GET';
120
121
  options.headers = _.reduce(options.headers || {}, function (accumulator, value, key) {
@@ -68,7 +68,7 @@ var _ = require('lodash'),
68
68
  // @todo trigger console warning (using callback) if not enabled.
69
69
  requests.enableNodeExtraCACerts();
70
70
 
71
- module.exports = function (request, options, onStart, callback) {
71
+ module.exports = function (request, options, onStart, onData, callback) {
72
72
  var req = {};
73
73
 
74
74
  async.waterfall([
@@ -88,6 +88,9 @@ module.exports = function (request, options, onStart, callback) {
88
88
 
89
89
  // emit responseStart event
90
90
  request.on('response', onStart);
91
+
92
+ // emit responseData event
93
+ request.on('data', onData);
91
94
  });
92
95
 
93
96
  return req;
@@ -6,8 +6,10 @@ var _ = require('lodash'),
6
6
  sdk = require('postman-collection'),
7
7
  requests = require('./request-wrapper'),
8
8
  dryRun = require('./dry-run'),
9
+ SSEProcessor = require('./sse-processor'),
9
10
 
10
11
  RESPONSE_START_EVENT_BASE = 'response.start.',
12
+ RESPONSE_DATA_EVENT_BASE = 'response.data.',
11
13
  RESPONSE_END_EVENT_BASE = 'response.end.',
12
14
 
13
15
  RESPONSE_START = 'responseStart',
@@ -138,6 +140,10 @@ class Requester extends EventEmitter {
138
140
  if (!_.isFinite(this.options.timeout)) {
139
141
  this.options.timeout = undefined;
140
142
  }
143
+
144
+ // Tracks server sent events
145
+ this.isSSEStream = false;
146
+ this.sseProcessor = null;
141
147
  }
142
148
 
143
149
  /**
@@ -299,6 +305,24 @@ class Requester extends EventEmitter {
299
305
  };
300
306
  },
301
307
 
308
+ onData = function (data) {
309
+ const emit = (data) => {
310
+ self.emit(RESPONSE_DATA_EVENT_BASE + id, data);
311
+ };
312
+
313
+ if (self.isSSEStream) {
314
+ if (!self.sseProcessor) {
315
+ self.sseProcessor = new SSEProcessor(emit);
316
+ }
317
+
318
+ self.sseProcessor.onData(data);
319
+ }
320
+ else {
321
+ // TODO: Enable emit for regular HTTP requests
322
+ // emit(data);
323
+ }
324
+ },
325
+
302
326
  /**
303
327
  * Helper function to trigger `responseStart` callback and
304
328
  * - transform postman-request response instance to SDK Response
@@ -337,6 +361,9 @@ class Requester extends EventEmitter {
337
361
  header: responseHeaders
338
362
  });
339
363
 
364
+ self.isSSEStream = String(sdkResponse.headers.get('content-type'))
365
+ .toLowerCase() === 'text/event-stream';
366
+
340
367
  // prepare history from request debug data
341
368
  history = getExecutionHistory(_.get(response, 'request._debug'));
342
369
 
@@ -396,7 +423,7 @@ class Requester extends EventEmitter {
396
423
  return onEnd(new Error(ERROR_RESTRICTED_ADDRESS + hostname));
397
424
  }
398
425
 
399
- return requests(request, requestOptions, onStart, function (err, res, resBody, debug) {
426
+ return requests(request, requestOptions, onStart, onData, function (err, res, resBody, debug) {
400
427
  // prepare history from request debug data
401
428
  var history = getExecutionHistory(debug),
402
429
  responseTime,
@@ -0,0 +1,129 @@
1
+ const bom = [239, 187, 191],
2
+ colon = 58,
3
+ lineFeed = 10,
4
+ carriageReturn = 13,
5
+
6
+ // Beyond 256KB we could not observe any gain in performance
7
+ maxBufferAheadAllocation = 1024 * 256;
8
+
9
+
10
+ function hasBom (buf) {
11
+ return bom.every((charCode, index) => {
12
+ return buf[index] === charCode;
13
+ });
14
+ }
15
+
16
+ // Adapted from https://github.com/EventSource/eventsource
17
+ class SSEStream {
18
+ constructor (emit) {
19
+ this.emit = emit;
20
+
21
+ this.buf = undefined;
22
+ this.newBuffer = undefined;
23
+ this.startingPos = 0;
24
+ this.startingFieldLength = -1;
25
+ this.newBufferSize = 0;
26
+ this.bytesUsed = 0;
27
+ this.discardTrailingNewline = false;
28
+
29
+ // Accumulates the event data util a new line is encountered
30
+ this.eventBuffer = undefined;
31
+ }
32
+
33
+ onData (chunk) {
34
+ if (!this.buf) {
35
+ this.buf = chunk;
36
+ if (hasBom(this.buf)) {
37
+ this.buf = this.buf.slice(bom.length);
38
+ }
39
+
40
+ this.bytesUsed = this.buf.length;
41
+ }
42
+ else {
43
+ if (chunk.length > this.buf.length - this.bytesUsed) {
44
+ this.newBufferSize = (this.buf.length * 2) + chunk.length;
45
+
46
+ if (this.newBufferSize > maxBufferAheadAllocation) {
47
+ this.newBufferSize = this.buf.length + chunk.length + maxBufferAheadAllocation;
48
+ }
49
+
50
+ this.newBuffer = Buffer.alloc(this.newBufferSize);
51
+ this.buf.copy(this.newBuffer, 0, 0, this.bytesUsed);
52
+ this.buf = this.newBuffer;
53
+ }
54
+
55
+ chunk.copy(this.buf, this.bytesUsed);
56
+ this.bytesUsed += chunk.length;
57
+ }
58
+
59
+ let pos = 0,
60
+ length = this.bytesUsed;
61
+
62
+ while (pos < length) {
63
+ if (this.discardTrailingNewline) {
64
+ if (this.buf[pos] === lineFeed) {
65
+ ++pos;
66
+ }
67
+
68
+ this.discardTrailingNewline = false;
69
+ }
70
+
71
+ let lineLength = -1,
72
+ fieldLength = this.startingFieldLength,
73
+ c;
74
+
75
+ for (let i = this.startingPos; lineLength < 0 && i < length; ++i) {
76
+ c = this.buf[i];
77
+
78
+ if (c === colon) {
79
+ if (fieldLength < 0) {
80
+ fieldLength = i - pos;
81
+ }
82
+ }
83
+ else if (c === carriageReturn) {
84
+ this.discardTrailingNewline = true;
85
+ lineLength = i - pos;
86
+ }
87
+ else if (c === lineFeed) {
88
+ lineLength = i - pos;
89
+ }
90
+ }
91
+
92
+ if (lineLength < 0) {
93
+ this.startingPos = length - pos;
94
+ this.startingFieldLength = fieldLength;
95
+ break;
96
+ }
97
+ else {
98
+ this.startingPos = 0;
99
+ this.startingFieldLength = -1;
100
+ }
101
+
102
+ // Dispatch event when a new line is encountered
103
+ if (lineLength === 0) {
104
+ this.eventBuffer = Buffer.from([...(this.eventBuffer || []), ...Buffer.from([lineFeed])]);
105
+
106
+ this.emit(this.eventBuffer);
107
+ this.eventBuffer = undefined;
108
+ }
109
+ else {
110
+ const bufToCopy = this.buf.slice(pos, pos + lineLength + 1);
111
+
112
+ this.eventBuffer = Buffer.from([...(this.eventBuffer || []), ...bufToCopy]);
113
+ }
114
+
115
+ pos += lineLength + 1;
116
+ }
117
+
118
+ if (pos === length) {
119
+ this.buf = undefined;
120
+ this.bytesUsed = 0;
121
+ }
122
+ else if (pos > 0) {
123
+ this.buf = this.buf.slice(pos, this.bytesUsed);
124
+ this.bytesUsed = this.buf.length;
125
+ }
126
+ }
127
+ }
128
+
129
+ module.exports = SSEStream;
@@ -6,7 +6,7 @@ var _ = require('lodash'),
6
6
  sdk = require('postman-collection'),
7
7
  sandbox = require('postman-sandbox'),
8
8
  serialisedError = require('serialised-error'),
9
- ToughCookie = require('tough-cookie').Cookie,
9
+ ToughCookie = require('@postman/tough-cookie').Cookie,
10
10
 
11
11
  createItemContext = require('../create-item-context'),
12
12
 
@@ -328,9 +328,6 @@ module.exports = {
328
328
 
329
329
  !Array.isArray(args) && (args = []);
330
330
 
331
- // set expected args length to make sure callback is always called
332
- args.length = cookieStore[fnName].length - 1;
333
-
334
331
  // there's no way cookie store can identify the difference
335
332
  // between regular and programmatic access. So, for now
336
333
  // we check for programmatic access using the cookieJar
@@ -13,6 +13,7 @@ var _ = require('lodash'),
13
13
  RequesterPool = require('../../requester').RequesterPool,
14
14
 
15
15
  RESPONSE_START_EVENT_BASE = 'response.start.',
16
+ RESPONSE_DATA_EVENT_BASE = 'response.data.',
16
17
  RESPONSE_END_EVENT_BASE = 'response.end.';
17
18
 
18
19
  module.exports = {
@@ -25,7 +26,7 @@ module.exports = {
25
26
 
26
27
  // the http trigger is actually directly triggered by the requester
27
28
  // todo - figure out whether we should trigger it from here rather than the requester.
28
- triggers: ['beforeRequest', 'request', 'responseStart', 'io'],
29
+ triggers: ['beforeRequest', 'request', 'responseStart', 'responseData', 'io'],
29
30
 
30
31
  process: {
31
32
  /**
@@ -148,6 +149,9 @@ module.exports = {
148
149
 
149
150
  requester.on(RESPONSE_END_EVENT_BASE + requestId, self.triggers.io.bind(self.triggers));
150
151
 
152
+ requester.on(RESPONSE_DATA_EVENT_BASE + requestId, function (data) {
153
+ self.triggers.responseData(context.coords, data);
154
+ });
151
155
  // eslint-disable-next-line max-len
152
156
  xhr = requester.request(requestId, item.request, context.protocolProfileBehavior, function (err, res, req, cookies, history) {
153
157
  err = err || null;