leankit-client
Advanced tools
Comparing version 2.0.2 to 2.1.0
module.exports = { | ||
extends: [ "leankit", "leankit/es6" ], | ||
parserOptions: { | ||
ecmaVersion: 2017 | ||
}, | ||
rules: { | ||
@@ -4,0 +7,0 @@ "no-unused-expressions": 2, |
@@ -17,3 +17,3 @@ { | ||
"license": "SEE LICENSE IN https://github.com/LeanKit/leankit-node-client/blob/master/LICENSE", | ||
"version": "2.0.2", | ||
"version": "2.1.0", | ||
"homepage": "https://github.com/LeanKit/leankit-node-client/", | ||
@@ -53,2 +53,3 @@ "bugs": { | ||
"test": "mocha -r ./spec/setup --reporter spec ./spec/*.spec.js", | ||
"test:watch": "npm run test -- -w", | ||
"scratch-tests": "npm run lint && node ./scratch" | ||
@@ -55,0 +56,0 @@ }, |
@@ -26,3 +26,3 @@ ## LeanKit API Client for Node.js | ||
account: "account-name", // change these properties to match your account | ||
email: "your@email.com", | ||
email: "your@email.com", // for token auth, see below | ||
password: "your-p@ssw0rd" | ||
@@ -39,2 +39,19 @@ }; | ||
### Authentication | ||
The LeanKit API Client supports both "Basic" and "Bearer" authentication. | ||
* provide a valid email and password for basic authentication | ||
* provide a valid token to use bearer authentication | ||
For most production uses, it's recommended that you use token authentication so that you don't need to worry about protecting your password. See the API section [Auth Tokens](#auth-tokens) for information on how to create and manage auth tokens. | ||
Using a token is simple: | ||
```javascript | ||
const auth = { | ||
account: "account-name", | ||
token: "2a0ec41e4fca6a10727a33je4f409545870c0ce199fd8cde287a027acdf671d73da3f5af1ee983441d1993dc3a2181302690885c0b46692e39b6c9e29bd132eb" | ||
} | ||
const client = LeanKitClient( auth ); | ||
``` | ||
### Support for JavaScript Promises, and `async`/`await` | ||
@@ -41,0 +58,0 @@ |
const utils = require( "../src/utils" ); | ||
describe( "utils tests", () => { | ||
it( "should turn an account name into a LeanKit URL", done => { | ||
const url = utils.buildUrl( "test" ); | ||
url.should.equal( "https://test.leankit.com/" ); | ||
done(); | ||
} ); | ||
describe( "buildUrl", () => { | ||
it( "should turn an account name into a LeanKit URL", done => { | ||
const url = utils.buildUrl( "test" ); | ||
url.should.equal( "https://test.leankit.com/" ); | ||
done(); | ||
} ); | ||
it( "should preserve given URL", done => { | ||
const url = utils.buildUrl( "http://test.com" ); | ||
url.should.equal( "http://test.com/" ); | ||
done(); | ||
it( "should preserve given URL", done => { | ||
const url = utils.buildUrl( "http://test.com" ); | ||
url.should.equal( "http://test.com/" ); | ||
done(); | ||
} ); | ||
}); | ||
describe( "buildDefaultConfig", ()=> { | ||
let result; | ||
describe( "when providing account and token", ()=> { | ||
before(() => { | ||
result = utils.buildDefaultConfig( "bob", "1234" ); | ||
}); | ||
it( "should have expanded url", ()=> { | ||
result.baseUrl.should.equal( "https://bob.leankit.com/" ); | ||
} ); | ||
it( "should use bearer token auth with specified token", ()=> { | ||
result.auth.should.eql( { bearer: "1234" } ); | ||
} ); | ||
it( "should have default headers", ()=> { | ||
result.headers.should.eql({ | ||
Accept: "application/json", | ||
"Content-Type": "application/json", | ||
"User-Agent": utils.getUserAgent() | ||
} ); | ||
} ); | ||
} ); | ||
describe( "when providing email and password", () => { | ||
before(() => { | ||
result = utils.buildDefaultConfig( "bob", null, "wcoyote@acme.com", "rr!#@" ); | ||
}); | ||
it( "should use provided credentials", ()=> { | ||
result.auth.should.eql({ | ||
username: "wcoyote@acme.com", | ||
password: "rr!#@" | ||
}); | ||
} ); | ||
}) | ||
describe( "when providing token plus email and password", () => { | ||
before(() => { | ||
result = utils.buildDefaultConfig( "bob", "1234", "wcoyote@acme.com", "rr!#@" ); | ||
}); | ||
it( "should use bearer token auth with specified token", ()=> { | ||
result.auth.should.eql( { bearer: "1234" } ); | ||
} ); | ||
}) | ||
describe( "when providing header values in config", ()=> { | ||
it( "should force values for Accept, Content-Type, and User-Agent", ()=> { | ||
let invalidHeaders = { | ||
Accept: "anything", | ||
"Content-Type": "text/css", | ||
"User-Agent": "Mozilla/5.0" | ||
} | ||
result = utils.buildDefaultConfig( "bob", "1234", null, null, { config: { headers: invalidHeaders } } ); | ||
result.headers.should.eql({ | ||
Accept: "application/json", | ||
"Content-Type": "application/json", | ||
"User-Agent": utils.getUserAgent() | ||
} ); | ||
} ); | ||
} ); | ||
describe( "when providing other header values in config", ()=> { | ||
it( "should include the header values", ()=> { | ||
let headers = { | ||
CustomHeader:"beardzrock" | ||
}; | ||
result = utils.buildDefaultConfig( "bob", "1234", null, null, { headers } ); | ||
result.headers.should.eql({ | ||
Accept: "application/json", | ||
"Content-Type": "application/json", | ||
"User-Agent": utils.getUserAgent(), | ||
CustomHeader: "beardzrock" | ||
} ); | ||
} ); | ||
} ); | ||
describe( "when providing a proxy value", ()=> { | ||
it( "should include the proxy", ()=> { | ||
let config = { | ||
proxy:{ | ||
} | ||
} | ||
result = utils.buildDefaultConfig( "bob", "1234", null, null, config ); | ||
result.proxy.should.eql({}); | ||
} ); | ||
} ); | ||
} ); | ||
} ); |
177
src/index.js
@@ -6,76 +6,33 @@ /* eslint-disable max-lines */ | ||
const v1ApiFactory = require( "./api.v1" ); | ||
const STATUS_200 = 200; | ||
const STATUS_201 = 201; | ||
const legacy = require( "./legacy" ); | ||
const buildDefaultConfig = ( account, email, password, config ) => { | ||
config = config || { headers: {} }; | ||
if ( !config.headers ) { | ||
config.headers = {}; | ||
const rejectError = ( err, res, body, reject ) => { | ||
const message = res ? res.statusMessage : err.message; | ||
const reqErr = new Error( message ); | ||
if ( err ) { | ||
reqErr.stack = err.stack; | ||
} | ||
const userAgent = utils.getPropertyValue( config.headers, "User-Agent", utils.getUserAgent() ); | ||
utils.removeProperties( config.headers, [ "User-Agent", "Content-Type", "Accept" ] ); | ||
const proxy = config.proxy || null; | ||
const defaultHeaders = { | ||
Accept: "application/json", | ||
"Content-Type": "application/json", | ||
"User-Agent": userAgent | ||
}; | ||
const headers = Object.assign( {}, config.headers, defaultHeaders ); | ||
const defaults = { | ||
auth: { | ||
username: email, | ||
password | ||
}, | ||
baseUrl: utils.buildUrl( account ), | ||
headers | ||
}; | ||
if ( proxy ) { | ||
defaults.proxy = proxy; | ||
if ( res ) { | ||
reqErr.status = res.statusCode; | ||
reqErr.statusText = res.statusMessage; | ||
} | ||
return defaults; | ||
if ( body ) { | ||
try { | ||
reqErr.data = JSON.parse( body ); | ||
} catch ( e ) { | ||
reqErr.data = body; | ||
} | ||
} | ||
return reject( reqErr ); | ||
}; | ||
const Client = ( { account, email, password, config } ) => { | ||
const defaults = buildDefaultConfig( account, email, password, config ); | ||
const Client = ( { account, token, email, password, config } ) => { | ||
const defaults = utils.buildDefaultConfig( account, token, email, password, config ); | ||
const legacyHandler = legacy( req, defaults ); | ||
const streamResponse = ( request, cfg, stream ) => { | ||
return new Promise( ( resolve, reject ) => { | ||
const response = {}; | ||
request( cfg ) | ||
.on( "error", err => { | ||
return reject( err ); | ||
} ) | ||
.on( "response", res => { | ||
response.status = res.statusCode; | ||
response.statusText = res.statusMessage; | ||
response.data = ""; | ||
} ) | ||
.on( "end", () => { | ||
if ( response.status < 200 || response.status >= 300 ) { // eslint-disable-line no-magic-numbers | ||
return reject( response ); | ||
} | ||
return resolve( response ); | ||
} ) | ||
.pipe( stream ); | ||
} ); | ||
}; | ||
const request = ( options, stream = null ) => { | ||
if ( options.headers ) { | ||
options.headers.Accept = defaults.headers.Accept; | ||
options.headers[ "User-Agent" ] = defaults.headers[ "User-Agent" ]; | ||
} | ||
const cfg = Object.assign( {}, defaults, options ); | ||
if ( cfg.data ) { | ||
if ( cfg.headers[ "Content-Type" ] === "application/json" ) { | ||
cfg.body = JSON.stringify( cfg.data ); | ||
} else { | ||
cfg.body = cfg.data; | ||
} | ||
delete cfg.data; | ||
} | ||
const cfg = utils.mergeOptions( options, defaults ); | ||
if ( stream ) { | ||
return streamResponse( req, cfg, stream ); | ||
return utils.streamResponse( req, cfg, stream ); | ||
} | ||
@@ -85,21 +42,4 @@ | ||
req( cfg, ( err, res, body ) => { | ||
if ( err || res.statusCode < 200 || res.statusCode >= 300 ) { // eslint-disable-line no-magic-numbers | ||
const message = res ? res.statusMessage : err.message; | ||
const reqErr = new Error( message ); | ||
if ( err ) { | ||
reqErr.stack = err.stack; | ||
} | ||
if ( res ) { | ||
reqErr.status = res.statusCode; | ||
reqErr.statusText = res.statusMessage; | ||
// reqErr.headers = res.headers; | ||
} | ||
if ( body ) { | ||
try { | ||
reqErr.data = JSON.parse( body ); | ||
} catch ( e ) { | ||
reqErr.data = body; | ||
} | ||
} | ||
return reject( reqErr ); | ||
if ( err || res.statusCode < utils.status.status200 || res.statusCode >= utils.status.status300 ) { | ||
return rejectError( err, res, body, reject ); | ||
} | ||
@@ -124,72 +64,5 @@ | ||
const parseReplyData = ( error, response, body, resolve, reject ) => { // eslint-disable-line max-statements | ||
const parsed = utils.parseBody( body ); | ||
if ( error ) { | ||
const message = response ? response.statusMessage : error.message; | ||
const reqErr = new Error( message ); | ||
reqErr.stack = error.stack; | ||
if ( response ) { | ||
reqErr.status = response.statusCode; | ||
reqErr.statusText = response.statusMessage; | ||
} | ||
reqErr.data = parsed; | ||
return reject( reqErr ); | ||
} | ||
if ( response.statusCode !== STATUS_200 ) { | ||
const err = new Error( response.statusText ); | ||
err.name = "clientRequestError"; | ||
err.replyCode = response.statusCode; | ||
err.data = parsed; | ||
return reject( err ); | ||
} | ||
if ( parsed && parsed.ReplyCode && parsed.ReplyCode >= 300 ) { // eslint-disable-line no-magic-numbers | ||
const err = new Error( parsed.ReplyText || "apiError" ); | ||
err.name = "apiError"; | ||
err.httpStatusCode = parsed.ReplyCode; | ||
err.replyCode = parsed.ReplyCode; | ||
err.replyText = parsed.ReplyText; | ||
err.replyData = parsed.ReplyData; | ||
return reject( err ); | ||
} | ||
if ( parsed && parsed.ReplyCode && parsed.ReplyCode !== STATUS_200 && parsed.ReplyCode !== STATUS_201 ) { | ||
return resolve( parsed ); | ||
} | ||
if ( parsed.ReplyData && parsed.ReplyData.length > 0 ) { | ||
return resolve( { | ||
status: parsed.ReplyCode, | ||
statusText: parsed.ReplyText, | ||
data: parsed.ReplyData[ 0 ] | ||
} ); | ||
} | ||
return resolve( parsed ); | ||
}; | ||
const legacyRequest = ( options, stream = null ) => { | ||
options.url = utils.checkLegacyPath( options.url ); | ||
if ( options.headers ) { | ||
options.headers.Accept = defaults.headers.Accept; | ||
options.headers[ "User-Agent" ] = defaults.headers[ "User-Agent" ]; | ||
} | ||
const cfg = Object.assign( {}, defaults, options ); | ||
if ( cfg.data ) { | ||
if ( cfg.headers[ "Content-Type" ] === "application/json" ) { | ||
cfg.body = JSON.stringify( cfg.data ); | ||
} else { | ||
cfg.body = cfg.data; | ||
} | ||
delete cfg.data; | ||
} | ||
if ( stream ) { | ||
return streamResponse( req, cfg, stream ); | ||
} | ||
return new Promise( ( resolve, reject ) => { | ||
req( cfg, ( err, res, body ) => { | ||
return parseReplyData( err, res, body, resolve, reject ); | ||
} ); | ||
} ); | ||
}; | ||
const api = {}; | ||
apiFactory( api, request, { accountName: account, email, password } ); | ||
v1ApiFactory( api, legacyRequest, { accountName: account, email, password } ); | ||
v1ApiFactory( api, legacyHandler.request, { accountName: account, email, password } ); | ||
return api; | ||
@@ -196,0 +69,0 @@ }; |
@@ -1,3 +0,3 @@ | ||
const fs = require( "fs" ); | ||
const path = require( "path" ); | ||
/* eslint-disable max-lines */ | ||
const pkg = require( "../package.json" ); | ||
@@ -22,11 +22,5 @@ const buildUrl = account => { | ||
const getUserAgent = () => { | ||
const pkgfilepath = path.resolve( __dirname, "../", "package.json" ); | ||
if ( fs.existsSync( pkgfilepath ) ) { | ||
const pkg = JSON.parse( fs.readFileSync( pkgfilepath, "utf-8" ) ); | ||
return `leankit-node-client/${ pkg.version }`; | ||
} | ||
return "leankit-node-client/2.0.0"; | ||
return `leankit-node-client/${ pkg.version }`; | ||
}; | ||
const getPropertyValue = ( obj, prop, def = null ) => { | ||
@@ -41,2 +35,3 @@ for ( const k in obj ) { | ||
const removeProperties = ( obj, props ) => { | ||
@@ -52,2 +47,29 @@ for ( let i = 0; i < props.length; i++ ) { | ||
const buildDefaultConfig = ( account, token, email, password, config ) => { | ||
config = config || { headers: {} }; | ||
if ( !config.headers ) { | ||
config.headers = {}; | ||
} | ||
const userAgent = getPropertyValue( config.headers, "User-Agent", getUserAgent() ); | ||
removeProperties( config.headers, [ "User-Agent", "Content-Type", "Accept" ] ); | ||
const proxy = config.proxy || null; | ||
const defaultHeaders = { | ||
Accept: "application/json", | ||
"Content-Type": "application/json", | ||
"User-Agent": userAgent | ||
}; | ||
const headers = Object.assign( {}, config.headers, defaultHeaders ); | ||
const auth = token ? { bearer: token } : { username: email, password }; | ||
const defaults = { | ||
auth, | ||
baseUrl: buildUrl( account ), | ||
headers | ||
}; | ||
if ( proxy ) { | ||
defaults.proxy = proxy; | ||
} | ||
return defaults; | ||
}; | ||
const checkLegacyPath = urlPath => { | ||
@@ -57,2 +79,19 @@ return ( urlPath.startsWith( "api/" ) ) ? urlPath : `kanban/api/${ urlPath }`; | ||
const mergeOptions = ( options, defaults ) => { | ||
if ( options.headers ) { | ||
options.headers.Accept = defaults.headers.Accept; | ||
options.headers[ "User-Agent" ] = defaults.headers[ "User-Agent" ]; | ||
} | ||
const config = Object.assign( {}, defaults, options ); | ||
if ( config.data ) { | ||
if ( config.headers[ "Content-Type" ] === "application/json" ) { | ||
config.body = JSON.stringify( config.data ); | ||
} else { | ||
config.body = config.data; | ||
} | ||
delete config.data; | ||
} | ||
return config; | ||
}; | ||
const parseBody = body => { | ||
@@ -72,4 +111,33 @@ let parsed; | ||
const streamResponse = ( request, cfg, stream ) => { | ||
return new Promise( ( resolve, reject ) => { | ||
const response = {}; | ||
request( cfg ) | ||
.on( "error", err => { | ||
return reject( err ); | ||
} ) | ||
.on( "response", res => { | ||
response.status = res.statusCode; | ||
response.statusText = res.statusMessage; | ||
response.data = ""; | ||
} ) | ||
.on( "end", () => { | ||
if ( response.status < 200 || response.status >= 300 ) { // eslint-disable-line no-magic-numbers | ||
return reject( response ); | ||
} | ||
return resolve( response ); | ||
} ) | ||
.pipe( stream ); | ||
} ); | ||
}; | ||
const status = { | ||
status200: 200, | ||
status201: 201, | ||
status300: 300 | ||
}; | ||
module.exports = { | ||
buildUrl, | ||
buildDefaultConfig, | ||
getUserAgent, | ||
@@ -79,3 +147,6 @@ getPropertyValue, | ||
checkLegacyPath, | ||
parseBody | ||
parseBody, | ||
streamResponse, | ||
mergeOptions, | ||
status | ||
}; |
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
51325
30
948
438