mirror of
https://github.com/hanzoai/openid-client.git
synced 2026-09-24 17:01:10 +00:00
initial commit
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "airbnb-base",
|
||||
"rules": {
|
||||
"strict": "off",
|
||||
"no-empty": ["error", { "allowEmptyCatch": true }],
|
||||
"no-underscore-dangle": ["warn", { "allowAfterThis": true }],
|
||||
"generator-star-spacing": ["warn", "both"],
|
||||
"no-cond-assign": ["error", "except-parens"],
|
||||
"no-param-reassign": ["error", { "props": false }]
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
node_modules
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 6
|
||||
script:
|
||||
- make test-travis
|
||||
before_script:
|
||||
- sudo apt-get install bc
|
||||
after_script:
|
||||
- npm install codecov
|
||||
- ./node_modules/.bin/codecov
|
||||
notifications:
|
||||
email: false
|
||||
@@ -0,0 +1,4 @@
|
||||
Following semver, 1.0.0 will mark the first API stable release and commence of this file,
|
||||
until then please use the compare views of github for reference.
|
||||
|
||||
- TODO
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Filip Skokan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,27 @@
|
||||
NODE_VERSION = $(wordlist 1,1,$(subst ., ,$(subst v, ,$(shell node -v))))
|
||||
|
||||
ifeq ($(shell echo ${NODE_VERSION}\<6 | bc), 1)
|
||||
FLAGS = --harmony_destructuring
|
||||
endif
|
||||
|
||||
TESTS = test/**/**/*.test.js test/**/*.test.js
|
||||
|
||||
test:
|
||||
node $(FLAGS) \
|
||||
./node_modules/.bin/_mocha \
|
||||
$(TESTS)
|
||||
|
||||
coverage:
|
||||
node $(FLAGS) \
|
||||
./node_modules/.bin/istanbul cover \
|
||||
./node_modules/.bin/_mocha \
|
||||
$(TESTS)
|
||||
|
||||
test-travis:
|
||||
node $(FLAGS) \
|
||||
./node_modules/.bin/istanbul cover \
|
||||
./node_modules/.bin/_mocha \
|
||||
--report lcovonly \
|
||||
$(TESTS)
|
||||
|
||||
.PHONY: test coverage
|
||||
@@ -0,0 +1,219 @@
|
||||
'use strict';
|
||||
|
||||
const util = require('util');
|
||||
const assert = require('assert');
|
||||
const jose = require('node-jose');
|
||||
const base64url = require('base64url');
|
||||
const url = require('url');
|
||||
const { merge, defaults, pick, forEach } = require('lodash');
|
||||
|
||||
const TokenSet = require('./token_set');
|
||||
const tokenHash = require('./token_hash');
|
||||
|
||||
const {
|
||||
USER_AGENT,
|
||||
CLIENT_METADATA,
|
||||
CLIENT_DEFAULTS,
|
||||
} = require('./consts');
|
||||
|
||||
const debug = require('debug')('oidc:client');
|
||||
|
||||
const got = require('got');
|
||||
const map = new WeakMap();
|
||||
|
||||
function instance(ctx) {
|
||||
if (!map.has(ctx)) map.set(ctx, {});
|
||||
return map.get(ctx);
|
||||
}
|
||||
|
||||
class BaseClient {
|
||||
constructor(metadata) {
|
||||
forEach(defaults(pick(metadata, CLIENT_METADATA), CLIENT_DEFAULTS), (value, key) => {
|
||||
instance(this)[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
authorizationUrl(params) {
|
||||
const query = defaults(params, {
|
||||
client_id: this.client_id,
|
||||
scope: 'openid',
|
||||
response_type: 'code',
|
||||
});
|
||||
|
||||
if (typeof query.claims === 'object') {
|
||||
query.claims = JSON.stringify(query.claims, ['id_token', 'userinfo']);
|
||||
}
|
||||
|
||||
return url.format(defaults({
|
||||
search: null,
|
||||
query,
|
||||
}, url.parse(this.provider.authorization_endpoint)));
|
||||
}
|
||||
|
||||
authorizationCallback(redirectUri, params) {
|
||||
if (params.error) {
|
||||
return Promise.reject(pick(params, 'error', 'error_description', 'state'));
|
||||
}
|
||||
|
||||
return this.grant({
|
||||
grant_type: 'authorization_code',
|
||||
code: params.code,
|
||||
redirect_uri: redirectUri,
|
||||
}).then(tokenset => this.validateIdToken(tokenset));
|
||||
}
|
||||
|
||||
validateIdToken(token) {
|
||||
let idToken = token;
|
||||
|
||||
if (idToken instanceof TokenSet) {
|
||||
if (!idToken.id_token) {
|
||||
throw new Error('id_token not present in TokenSet');
|
||||
}
|
||||
|
||||
idToken = idToken.id_token;
|
||||
}
|
||||
|
||||
const now = Math.ceil(Date.now() / 1000);
|
||||
const parts = idToken.split('.');
|
||||
const header = parts[0];
|
||||
const payload = parts[1];
|
||||
const headerObject = JSON.parse(base64url.decode(header));
|
||||
const payloadObject = JSON.parse(base64url.decode(payload));
|
||||
|
||||
const verifyPresence = (prop) => {
|
||||
if (payloadObject[prop] === undefined) {
|
||||
throw new Error(`missing required JWT property ${prop}`);
|
||||
}
|
||||
};
|
||||
|
||||
assert.equal(this.id_token_signed_response_alg, headerObject.alg, 'unexpected algorithm used');
|
||||
|
||||
['iss', 'sub', 'aud', 'exp', 'iat'].forEach(verifyPresence);
|
||||
assert.equal(this.provider.issuer, payloadObject.iss, 'unexpected iss value');
|
||||
|
||||
assert(typeof payloadObject.iat === 'number', 'iat is not a number');
|
||||
assert(payloadObject.iat <= now, 'id_token issued in the future');
|
||||
|
||||
if (payloadObject.nbf !== undefined) {
|
||||
assert(typeof payloadObject.nbf === 'number', 'nbf is not a number');
|
||||
assert(payloadObject.nbf <= now, 'id_token not active yet');
|
||||
}
|
||||
|
||||
assert(typeof payloadObject.exp === 'number', 'exp is not a number');
|
||||
assert(now < payloadObject.exp, 'id_token expired');
|
||||
|
||||
if (payloadObject.azp !== undefined) {
|
||||
assert.equal(this.client_id, payloadObject.azp, 'azp must be the client_id');
|
||||
}
|
||||
|
||||
if (!Array.isArray(payloadObject.aud)) {
|
||||
payloadObject.aud = [payloadObject.aud];
|
||||
} else if (payloadObject.aud.length > 1 && !payloadObject.azp) {
|
||||
throw new Error('missing required JWT property azp');
|
||||
}
|
||||
|
||||
assert(payloadObject.aud.indexOf(this.client_id) !== -1, 'aud is missing the client_id');
|
||||
|
||||
if (payloadObject.at_hash && token.access_token) {
|
||||
assert.equal(payloadObject.at_hash, tokenHash(token.access_token, headerObject.alg),
|
||||
'at_hash mismatch');
|
||||
}
|
||||
|
||||
if (payloadObject.c_hash && token.code) {
|
||||
assert.equal(payloadObject.at_hash, tokenHash(token.code, headerObject.alg), 'c_hash mismatch');
|
||||
}
|
||||
|
||||
return this.provider.key(headerObject)
|
||||
.then(key => jose.JWS.createVerify(key).verify(idToken))
|
||||
.then(() => token);
|
||||
}
|
||||
|
||||
// implicitCallback(params, verify) {
|
||||
// if (params.error) {
|
||||
// return Promise.reject(pick(params, 'error', 'error_description', 'state'));
|
||||
// }
|
||||
// }
|
||||
|
||||
refresh(refreshToken) {
|
||||
let token = refreshToken;
|
||||
|
||||
if (token instanceof TokenSet) {
|
||||
if (!token.refresh_token) {
|
||||
return Promise.reject(new Error('refresh_token not present in TokenSet'));
|
||||
}
|
||||
token = token.refresh_token;
|
||||
}
|
||||
|
||||
return this.grant({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: String(token),
|
||||
}).then(tokenset => this.validateIdToken(tokenset));
|
||||
}
|
||||
|
||||
grant(body) {
|
||||
const auth = this.grantAuth();
|
||||
debug('client %s %s grant request started', this.client_id, body.grant_type);
|
||||
|
||||
return got.post(this.provider.token_endpoint, merge({
|
||||
body,
|
||||
retries: 0,
|
||||
followRedirect: false,
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
},
|
||||
}, auth)).then((response) => new TokenSet(JSON.parse(response.body)), (err) => {
|
||||
debug('client %s grant request failed (%s > %s)', this.client_id, err.name, err.message);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
grantAuth() {
|
||||
switch (this.token_endpoint_auth_method) {
|
||||
case 'client_secret_post':
|
||||
return {
|
||||
body: {
|
||||
client_id: this.client_id,
|
||||
client_secret: this.client_secret,
|
||||
},
|
||||
};
|
||||
default: {
|
||||
const value = new Buffer(`${this.client_id}:${this.client_secret}`).toString('base64');
|
||||
return {
|
||||
headers: {
|
||||
Authorization: `Basic ${value}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inspect() {
|
||||
return util.format('Client <%s>', this.client_id);
|
||||
}
|
||||
|
||||
static fromUri(uri, token) {
|
||||
debug('fetching client from %s', uri);
|
||||
|
||||
return got.get(uri, {
|
||||
retries: 0,
|
||||
followRedirect: false,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': USER_AGENT,
|
||||
},
|
||||
}).then((response) => new this(JSON.parse(response.body)), (err) => {
|
||||
debug('%s request failed (%s > %s)', uri, err.name, err.message);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
CLIENT_METADATA.forEach((prop) => {
|
||||
Object.defineProperty(BaseClient.prototype, prop, {
|
||||
get() {
|
||||
return instance(this)[prop];
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = BaseClient;
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
const pkg = require('../package.json');
|
||||
|
||||
const USER_AGENT = `${pkg.name}/${pkg.version} (${pkg.homepage})`;
|
||||
|
||||
const WELL_KNOWN = '/.well-known/openid-configuration';
|
||||
|
||||
const PROVIDER_METADATA = [
|
||||
'acr_values_supported',
|
||||
'authorization_endpoint',
|
||||
'claims_parameter_supported',
|
||||
'claims_supported',
|
||||
'grant_types_supported',
|
||||
'id_token_signing_alg_values_supported',
|
||||
'issuer',
|
||||
'jwks_uri',
|
||||
'registration_endpoint',
|
||||
'request_object_signing_alg_values_supported',
|
||||
'request_parameter_supported',
|
||||
'request_uri_parameter_supported',
|
||||
'response_modes_supported',
|
||||
'response_types_supported',
|
||||
'scopes_supported',
|
||||
'subject_types_supported',
|
||||
'token_endpoint',
|
||||
'token_endpoint_auth_methods_supported',
|
||||
'token_endpoint_auth_signing_alg_values_supported',
|
||||
'token_introspection_endpoint',
|
||||
'token_revocation_endpoint',
|
||||
'userinfo_endpoint',
|
||||
'userinfo_signing_alg_values_supported',
|
||||
'id_token_encryption_alg_values_supported',
|
||||
'id_token_encryption_enc_values_supported',
|
||||
'userinfo_encryption_alg_values_supported',
|
||||
'userinfo_encryption_enc_values_supported',
|
||||
'request_object_encryption_alg_values_supported',
|
||||
'request_object_encryption_enc_values_supported',
|
||||
'check_session_iframe',
|
||||
'end_session_endpoint',
|
||||
];
|
||||
|
||||
const CLIENT_METADATA = [
|
||||
'application_type',
|
||||
'client_id',
|
||||
'client_name',
|
||||
'client_secret',
|
||||
'client_secret_expires_at',
|
||||
'client_uri',
|
||||
'contacts',
|
||||
'default_acr_values',
|
||||
'default_max_age',
|
||||
'grant_types',
|
||||
'id_token_encrypted_response_alg',
|
||||
'id_token_encrypted_response_enc',
|
||||
'id_token_signed_response_alg',
|
||||
'initiate_login_uri',
|
||||
'jwks',
|
||||
'jwks_uri',
|
||||
'logo_uri',
|
||||
'policy_uri',
|
||||
'post_logout_redirect_uris',
|
||||
'redirect_uris',
|
||||
'registration_access_token',
|
||||
'request_object_encryption_alg',
|
||||
'request_object_encryption_enc',
|
||||
'request_object_signing_alg',
|
||||
'request_uris',
|
||||
'require_auth_time',
|
||||
'response_types',
|
||||
'sector_identifier_uri',
|
||||
'subject_type',
|
||||
'token_endpoint_auth_method',
|
||||
'token_endpoint_auth_signing_alg',
|
||||
'tos_uri',
|
||||
'userinfo_encrypted_response_alg',
|
||||
'userinfo_encrypted_response_enc',
|
||||
'userinfo_signed_response_alg',
|
||||
];
|
||||
|
||||
const PROVIDER_DEFAULTS = {
|
||||
response_modes_supported: ['query', 'fragment'],
|
||||
grant_types_supported: ['authorization_code', 'implicit'],
|
||||
token_endpoint_auth_methods_supported: ['client_secret_basic'],
|
||||
claims_parameter_supported: false,
|
||||
request_parameter_supported: false,
|
||||
request_uri_parameter_supported: true,
|
||||
require_request_uri_registration: false,
|
||||
};
|
||||
|
||||
const CLIENT_DEFAULTS = {
|
||||
response_types: ['code'],
|
||||
grant_types: ['authorization_code'],
|
||||
application_type: ['web'],
|
||||
id_token_signed_response_alg: 'RS256',
|
||||
token_endpoint_auth_method: 'client_secret_basic',
|
||||
};
|
||||
|
||||
module.exports.WELL_KNOWN = WELL_KNOWN;
|
||||
module.exports.PROVIDER_DEFAULTS = PROVIDER_DEFAULTS;
|
||||
module.exports.PROVIDER_METADATA = PROVIDER_METADATA;
|
||||
module.exports.CLIENT_DEFAULTS = CLIENT_DEFAULTS;
|
||||
module.exports.CLIENT_METADATA = CLIENT_METADATA;
|
||||
module.exports.USER_AGENT = USER_AGENT;
|
||||
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Provider = require('./provider');
|
||||
|
||||
module.exports = {
|
||||
Provider,
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
'use strict';
|
||||
|
||||
const url = require('url');
|
||||
const jose = require('node-jose');
|
||||
const util = require('util');
|
||||
const { defaults, pick, forEach } = require('lodash');
|
||||
|
||||
const {
|
||||
USER_AGENT,
|
||||
WELL_KNOWN,
|
||||
PROVIDER_METADATA,
|
||||
PROVIDER_DEFAULTS,
|
||||
} = require('./consts');
|
||||
|
||||
const BaseClient = require('./base_client');
|
||||
|
||||
const debug = require('debug')('oidc:provider');
|
||||
|
||||
const got = require('got');
|
||||
const map = new WeakMap();
|
||||
|
||||
function instance(ctx) {
|
||||
if (!map.has(ctx)) map.set(ctx, {});
|
||||
return map.get(ctx);
|
||||
}
|
||||
|
||||
class Provider {
|
||||
constructor(metadata) {
|
||||
forEach(defaults(pick(metadata, PROVIDER_METADATA), PROVIDER_DEFAULTS), (value, key) => {
|
||||
instance(this)[key] = value;
|
||||
});
|
||||
|
||||
const self = this;
|
||||
|
||||
Object.defineProperty(this, 'Client', {
|
||||
value: class Client extends BaseClient {
|
||||
static get provider() {
|
||||
return self;
|
||||
}
|
||||
|
||||
get provider() {
|
||||
return this.constructor.provider;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
inspect() {
|
||||
return util.format('Provider <%s>', this.issuer);
|
||||
}
|
||||
|
||||
keyStore() {
|
||||
debug('%s request started', this.jwks_uri);
|
||||
|
||||
return got.get(this.jwks_uri)
|
||||
.then((response) => JSON.parse(response.body), (err) => {
|
||||
debug('%s request failed (%s > %s)', this.provider.jwks_uri, err.name, err.message);
|
||||
throw err;
|
||||
})
|
||||
.then(jwks => jose.JWK.asKeyStore(jwks));
|
||||
}
|
||||
|
||||
key(def) {
|
||||
return this.keyStore().then(store => store.get(def));
|
||||
}
|
||||
|
||||
static discover(uri) {
|
||||
const isWellKnown = uri.endsWith(WELL_KNOWN);
|
||||
const wellKnownUri = isWellKnown ? uri : url.resolve(uri, WELL_KNOWN);
|
||||
|
||||
debug('discovering configuration from %s', wellKnownUri);
|
||||
|
||||
return got.get(wellKnownUri, {
|
||||
retries: 0,
|
||||
followRedirect: false,
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
},
|
||||
}).then((response) => new this(JSON.parse(response.body)), (err) => {
|
||||
debug('%s discovery failed (%s > %s)', wellKnownUri, err.name, err.message);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
PROVIDER_METADATA.forEach((prop) => {
|
||||
Object.defineProperty(Provider.prototype, prop, {
|
||||
get() {
|
||||
return instance(this)[prop];
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = Provider;
|
||||
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
const base64url = require('base64url');
|
||||
const crypto = require('crypto');
|
||||
|
||||
module.exports = function tokenHash(token, alg) {
|
||||
const size = alg.slice(-3);
|
||||
let hashingAlg;
|
||||
|
||||
switch (size) {
|
||||
case '512':
|
||||
hashingAlg = 'sha512';
|
||||
break;
|
||||
case '384':
|
||||
hashingAlg = 'sha384';
|
||||
break;
|
||||
default:
|
||||
hashingAlg = 'sha256';
|
||||
}
|
||||
|
||||
const digest = crypto.createHash(hashingAlg).update(token).digest('hex');
|
||||
return base64url(new Buffer(digest.slice(0, digest.length / 2), 'hex'));
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
class TokenSet {
|
||||
constructor(values) {
|
||||
Object.assign(this, values);
|
||||
}
|
||||
|
||||
set expires_in(value) { // eslint-disable-line camelcase
|
||||
const now = new Date() / 1000 | 0;
|
||||
this.expires_at = now + Number(value);
|
||||
}
|
||||
|
||||
get expires_in() { // eslint-disable-line camelcase
|
||||
const now = new Date() / 1000 | 0;
|
||||
return Math.max.apply(null, [this.expires_at - now, 0]);
|
||||
}
|
||||
|
||||
expired() {
|
||||
return this.expires_in === 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TokenSet;
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "oidc-client",
|
||||
"version": "0.0.1",
|
||||
"description": "OpenID Relying Party (RP, Client) implementation for Node.js",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"coverage": "make coverage",
|
||||
"test": "make test",
|
||||
"lint": "eslint lib example test --fix"
|
||||
},
|
||||
"repository": "panva/node-oidc-client",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"homepage": "https://github.com/panva/node-oidc-client",
|
||||
"keywords": [
|
||||
"openid",
|
||||
"connect",
|
||||
"client",
|
||||
"relying",
|
||||
"party",
|
||||
"oidc",
|
||||
"oauth",
|
||||
"oauth2"
|
||||
],
|
||||
"author": "Filip Skokan",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"devDependencies": {
|
||||
"chai": "^3.5.0",
|
||||
"eslint": "^3.0.1",
|
||||
"eslint-config-airbnb-base": "^4.0.0",
|
||||
"eslint-plugin-import": "^1.10.2",
|
||||
"istanbul": "^0.4.4",
|
||||
"mocha": "^2.5.3",
|
||||
"nock": "^8.0.0",
|
||||
"sinon": "^1.17.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"base64url": "^1.0.6",
|
||||
"debug": "^2.2.0",
|
||||
"got": "^6.3.0",
|
||||
"lodash": "^4.13.1",
|
||||
"lru-cache": "^4.0.1",
|
||||
"node-jose": "^0.8.0",
|
||||
"node-uuid": "^1.4.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"rules": {
|
||||
"max-len": "off",
|
||||
"func-names": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
"comma-dangle": "off",
|
||||
"no-unused-expressions": "off",
|
||||
"import/no-extraneous-dependencies": "off"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user