UNPKG

11.5 kBJavaScriptView Raw
1'use strict';
2
3const EventEmitter = require('events').EventEmitter;
4const qs = require('qs');
5const crypto = require('crypto');
6
7const hasOwn = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
8
9// Certain sandboxed environments (our known example right now are CloudFlare
10// Workers) may make `child_process` unavailable. Because `exec` isn't critical
11// to the operation of stripe-node, we handle this unavailability gracefully.
12let exec = null;
13try {
14 exec = require('child_process').exec;
15} catch (e) {
16 if (e.code !== 'MODULE_NOT_FOUND') {
17 throw e;
18 }
19}
20
21const OPTIONS_KEYS = [
22 'apiKey',
23 'idempotencyKey',
24 'stripeAccount',
25 'apiVersion',
26 'maxNetworkRetries',
27 'timeout',
28];
29
30const DEPRECATED_OPTIONS = {
31 api_key: 'apiKey',
32 idempotency_key: 'idempotencyKey',
33 stripe_account: 'stripeAccount',
34 stripe_version: 'apiVersion',
35 stripeVersion: 'apiVersion',
36};
37const DEPRECATED_OPTIONS_KEYS = Object.keys(DEPRECATED_OPTIONS);
38
39const utils = (module.exports = {
40 isOptionsHash(o) {
41 return (
42 o &&
43 typeof o === 'object' &&
44 (OPTIONS_KEYS.some((prop) => hasOwn(o, prop)) ||
45 DEPRECATED_OPTIONS_KEYS.some((prop) => hasOwn(o, prop)))
46 );
47 },
48
49 /**
50 * Stringifies an Object, accommodating nested objects
51 * (forming the conventional key 'parent[child]=value')
52 */
53 stringifyRequestData: (data) => {
54 return (
55 qs
56 .stringify(data, {
57 serializeDate: (d) => Math.floor(d.getTime() / 1000),
58 })
59 // Don't use strict form encoding by changing the square bracket control
60 // characters back to their literals. This is fine by the server, and
61 // makes these parameter strings easier to read.
62 .replace(/%5B/g, '[')
63 .replace(/%5D/g, ']')
64 );
65 },
66
67 /**
68 * Outputs a new function with interpolated object property values.
69 * Use like so:
70 * var fn = makeURLInterpolator('some/url/{param1}/{param2}');
71 * fn({ param1: 123, param2: 456 }); // => 'some/url/123/456'
72 */
73 makeURLInterpolator: (() => {
74 const rc = {
75 '\n': '\\n',
76 '"': '\\"',
77 '\u2028': '\\u2028',
78 '\u2029': '\\u2029',
79 };
80 return (str) => {
81 const cleanString = str.replace(/["\n\r\u2028\u2029]/g, ($0) => rc[$0]);
82 return (outputs) => {
83 return cleanString.replace(/\{([\s\S]+?)\}/g, ($0, $1) =>
84 encodeURIComponent(outputs[$1] || '')
85 );
86 };
87 };
88 })(),
89
90 extractUrlParams: (path) => {
91 const params = path.match(/\{\w+\}/g);
92 if (!params) {
93 return [];
94 }
95
96 return params.map((param) => param.replace(/[{}]/g, ''));
97 },
98
99 /**
100 * Return the data argument from a list of arguments
101 *
102 * @param {object[]} args
103 * @returns {object}
104 */
105 getDataFromArgs(args) {
106 if (!Array.isArray(args) || !args[0] || typeof args[0] !== 'object') {
107 return {};
108 }
109
110 if (!utils.isOptionsHash(args[0])) {
111 return args.shift();
112 }
113
114 const argKeys = Object.keys(args[0]);
115
116 const optionKeysInArgs = argKeys.filter((key) =>
117 OPTIONS_KEYS.includes(key)
118 );
119
120 // In some cases options may be the provided as the first argument.
121 // Here we're detecting a case where there are two distinct arguments
122 // (the first being args and the second options) and with known
123 // option keys in the first so that we can warn the user about it.
124 if (
125 optionKeysInArgs.length > 0 &&
126 optionKeysInArgs.length !== argKeys.length
127 ) {
128 emitWarning(
129 `Options found in arguments (${optionKeysInArgs.join(
130 ', '
131 )}). Did you mean to pass an options object? See https://github.com/stripe/stripe-node/wiki/Passing-Options.`
132 );
133 }
134
135 return {};
136 },
137
138 /**
139 * Return the options hash from a list of arguments
140 */
141 getOptionsFromArgs: (args) => {
142 const opts = {
143 auth: null,
144 headers: {},
145 settings: {},
146 };
147 if (args.length > 0) {
148 const arg = args[args.length - 1];
149 if (typeof arg === 'string') {
150 opts.auth = args.pop();
151 } else if (utils.isOptionsHash(arg)) {
152 const params = args.pop();
153
154 const extraKeys = Object.keys(params).filter(
155 (key) => !OPTIONS_KEYS.includes(key)
156 );
157
158 if (extraKeys.length) {
159 const nonDeprecated = extraKeys.filter((key) => {
160 if (!DEPRECATED_OPTIONS[key]) {
161 return true;
162 }
163 const newParam = DEPRECATED_OPTIONS[key];
164 if (params[newParam]) {
165 throw Error(
166 `Both '${newParam}' and '${key}' were provided; please remove '${key}', which is deprecated.`
167 );
168 }
169 /**
170 * TODO turn this into a hard error in a future major version (once we have fixed our docs).
171 */
172 emitWarning(`'${key}' is deprecated; use '${newParam}' instead.`);
173 params[newParam] = params[key];
174 });
175 if (nonDeprecated.length) {
176 emitWarning(
177 `Invalid options found (${extraKeys.join(', ')}); ignoring.`
178 );
179 }
180 }
181
182 if (params.apiKey) {
183 opts.auth = params.apiKey;
184 }
185 if (params.idempotencyKey) {
186 opts.headers['Idempotency-Key'] = params.idempotencyKey;
187 }
188 if (params.stripeAccount) {
189 opts.headers['Stripe-Account'] = params.stripeAccount;
190 }
191 if (params.apiVersion) {
192 opts.headers['Stripe-Version'] = params.apiVersion;
193 }
194 if (Number.isInteger(params.maxNetworkRetries)) {
195 opts.settings.maxNetworkRetries = params.maxNetworkRetries;
196 }
197 if (Number.isInteger(params.timeout)) {
198 opts.settings.timeout = params.timeout;
199 }
200 }
201 }
202 return opts;
203 },
204
205 /**
206 * Provide simple "Class" extension mechanism
207 */
208 protoExtend(sub) {
209 const Super = this;
210 const Constructor = hasOwn(sub, 'constructor')
211 ? sub.constructor
212 : function(...args) {
213 Super.apply(this, args);
214 };
215
216 // This initialization logic is somewhat sensitive to be compatible with
217 // divergent JS implementations like the one found in Qt. See here for more
218 // context:
219 //
220 // https://github.com/stripe/stripe-node/pull/334
221 Object.assign(Constructor, Super);
222 Constructor.prototype = Object.create(Super.prototype);
223 Object.assign(Constructor.prototype, sub);
224
225 return Constructor;
226 },
227
228 /**
229 * Secure compare, from https://github.com/freewil/scmp
230 */
231 secureCompare: (a, b) => {
232 a = Buffer.from(a);
233 b = Buffer.from(b);
234
235 // return early here if buffer lengths are not equal since timingSafeEqual
236 // will throw if buffer lengths are not equal
237 if (a.length !== b.length) {
238 return false;
239 }
240
241 // use crypto.timingSafeEqual if available (since Node.js v6.6.0),
242 // otherwise use our own scmp-internal function.
243 if (crypto.timingSafeEqual) {
244 return crypto.timingSafeEqual(a, b);
245 }
246
247 const len = a.length;
248 let result = 0;
249
250 for (let i = 0; i < len; ++i) {
251 result |= a[i] ^ b[i];
252 }
253 return result === 0;
254 },
255
256 /**
257 * Remove empty values from an object
258 */
259 removeNullish: (obj) => {
260 if (typeof obj !== 'object') {
261 throw new Error('Argument must be an object');
262 }
263
264 return Object.keys(obj).reduce((result, key) => {
265 if (obj[key] != null) {
266 result[key] = obj[key];
267 }
268 return result;
269 }, {});
270 },
271
272 /**
273 * Normalize standard HTTP Headers:
274 * {'foo-bar': 'hi'}
275 * becomes
276 * {'Foo-Bar': 'hi'}
277 */
278 normalizeHeaders: (obj) => {
279 if (!(obj && typeof obj === 'object')) {
280 return obj;
281 }
282
283 return Object.keys(obj).reduce((result, header) => {
284 result[utils.normalizeHeader(header)] = obj[header];
285 return result;
286 }, {});
287 },
288
289 /**
290 * Stolen from https://github.com/marten-de-vries/header-case-normalizer/blob/master/index.js#L36-L41
291 * without the exceptions which are irrelevant to us.
292 */
293 normalizeHeader: (header) => {
294 return header
295 .split('-')
296 .map(
297 (text) => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase()
298 )
299 .join('-');
300 },
301
302 /**
303 * Determine if file data is a derivative of EventEmitter class.
304 * https://nodejs.org/api/events.html#events_events
305 */
306 checkForStream: (obj) => {
307 if (obj.file && obj.file.data) {
308 return obj.file.data instanceof EventEmitter;
309 }
310 return false;
311 },
312
313 callbackifyPromiseWithTimeout: (promise, callback) => {
314 if (callback) {
315 // Ensure callback is called outside of promise stack.
316 return promise.then(
317 (res) => {
318 setTimeout(() => {
319 callback(null, res);
320 }, 0);
321 },
322 (err) => {
323 setTimeout(() => {
324 callback(err, null);
325 }, 0);
326 }
327 );
328 }
329
330 return promise;
331 },
332
333 /**
334 * Allow for special capitalization cases (such as OAuth)
335 */
336 pascalToCamelCase: (name) => {
337 if (name === 'OAuth') {
338 return 'oauth';
339 } else {
340 return name[0].toLowerCase() + name.substring(1);
341 }
342 },
343
344 emitWarning,
345
346 /**
347 * Node's built in `exec` function sometimes throws outright,
348 * and sometimes has a callback with an error,
349 * depending on the type of error.
350 *
351 * This unifies that interface.
352 */
353 safeExec: (cmd, cb) => {
354 // Occurs if we couldn't load the `child_process` module, which might
355 // happen in certain sandboxed environments like a CloudFlare Worker.
356 if (utils._exec === null) {
357 cb(new Error('exec not available'), null);
358 return;
359 }
360
361 try {
362 utils._exec(cmd, cb);
363 } catch (e) {
364 cb(e, null);
365 }
366 },
367
368 // For mocking in tests.
369 _exec: exec,
370
371 isObject: (obj) => {
372 const type = typeof obj;
373 return (type === 'function' || type === 'object') && !!obj;
374 },
375
376 // For use in multipart requests
377 flattenAndStringify: (data) => {
378 const result = {};
379
380 const step = (obj, prevKey) => {
381 Object.keys(obj).forEach((key) => {
382 const value = obj[key];
383
384 const newKey = prevKey ? `${prevKey}[${key}]` : key;
385
386 if (utils.isObject(value)) {
387 if (!Buffer.isBuffer(value) && !value.hasOwnProperty('data')) {
388 // Non-buffer non-file Objects are recursively flattened
389 return step(value, newKey);
390 } else {
391 // Buffers and file objects are stored without modification
392 result[newKey] = value;
393 }
394 } else {
395 // Primitives are converted to strings
396 result[newKey] = String(value);
397 }
398 });
399 };
400
401 step(data);
402
403 return result;
404 },
405
406 /**
407 * https://stackoverflow.com/a/2117523
408 */
409 uuid4: () => {
410 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
411 const r = (Math.random() * 16) | 0;
412 const v = c === 'x' ? r : (r & 0x3) | 0x8;
413 return v.toString(16);
414 });
415 },
416
417 validateInteger: (name, n, defaultVal) => {
418 if (!Number.isInteger(n)) {
419 if (defaultVal !== undefined) {
420 return defaultVal;
421 } else {
422 throw new Error(`${name} must be an integer`);
423 }
424 }
425
426 return n;
427 },
428});
429
430function emitWarning(warning) {
431 if (typeof process.emitWarning !== 'function') {
432 return console.warn(
433 `Stripe: ${warning}`
434 ); /* eslint-disable-line no-console */
435 }
436
437 return process.emitWarning(warning, 'Stripe');
438}