lob 7.0.1 → 7.1.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/README.md CHANGED
@@ -146,16 +146,22 @@ To contribute, please see the [CONTRIBUTING.md](https://github.com/lob/lob-node/
146
146
 
147
147
  ## Testing
148
148
 
149
- To run the tests with coverage:
149
+ To run unit tests with coverage:
150
150
 
151
151
  ```
152
- LOB_API_KEY=YOUR_TEST_API_KEY npm test
152
+ npm test
153
153
  ```
154
154
 
155
- To run the tests without coverage:
155
+ To run integration tests (requires API keys):
156
156
 
157
157
  ```
158
- LOB_API_KEY=YOUR_TEST_API_KEY npm run test-no-cover
158
+ TEST_API_KEY=your_test_key npm run test:integration
159
+ ```
160
+
161
+ Some integration tests require a live API key:
162
+
163
+ ```
164
+ TEST_API_KEY=your_test_key LIVE_API_KEY=your_live_key npm run test:integration
159
165
  ```
160
166
 
161
167
  =======================
package/lib/index.js CHANGED
@@ -6,10 +6,10 @@ const Resources = require('./resources');
6
6
  const LOB_HOST = process.env.LOB_HOST || 'https://api.lob.com/v1/';
7
7
  const LOB_USERAGENT = `Lob/v1 NodeBindings/${ClientVersion}`;
8
8
 
9
- const Lob = function (apiKey, options) {
9
+ const LobClient = function (apiKey, options) {
10
10
 
11
- if (!(this instanceof Lob)) {
12
- return new Lob(apiKey, options);
11
+ if (!(this instanceof LobClient)) {
12
+ return new LobClient(apiKey, options);
13
13
  }
14
14
 
15
15
  this.resourceBase = require('./resources/resourceBase');
@@ -28,7 +28,7 @@ const Lob = function (apiKey, options) {
28
28
 
29
29
  if (options && typeof options === 'object') {
30
30
 
31
- if (Object.prototype.hasOwnProperty.call(options, 'apiVersion')) {
31
+ if (Reflect.apply(Object.prototype.hasOwnProperty, options, ['apiVersion'])) {
32
32
  this.options.headers['Lob-Version'] = options.apiVersion;
33
33
  }
34
34
 
@@ -41,7 +41,7 @@ const Lob = function (apiKey, options) {
41
41
  this._initResources();
42
42
  };
43
43
 
44
- (function () {
44
+ Reflect.apply(function () {
45
45
 
46
46
  this._initResources = function () {
47
47
  const services = Object.keys(Resources);
@@ -52,6 +52,6 @@ const Lob = function (apiKey, options) {
52
52
  }
53
53
  };
54
54
 
55
- }).call(Lob.prototype);
55
+ }, LobClient.prototype, []);
56
56
 
57
- module.exports = Lob;
57
+ module.exports = LobClient;
@@ -30,7 +30,7 @@ class BankAccounts extends ResourceBase {
30
30
  }
31
31
 
32
32
  verify (id, params, callback) {
33
- return this._transmit('POST', `${id}/verify`, null, params, callback);
33
+ return this._transmit('POST', `${encodeURIComponent(id)}/verify`, null, params, callback);
34
34
  }
35
35
 
36
36
  }
@@ -69,7 +69,7 @@ class Cards extends ResourceBase {
69
69
  params[`${p}[${key}]`] = params[p][key];
70
70
  }
71
71
 
72
- delete params[p];
72
+ Reflect.deleteProperty(params, p);
73
73
  }
74
74
 
75
75
  return this._transmit('POST', null, null, params, headers, callback);
@@ -55,7 +55,7 @@ class Letters extends ResourceBase {
55
55
  params[`${p}[${key}]`] = params[p][key];
56
56
  }
57
57
 
58
- delete params[p];
58
+ Reflect.deleteProperty(params, p);
59
59
  }
60
60
 
61
61
  return this._transmit('POST', null, null, params, headers, callback);
@@ -66,7 +66,7 @@ class Postcards extends ResourceBase {
66
66
  params[`${p}[${key}]`] = params[p][key];
67
67
  }
68
68
 
69
- delete params[p];
69
+ Reflect.deleteProperty(params, p);
70
70
  }
71
71
 
72
72
  return this._transmit('POST', null, null, params, headers, callback);
@@ -1,7 +1,14 @@
1
1
  'use strict';
2
2
 
3
- const Request = require('request');
4
- const Stream = require('stream');
3
+ const axios = require('axios');
4
+ const FormDataLib = require('form-data');
5
+ const Stream = require('stream');
6
+
7
+ // Helper to create a compatibility response object from axios response
8
+ const createCompatResponse = (axiosResp) => Object.assign({}, axiosResp, {
9
+ statusCode: axiosResp.status,
10
+ statusMessage: axiosResp.statusText
11
+ });
5
12
 
6
13
  class ResourceBase {
7
14
 
@@ -20,94 +27,149 @@ class ResourceBase {
20
27
 
21
28
  const allHeaders = Object.assign({}, this.config.headers, headers);
22
29
 
23
- const opts = {
24
- url: `${this.uri}${uri ? `/${uri}` : ''}`,
30
+ // Encode URI to prevent path traversal, but only for simple IDs
31
+ // Composite paths (containing /) should be encoded by the caller
32
+ const encodedUri = uri && !uri.includes('/') ? encodeURIComponent(uri) : uri;
33
+
34
+ const config = {
35
+ url: `${this.uri}${encodedUri ? `/${encodedUri}` : ''}`,
25
36
  method,
26
- auth: { user: this.config.apiKey, password: '' },
37
+ auth: { username: this.config.apiKey, password: '' },
27
38
  headers: allHeaders,
28
- json: true
39
+ validateStatus: () => true
29
40
  };
30
41
 
31
42
  if (this.config.agent) {
32
- opts.agent = this.config.agent;
43
+ const isHttps = this.uri.startsWith('https');
44
+ if (isHttps) {
45
+ config.httpsAgent = this.config.agent;
46
+ } else {
47
+ config.httpAgent = this.config.agent;
48
+ }
33
49
  }
34
50
 
35
51
  let isMultiPartForm = false;
52
+ let requiresJson = false;
36
53
 
37
- for (const key in form) {
54
+ Object.keys(form || {}).forEach((key) => {
38
55
  if (form[key] === undefined) {
39
- delete form[key];
56
+ Reflect.deleteProperty(form, key);
40
57
  }
41
58
  if (form[key] === true || form[key] === false) {
42
59
  form[key] = form[key].toString();
43
60
  }
44
- }
61
+ });
45
62
 
46
- for (const param in form) {
63
+ Object.keys(form || {}).forEach((param) => {
47
64
  const val = form[param];
48
65
 
49
66
  if (val instanceof Stream.Stream) {
50
67
  isMultiPartForm = true;
51
- break;
52
68
  }
53
69
 
54
- if (val !== undefined && val !== null && Object.prototype.hasOwnProperty.call(val, 'value')) {
70
+ if (val !== undefined && val !== null && Reflect.apply(Object.prototype.hasOwnProperty, val, ['value'])) {
55
71
  isMultiPartForm = true;
56
- break;
57
72
  }
58
- }
73
+
74
+ // Check if array contains objects (requires JSON encoding)
75
+ if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'object') {
76
+ requiresJson = true;
77
+ }
78
+ });
59
79
 
60
80
  if (qs) {
61
- opts.qs = qs;
81
+ config.params = qs;
62
82
  }
63
83
 
64
84
  if (form) {
65
85
  if (isMultiPartForm) {
66
- opts.formData = form;
86
+ const formData = new FormDataLib();
87
+
88
+ Object.keys(form).forEach((key) => {
89
+ const val = form[key];
90
+
91
+ if (val instanceof Stream.Stream) {
92
+ formData.append(key, val);
93
+ } else if (val !== undefined && val !== null && Reflect.apply(Object.prototype.hasOwnProperty, val, ['value'])) {
94
+ formData.append(key, val.value, val.options);
95
+ } else if (val !== undefined && val !== null) {
96
+ formData.append(key, val);
97
+ }
98
+ });
99
+
100
+ config.data = formData;
101
+ config.headers = Object.assign({}, config.headers, formData.getHeaders());
102
+ } else if (requiresJson) {
103
+ // Use JSON for requests with nested objects (e.g., bulk verifications)
104
+ config.data = form;
105
+ config.headers['Content-Type'] = 'application/json';
67
106
  } else {
68
- opts.form = form;
107
+ const params = new URLSearchParams();
108
+ Object.keys(form).forEach((key) => {
109
+ const val = form[key];
110
+ if (val !== undefined && val !== null) {
111
+ if (Array.isArray(val)) {
112
+ // Handle arrays: amounts[0]=23&amounts[1]=34
113
+ val.forEach((item, index) => {
114
+ params.append(`${key}[${index}]`, item);
115
+ });
116
+ } else {
117
+ params.append(key, val);
118
+ }
119
+ }
120
+ });
121
+ config.data = params;
122
+ config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
69
123
  }
70
124
  }
71
125
 
72
- const promise = new Promise((resolve, reject) => {
73
- Request(opts, (err, resp, body) => {
126
+ const promise = axios(config)
127
+ .then((resp) => {
74
128
  /* istanbul ignore next */
75
- body = body || {};
76
-
77
- /* istanbul ignore next */
78
- if (err) {
79
- return reject(err);
80
- }
129
+ const body = resp.data || {};
81
130
 
82
131
  if (body && body.error) {
83
132
  const error = new Error(body.error.message);
84
133
  error.status_code = body.error.status_code;
85
- error._response = resp;
86
- return reject(error);
134
+ error._response = createCompatResponse(resp);
135
+ throw error;
87
136
  }
88
137
 
89
- if (resp && resp.statusCode >= 500) {
90
- const error = new Error(resp.statusMessage);
91
- error.status_code = resp.statusCode;
92
- error._response = resp;
93
- return reject(error);
138
+ if (resp.status >= 500) {
139
+ const error = new Error(resp.statusText);
140
+ error.status_code = resp.status;
141
+ error._response = createCompatResponse(resp);
142
+ throw error;
94
143
  }
95
144
 
96
- Object.defineProperty(body, '_response', {
145
+ const compatResponse = createCompatResponse(resp);
146
+
147
+ Reflect.defineProperty(body, '_response', {
97
148
  enumerable: false,
98
149
  writable: false,
99
- value: resp
150
+ value: compatResponse
100
151
  });
101
152
 
102
- return resolve(body);
153
+ return body;
154
+ })
155
+ .catch((err) => {
156
+ // Re-throw errors that were already processed in .then() block
157
+ // (they have _response attached)
158
+ /* istanbul ignore next */
159
+ if (err._response) {
160
+ throw err;
161
+ }
162
+
163
+ // Network errors (no response from server) - just re-throw
164
+ // Note: HTTP status errors are handled in .then() since validateStatus: () => true
165
+ throw err;
103
166
  });
104
- })
105
167
 
106
168
  if (callback) {
107
- promise.then(body => callback(null, body), err => callback(err));
169
+ promise.then((body) => callback(null, body), (err) => callback(err));
108
170
  }
109
171
 
110
- return promise
172
+ return promise;
111
173
  }
112
174
 
113
175
  }
@@ -66,7 +66,7 @@ class SelfMailers extends ResourceBase {
66
66
  params[`${p}[${key}]`] = params[p][key];
67
67
  }
68
68
 
69
- delete params[p];
69
+ Reflect.deleteProperty(params, p);
70
70
  }
71
71
 
72
72
  return this._transmit('POST', null, null, params, headers, callback);
@@ -4,7 +4,7 @@ const ResourceBase = require('./resourceBase');
4
4
 
5
5
  class Template extends ResourceBase {
6
6
  constructor (config) {
7
- super('templates', config);
7
+ super('templates', config);
8
8
  }
9
9
 
10
10
  list (options, callback) {
@@ -29,4 +29,4 @@ class Template extends ResourceBase {
29
29
  }
30
30
  }
31
31
 
32
- module.exports = Template;
32
+ module.exports = Template;
@@ -14,4 +14,4 @@ class USReverseGeocodeLookups extends ResourceBase {
14
14
 
15
15
  }
16
16
 
17
- module.exports = USReverseGeocodeLookups;
17
+ module.exports = USReverseGeocodeLookups;
package/package.json CHANGED
@@ -10,11 +10,12 @@
10
10
  "Lob.com",
11
11
  "printing"
12
12
  ],
13
- "version": "7.0.1",
13
+ "version": "7.1.0",
14
14
  "homepage": "https://github.com/lob/lob-node",
15
15
  "author": "Lob <support@lob.com> (https://lob.com/)",
16
16
  "dependencies": {
17
- "request": "^2.88.0"
17
+ "axios": "^1.13.2",
18
+ "form-data": "^4.0.5"
18
19
  },
19
20
  "devDependencies": {
20
21
  "agentkeepalive": "^4.1.0",
@@ -28,6 +29,7 @@
28
29
  "json-2-csv": "^3.15.1",
29
30
  "mocha": "^10.0.0",
30
31
  "moment": "^2.22.1",
32
+ "nock": "^14.0.10",
31
33
  "nyc": "^15.1.0",
32
34
  "p-map": "^2.1.0",
33
35
  "uuid": "^3.1.0"
@@ -43,8 +45,9 @@
43
45
  "npm": ">= 11.5.1"
44
46
  },
45
47
  "scripts": {
46
- "test": "cross-env NODE_ENV=test nyc mocha test --recursive --timeout 30000",
47
- "test-no-cover": "cross-env NODE_ENV=test mocha test --recursive --timeout 30000",
48
+ "test": "cross-env NODE_ENV=test nyc mocha test",
49
+ "test:integration": "mocha --config test/integration/.mocharc.json",
50
+ "test-no-cover": "cross-env NODE_ENV=test mocha test",
48
51
  "coverage": "nyc report --reporter=text",
49
52
  "enforce": "nyc check-coverage --statements 100 --lines 100 --functions 100 --branches 100",
50
53
  "test-lcov": "nyc report --reporter=lcov",