lob 7.0.1 → 8.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
@@ -3,12 +3,8 @@
3
3
  [downloads-image]: http://img.shields.io/npm/dm/lob.svg
4
4
  [npm-url]: https://npmjs.org/package/lob
5
5
  [npm-image]: https://badge.fury.io/js/lob.svg
6
- [travis-url]: https://travis-ci.org/lob/lob-node
7
- [travis-image]: https://travis-ci.org/lob/lob-node.svg?branch=master
8
- [depstat-url]: https://david-dm.org/Lob/Lob-node
9
- [depstat-image]: https://david-dm.org/Lob/Lob-node.svg
10
6
 
11
- [![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][npm-url] [![Build Status](https://travis-ci.org/lob/lob-node.svg?branch=master)](https://travis-ci.org/lob/lob-node) [![Dependency Status](https://david-dm.org/lob/lob-node.svg)](https://david-dm.org/lob/lob-node) [![Dev Dependency Status](https://david-dm.org/lob/lob-node/dev-status.svg)](https://david-dm.org/lob/lob-node) [![Coverage Status](https://coveralls.io/repos/lob/lob-node/badge.svg?branch=master)](https://coveralls.io/r/lob/lob-node?branch=master)
7
+ [![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][npm-url] [![CI](https://github.com/lob/lob-node/actions/workflows/run_tests.yml/badge.svg)](https://github.com/lob/lob-node/actions/workflows/run_tests.yml) [![Coverage Status](https://coveralls.io/repos/lob/lob-node/badge.svg?branch=master)](https://coveralls.io/r/lob/lob-node?branch=master)
12
8
 
13
9
  Node.js wrapper for the [Lob.com](https://lob.com) API. See full Lob.com documentation [here](https://lob.com/docs/node).
14
10
  ******
@@ -63,6 +59,8 @@ $ git clone git@github.com:lob/lob-node.git
63
59
  $ npm install
64
60
  ```
65
61
 
62
+ **Requirements:** Node.js >= 24.15.0, npm >= 11.5.1
63
+
66
64
  ### Usage
67
65
  ```javascript
68
66
  const Lob = require('lob')('YOUR API KEY');
@@ -146,18 +144,38 @@ To contribute, please see the [CONTRIBUTING.md](https://github.com/lob/lob-node/
146
144
 
147
145
  ## Testing
148
146
 
149
- To run the tests with coverage:
147
+ To run unit tests with coverage:
148
+
149
+ ```
150
+ npm test
151
+ ```
152
+
153
+ To run integration tests (requires API keys):
154
+
155
+ ```
156
+ TEST_API_KEY=your_test_key npm run test:integration
157
+ ```
158
+
159
+ Some integration tests require a live API key:
150
160
 
151
161
  ```
152
- LOB_API_KEY=YOUR_TEST_API_KEY npm test
162
+ TEST_API_KEY=your_test_key LIVE_API_KEY=your_live_key npm run test:integration
153
163
  ```
154
164
 
155
- To run the tests without coverage:
165
+ To run lint:
156
166
 
157
167
  ```
158
- LOB_API_KEY=YOUR_TEST_API_KEY npm run test-no-cover
168
+ npm run lint
159
169
  ```
160
170
 
171
+ To check for vulnerabilities:
172
+
173
+ ```
174
+ npm audit
175
+ ```
176
+
177
+ Target: zero `moderate` and zero `high` findings. Current status: **0 vulnerabilities**.
178
+
161
179
  =======================
162
180
 
163
181
  Copyright © 2013 Lob.com
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,27 +10,32 @@
10
10
  "Lob.com",
11
11
  "printing"
12
12
  ],
13
- "version": "7.0.1",
13
+ "version": "8.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.16.1",
18
+ "form-data": "^4.0.5"
18
19
  },
19
20
  "devDependencies": {
21
+ "@eslint/js": "^10.0.1",
22
+ "@stylistic/eslint-plugin": "^5.10.0",
20
23
  "agentkeepalive": "^4.1.0",
21
- "chai": "^2.2.0",
22
- "coveralls": "^3.0.5",
23
- "cross-env": "^5.2.0",
24
- "csv-parse": "^4.4.6",
25
- "eslint": "^8.6.0",
26
- "eslint-config-lob": "^5.2.0",
24
+ "chai": "^6.2.2",
25
+ "cross-env": "^10.1.0",
26
+ "csv-parse": "^6.2.1",
27
+ "eslint": "^10.4.0",
28
+ "eslint-config-lob": "^7.0.0",
29
+ "eslint-plugin-jsdoc": "^62.9.0",
30
+ "eslint-plugin-lob": "^3.0.2",
27
31
  "generate-changelog": "^1.0.0",
28
- "json-2-csv": "^3.15.1",
29
- "mocha": "^10.0.0",
32
+ "globals": "^17.6.0",
33
+ "json-2-csv": "^5.5.10",
34
+ "mocha": "^11.7.5",
30
35
  "moment": "^2.22.1",
31
- "nyc": "^15.1.0",
32
- "p-map": "^2.1.0",
33
- "uuid": "^3.1.0"
36
+ "nock": "^14.0.15",
37
+ "nyc": "^18.0.0",
38
+ "p-map": "^7.0.4"
34
39
  },
35
40
  "repository": {
36
41
  "type": "git",
@@ -39,12 +44,13 @@
39
44
  "bugs:": "https://github.com/lob/lob-node/issues",
40
45
  "main": "./lib/index",
41
46
  "engines": {
42
- "node": ">= 20.0.0",
47
+ "node": ">= 24.15.0",
43
48
  "npm": ">= 11.5.1"
44
49
  },
45
50
  "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",
51
+ "test": "cross-env NODE_ENV=test nyc mocha test",
52
+ "test:integration": "mocha --config test/integration/.mocharc.json",
53
+ "test-no-cover": "cross-env NODE_ENV=test mocha test",
48
54
  "coverage": "nyc report --reporter=text",
49
55
  "enforce": "nyc check-coverage --statements 100 --lines 100 --functions 100 --branches 100",
50
56
  "test-lcov": "nyc report --reporter=lcov",
@@ -63,5 +69,9 @@
63
69
  "lib",
64
70
  "LICENSE.txt"
65
71
  ],
66
- "license": "MIT"
72
+ "license": "MIT",
73
+ "overrides": {
74
+ "serialize-javascript": "^7.0.5",
75
+ "diff": "^9.0.0"
76
+ }
67
77
  }