diff --git a/.gitignore b/.gitignore index 50b2b0a45..ca068bf0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,13 @@ +# NodeJs modules node_modules/ +# Coverage reports coverage/ +src/.baseDir.ts +.vscode/ + *.swp *.sh @@ -11,6 +16,11 @@ config.yml npm-debug.log +# Directory used by example notifications/ +# VSCode user configuration .vscode/ + +# Generated by TypeScript compiler +dist/ diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 000000000..f5a6b6589 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,49 @@ +module.exports = function(grunt) { + grunt.initConfig({ + run: { + options: {}, + "build-ts": { + cmd: "npm", + args: ['run', 'build-ts'] + }, + "tslint": { + cmd: "npm", + args: ['run', 'tslint'] + }, + "test": { + cmd: "npm", + args: ['run', 'test'] + } + }, + copy: { + resources: { + expand: true, + cwd: 'src/resources/', + src: '**', + dest: 'dist/src/resources/' + }, + views: { + expand: true, + cwd: 'src/views/', + src: '**', + dest: 'dist/src/views/' + }, + public_html: { + expand: true, + cwd: 'src/public_html/', + src: '**', + dest: 'dist/src/public_html/' + } + } + }); + + grunt.loadNpmTasks('grunt-run'); + grunt.loadNpmTasks('grunt-contrib-copy'); + + grunt.registerTask('default', ['build']); + + grunt.registerTask('res', ['copy:resources', 'copy:views', 'copy:public_html']); + grunt.registerTask('build', ['run:tslint', 'run:build-ts', 'res']); + + grunt.registerTask('test', ['run:test']); +}; diff --git a/package.json b/package.json index 563d58a8f..8d52213a2 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,15 @@ "authelia": "src/index.js" }, "scripts": { - "test": "./node_modules/.bin/mocha --recursive test/unitary", + "test": "./node_modules/.bin/mocha -r ts-node/register --recursive test/unitary", "unit-test": "./node_modules/.bin/mocha --recursive test/unitary", "int-test": "./node_modules/.bin/mocha --recursive test/integration", "all-test": "./node_modules/.bin/mocha --recursive test", - "coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test" + "coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test", + "build-ts": "tsc", + "watch-ts": "tsc -w", + "tslint": "tslint -c tslint.json -p tsconfig.json", + "serve": "node dist/src/index.js" }, "repository": { "type": "git", @@ -43,12 +47,29 @@ "yamljs": "^0.2.8" }, "devDependencies": { + "@types/assert": "0.0.31", + "@types/bluebird": "^3.5.3", + "@types/express": "^4.0.35", + "@types/express-session": "0.0.32", + "@types/ldapjs": "^1.0.0", + "@types/mocha": "^2.2.41", + "@types/nedb": "^1.8.3", + "@types/nodemailer": "^1.3.32", + "@types/object-path": "^0.9.28", + "@types/sinon": "^2.2.1", + "@types/winston": "^2.3.2", + "@types/yamljs": "^0.2.30", + "grunt": "^1.0.1", + "grunt-contrib-copy": "^1.0.0", + "grunt-run": "^0.6.0", "mocha": "^3.2.0", "mockdate": "^2.0.1", "request": "^2.79.0", "should": "^11.1.1", "sinon": "^1.17.6", "sinon-promise": "^0.1.3", - "tmp": "0.0.31" + "tmp": "0.0.31", + "ts-node": "^3.0.4", + "tslint": "^5.2.0" } } diff --git a/src/index.js b/src/index.js deleted file mode 100755 index e593e48b8..000000000 --- a/src/index.js +++ /dev/null @@ -1,36 +0,0 @@ -#! /usr/bin/env node - -process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; - -var server = require('./lib/server'); - -var ldapjs = require('ldapjs'); -var u2f = require('authdog'); -var nodemailer = require('nodemailer'); -var nedb = require('nedb'); -var YAML = require('yamljs'); -var session = require('express-session'); -var winston = require('winston'); -var speakeasy = require('speakeasy'); - -var config_path = process.argv[2]; -if(!config_path) { - console.log('No config file has been provided.'); - console.log('Usage: authelia '); - process.exit(0); -} - -console.log('Parse configuration file: %s', config_path); - -var yaml_config = YAML.load(config_path); - -var deps = {}; -deps.u2f = u2f; -deps.nedb = nedb; -deps.nodemailer = nodemailer; -deps.ldapjs = ldapjs; -deps.session = session; -deps.winston = winston; -deps.speakeasy = speakeasy; - -server.run(yaml_config, deps); diff --git a/src/index.ts b/src/index.ts new file mode 100755 index 000000000..2de121e9b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,29 @@ +#! /usr/bin/env node + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + +import * as server from "./lib/server"; +const YAML = require("yamljs"); + +const config_path = process.argv[2]; +if (!config_path) { + console.log("No config file has been provided."); + console.log("Usage: authelia "); + process.exit(0); +} + +console.log("Parse configuration file: %s", config_path); + +const yaml_config = YAML.load(config_path); + +const deps = { + u2f: require("authdog"), + nodemailer: require("nodemailer"), + ldapjs: require("ldapjs"), + session: require("express-session"), + winston: require("winston"), + speakeasy: require("speakeasy"), + nedb: require("nedb") +}; + +server.run(yaml_config, deps); diff --git a/src/lib/config_adapter.js b/src/lib/config_adapter.js deleted file mode 100644 index e91547f2b..000000000 --- a/src/lib/config_adapter.js +++ /dev/null @@ -1,17 +0,0 @@ - -var objectPath = require('object-path'); - -module.exports = function(yaml_config) { - return { - port: objectPath.get(yaml_config, 'port', 8080), - ldap: objectPath.get(yaml_config, 'ldap', 'ldap://127.0.0.1:389'), - session_domain: objectPath.get(yaml_config, 'session.domain'), - session_secret: objectPath.get(yaml_config, 'session.secret'), - session_max_age: objectPath.get(yaml_config, 'session.expiration', 3600000), // in ms - store_directory: objectPath.get(yaml_config, 'store_directory'), - logs_level: objectPath.get(yaml_config, 'logs_level'), - notifier: objectPath.get(yaml_config, 'notifier'), - access_control: objectPath.get(yaml_config, 'access_control') - } -}; - diff --git a/src/lib/config_adapter.ts b/src/lib/config_adapter.ts new file mode 100644 index 000000000..6c9721e39 --- /dev/null +++ b/src/lib/config_adapter.ts @@ -0,0 +1,38 @@ + +import * as ObjectPath from "object-path"; +import { authelia } from "../types/authelia"; + + +function get_optional(config: object, path: string, default_value: T): T { + let entry = default_value; + if (ObjectPath.has(config, path)) { + entry = ObjectPath.get(config, path); + } + return entry; +} + +function ensure_key_existence(config: object, path: string): void { + if (!ObjectPath.has(config, path)) { + throw new Error(`Configuration error: key '${path}' is missing in configuration file`); + } +} + +export = function(yaml_config: object): authelia.Configuration { + ensure_key_existence(yaml_config, "ldap"); + ensure_key_existence(yaml_config, "session.secret"); + + const port = ObjectPath.get(yaml_config, "port", 8080); + + return { + port: port, + ldap: ObjectPath.get(yaml_config, "ldap"), + session_domain: ObjectPath.get(yaml_config, "session.domain"), + session_secret: ObjectPath.get(yaml_config, "session.secret"), + session_max_age: get_optional(yaml_config, "session.expiration", 3600000), // in ms + store_directory: get_optional(yaml_config, "store_directory", undefined), + logs_level: get_optional(yaml_config, "logs_level", "info"), + notifier: ObjectPath.get(yaml_config, "notifier"), + access_control: ObjectPath.get(yaml_config, "access_control") + }; +}; + diff --git a/src/lib/identity_check.js b/src/lib/identity_check.js index bef355343..0b03509a6 100644 --- a/src/lib/identity_check.js +++ b/src/lib/identity_check.js @@ -106,7 +106,7 @@ function identity_check_post(endpoint, icheck_interface) { return identity_check.issue_token(userid, undefined, logger); }, function(err) { - throw new exceptions.AccessDeniedError(); + throw new exceptions.AccessDeniedError(err); }) .then(function(token) { var redirect_url = objectPath.get(req, 'body.redirect'); @@ -124,12 +124,12 @@ function identity_check_post(endpoint, icheck_interface) { res.send(); }) .catch(exceptions.IdentityError, function(err) { - logger.error('POST identity_check: IdentityError %s', err); + logger.error('POST identity_check: %s', err); res.status(400); res.send(); }) .catch(exceptions.AccessDeniedError, function(err) { - logger.error('POST identity_check: AccessDeniedError %s', err); + logger.error('POST identity_check: %s', err); res.status(403); res.send(); }) diff --git a/src/lib/routes/totp.js b/src/lib/routes/totp.js index 6621ba423..b5a00e23c 100644 --- a/src/lib/routes/totp.js +++ b/src/lib/routes/totp.js @@ -1,5 +1,5 @@ -module.exports = totp; +module.exports = totp_fn; var totp = require('../totp'); var objectPath = require('object-path'); @@ -7,7 +7,7 @@ var exceptions = require('../../../src/lib/exceptions'); var UNAUTHORIZED_MESSAGE = 'Unauthorized access'; -function totp(req, res) { +function totp_fn(req, res) { var logger = req.app.get('logger'); var userid = objectPath.get(req, 'session.auth_session.userid'); logger.info('POST 2ndfactor totp: Initiate TOTP validation for user %s', userid); diff --git a/src/lib/server.js b/src/lib/server.js deleted file mode 100644 index f4c1ec836..000000000 --- a/src/lib/server.js +++ /dev/null @@ -1,73 +0,0 @@ - -module.exports = { - run: run -} - -var express = require('express'); -var bodyParser = require('body-parser'); -var path = require('path'); -var UserDataStore = require('./user_data_store'); -var Notifier = require('./notifier'); -var AuthenticationRegulator = require('./authentication_regulator'); -var setup_endpoints = require('./setup_endpoints'); -var config_adapter = require('./config_adapter'); -var Ldap = require('./ldap'); -var AccessControl = require('./access_control'); - -function run(yaml_config, deps, fn) { - var config = config_adapter(yaml_config); - - var view_directory = path.resolve(__dirname, '../views'); - var public_html_directory = path.resolve(__dirname, '../public_html'); - var datastore_options = {}; - datastore_options.directory = config.store_directory; - if(config.store_in_memory) - datastore_options.inMemory = true; - - var app = express(); - app.use(express.static(public_html_directory)); - app.use(bodyParser.urlencoded({ extended: false })); - app.use(bodyParser.json()); - app.set('trust proxy', 1); // trust first proxy - - app.use(deps.session({ - secret: config.session_secret, - resave: false, - saveUninitialized: true, - cookie: { - secure: false, - maxAge: config.session_max_age, - domain: config.session_domain - }, - })); - - app.set('views', view_directory); - app.set('view engine', 'ejs'); - - // by default the level of logs is info - deps.winston.level = config.logs_level || 'info'; - - var five_minutes = 5 * 60; - var data_store = new UserDataStore(deps.nedb, datastore_options); - var regulator = new AuthenticationRegulator(data_store, five_minutes); - var notifier = new Notifier(config.notifier, deps); - var ldap = new Ldap(deps, config.ldap); - var access_control = AccessControl(deps.winston, config.access_control); - - app.set('logger', deps.winston); - app.set('ldap', ldap); - app.set('totp engine', deps.speakeasy); - app.set('u2f', deps.u2f); - app.set('user data store', data_store); - app.set('notifier', notifier); - app.set('authentication regulator', regulator); - app.set('config', config); - app.set('access control', access_control); - - setup_endpoints(app); - - return app.listen(config.port, function(err) { - console.log('Listening on %d...', config.port); - if(fn) fn(); - }); -} diff --git a/src/lib/server.ts b/src/lib/server.ts new file mode 100644 index 000000000..279c3fbda --- /dev/null +++ b/src/lib/server.ts @@ -0,0 +1,70 @@ + +import { authelia } from "../types/authelia"; +import * as Express from "express"; +import * as BodyParser from "body-parser"; +import * as Path from "path"; + +const UserDataStore = require("./user_data_store"); +const Notifier = require("./notifier"); +const AuthenticationRegulator = require("./authentication_regulator"); +const setup_endpoints = require("./setup_endpoints"); +const config_adapter = require("./config_adapter"); +const Ldap = require("./ldap"); +const AccessControl = require("./access_control"); + +export function run(yaml_configuration: authelia.Configuration, deps: authelia.GlobalDependencies, fn?: () => undefined) { + const config = config_adapter(yaml_configuration); + + const view_directory = Path.resolve(__dirname, "../views"); + const public_html_directory = Path.resolve(__dirname, "../public_html"); + const datastore_options = { + directory: config.store_directory, + inMemory: config.store_in_memory + }; + + const app = Express(); + app.use(Express.static(public_html_directory)); + app.use(BodyParser.urlencoded({ extended: false })); + app.use(BodyParser.json()); + app.set("trust proxy", 1); // trust first proxy + + app.use(deps.session({ + secret: config.session_secret, + resave: false, + saveUninitialized: true, + cookie: { + secure: false, + maxAge: config.session_max_age, + domain: config.session_domain + }, + })); + + app.set("views", view_directory); + app.set("view engine", "ejs"); + + // by default the level of logs is info + deps.winston.level = config.logs_level || "info"; + + const five_minutes = 5 * 60; + const data_store = new UserDataStore(deps.nedb, datastore_options); + const regulator = new AuthenticationRegulator(data_store, five_minutes); + const notifier = new Notifier(config.notifier, deps); + const ldap = new Ldap(deps, config.ldap); + const access_control = AccessControl(deps.winston, config.access_control); + + app.set("logger", deps.winston); + app.set("ldap", ldap); + app.set("totp engine", deps.speakeasy); + app.set("u2f", deps.u2f); + app.set("user data store", data_store); + app.set("notifier", notifier); + app.set("authentication regulator", regulator); + app.set("config", config); + app.set("access control", access_control); + setup_endpoints(app); + + return app.listen(config.port, function(err: string) { + console.log("Listening on %d...", config.port); + if (fn) fn(); + }); +} diff --git a/src/lib/utils.js b/src/lib/utils.js deleted file mode 100644 index 9b3845f41..000000000 --- a/src/lib/utils.js +++ /dev/null @@ -1,35 +0,0 @@ - -module.exports = { - 'promisify': promisify, - 'resolve': resolve, - 'reject': reject -} - -var Q = require('q'); - -function promisify(fn, context) { - return function() { - var defer = Q.defer(); - var args = Array.prototype.slice.call(arguments); - args.push(function(err, val) { - if (err !== null && err !== undefined) { - return defer.reject(err); - } - return defer.resolve(val); - }); - fn.apply(context || {}, args); - return defer.promise; - }; -} - -function resolve(data) { - var defer = Q.defer(); - defer.resolve(data); - return defer.promise; -} - -function reject(err) { - var defer = Q.defer(); - defer.reject(err); - return defer.promise; -} diff --git a/src/types/authelia.d.ts b/src/types/authelia.d.ts new file mode 100644 index 000000000..9b2223566 --- /dev/null +++ b/src/types/authelia.d.ts @@ -0,0 +1,62 @@ + +import * as winston from "winston"; +import * as nedb from "nedb"; + +declare namespace authelia { + + interface LdapConfiguration { + url: string; + base_dn: string; + additional_user_dn?: string; + user_name_attribute?: string; // cn by default + additional_group_dn?: string; + group_name_attribute?: string; // cn by default + user: string; // admin username + password: string; // admin password + } + + type UserName = string; + type GroupName = string; + type DomainPattern = string; + + type ACLDefaultRules = Array; + type ACLGroupsRules = Map; + type ACLUsersRules = Map; + + export interface ACLConfiguration { + default: ACLDefaultRules; + groups: ACLGroupsRules; + users: ACLUsersRules; + } + + interface SessionCookieConfiguration { + secret: string; + expiration: number; + domain: string + } + + type NotifierType = string; + export type NotifiersConfiguration = Map; + + export interface Configuration { + port: number; + logs_level: string; + ldap: LdapConfiguration | {}; + session_domain?: string; + session_secret: string; + session_max_age: number; + store_directory?: string; + notifier: NotifiersConfiguration; + access_control: ACLConfiguration; + } + + export interface GlobalDependencies { + u2f: object; + nodemailer: any; + ldapjs: object; + session: any; + winston: winston.Winston; + speakeasy: object; + nedb: object; + } +} \ No newline at end of file diff --git a/test/unitary/config_adapter.test.ts b/test/unitary/config_adapter.test.ts new file mode 100644 index 000000000..3db5cd58a --- /dev/null +++ b/test/unitary/config_adapter.test.ts @@ -0,0 +1,88 @@ +import * as mocha from "mocha"; +import * as Assert from "assert"; + +const config_adapter = require("../../src/lib/config_adapter"); + +describe("test config adapter", function() { + function build_yaml_config(): any { + const yaml_config = { + port: 8080, + ldap: {}, + session: { + domain: "example.com", + secret: "secret", + max_age: 40000 + }, + store_directory: "/mydirectory", + logs_level: "debug" + }; + return yaml_config; + } + + it("should read the port from the yaml file", function() { + const yaml_config = build_yaml_config(); + yaml_config.port = 7070; + const config = config_adapter(yaml_config); + Assert.equal(config.port, 7070); + }); + + it("should default the port to 8080 if not provided", function() { + const yaml_config = build_yaml_config(); + delete yaml_config.port; + const config = config_adapter(yaml_config); + Assert.equal(config.port, 8080); + }); + + it("should get the ldap attributes", function() { + const yaml_config = build_yaml_config(); + yaml_config.ldap = { + url: "http://ldap", + user_search_base: "ou=groups,dc=example,dc=com", + user_search_filter: "uid", + user: "admin", + password: "pass" + }; + + const config = config_adapter(yaml_config); + + Assert.equal(config.ldap.url, "http://ldap"); + Assert.equal(config.ldap.user_search_base, "ou=groups,dc=example,dc=com"); + Assert.equal(config.ldap.user_search_filter, "uid"); + Assert.equal(config.ldap.user, "admin"); + Assert.equal(config.ldap.password, "pass"); + }); + + it("should get the session attributes", function() { + const yaml_config = build_yaml_config(); + yaml_config.session = { + domain: "example.com", + secret: "secret", + expiration: 3600 + }; + const config = config_adapter(yaml_config); + Assert.equal(config.session_domain, "example.com"); + Assert.equal(config.session_secret, "secret"); + Assert.equal(config.session_max_age, 3600); + }); + + it("should get the log level", function() { + const yaml_config = build_yaml_config(); + yaml_config.logs_level = "debug"; + const config = config_adapter(yaml_config); + Assert.equal(config.logs_level, "debug"); + }); + + it("should get the notifier config", function() { + const yaml_config = build_yaml_config(); + yaml_config.notifier = "notifier"; + const config = config_adapter(yaml_config); + Assert.equal(config.notifier, "notifier"); + }); + + it("should get the access_control config", function() { + const yaml_config = build_yaml_config(); + yaml_config.access_control = "access_control"; + const config = config_adapter(yaml_config); + Assert.equal(config.access_control, "access_control"); + }); +}); diff --git a/test/unitary/test_config_adapter.js b/test/unitary/test_config_adapter.js deleted file mode 100644 index 5ffcc84a3..000000000 --- a/test/unitary/test_config_adapter.js +++ /dev/null @@ -1,76 +0,0 @@ - -var assert = require('assert'); -var config_adapter = require('../../src/lib/config_adapter'); - -describe('test config adapter', function() { - it('should read the port from the yaml file', function() { - yaml_config = {}; - yaml_config.port = 7070; - var config = config_adapter(yaml_config); - assert.equal(config.port, 7070); - }); - - it('should default the port to 8080 if not provided', function() { - yaml_config = {}; - var config = config_adapter(yaml_config); - assert.equal(config.port, 8080); - }); - - it('should get the ldap attributes', function() { - yaml_config = {}; - yaml_config.ldap = {}; - yaml_config.ldap.url = 'http://ldap'; - yaml_config.ldap.user_search_base = 'ou=groups,dc=example,dc=com'; - yaml_config.ldap.user_search_filter = 'uid'; - yaml_config.ldap.user = 'admin'; - yaml_config.ldap.password = 'pass'; - - var config = config_adapter(yaml_config); - - assert.equal(config.ldap.url, 'http://ldap'); - assert.equal(config.ldap.user_search_base, 'ou=groups,dc=example,dc=com'); - assert.equal(config.ldap.user_search_filter, 'uid'); - assert.equal(config.ldap.user, 'admin'); - assert.equal(config.ldap.password, 'pass'); - }); - - it('should get the session attributes', function() { - yaml_config = {}; - yaml_config.session = {}; - yaml_config.session.domain = 'example.com'; - yaml_config.session.secret = 'secret'; - yaml_config.session.expiration = 3600; - - var config = config_adapter(yaml_config); - - assert.equal(config.session_domain, 'example.com'); - assert.equal(config.session_secret, 'secret'); - assert.equal(config.session_max_age, 3600); - }); - - it('should get the log level', function() { - yaml_config = {}; - yaml_config.logs_level = 'debug'; - - var config = config_adapter(yaml_config); - assert.equal(config.logs_level, 'debug'); - }); - - it('should get the notifier config', function() { - yaml_config = {}; - yaml_config.notifier = 'notifier'; - - var config = config_adapter(yaml_config); - - assert.equal(config.notifier, 'notifier'); - }); - - it('should get the access_control config', function() { - yaml_config = {}; - yaml_config.access_control = 'access_control'; - - var config = config_adapter(yaml_config); - - assert.equal(config.access_control, 'access_control'); - }); -}); diff --git a/test/unitary/test_data_persistence.js b/test/unitary/test_data_persistence.js index 35c2c9807..ec6efbdb1 100644 --- a/test/unitary/test_data_persistence.js +++ b/test/unitary/test_data_persistence.js @@ -12,8 +12,6 @@ var session = require('express-session'); var winston = require('winston'); var PORT = 8050; -var BASE_URL = 'http://localhost:' + PORT; - var requests = require('./requests')(PORT); diff --git a/test/unitary/test_server_config.js b/test/unitary/test_server_config.js index b5c26b5e3..aadca1259 100644 --- a/test/unitary/test_server_config.js +++ b/test/unitary/test_server_config.js @@ -41,6 +41,7 @@ describe('test server configuration', function() { it('should set cookie scope to domain set in the config', function() { config.session = {}; config.session.domain = 'example.com'; + config.session.secret = 'secret'; config.ldap = {}; config.ldap.url = 'http://ldap'; server.run(config, deps); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..40cc38223 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "noImplicitAny": true, + "moduleResolution": "node", + "sourceMap": true, + "outDir": "dist", + "baseUrl": ".", + "allowJs": true, + "paths": { + "*": [ + "node_modules/*", + "src/types/*" + ] + } + }, + "include": [ + "src/**/*" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 000000000..c2c1b7501 --- /dev/null +++ b/tslint.json @@ -0,0 +1,60 @@ +{ + "rules": { + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "indent": [ + true, + "spaces" + ], + "one-line": [ + true, + "check-open-brace", + "check-whitespace" + ], + "no-var-keyword": true, + "quotemark": [ + true, + "double", + "avoid-escape" + ], + "semicolon": [ + true, + "always", + "ignore-bound-class-methods" + ], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-module", + "check-separator", + "check-type" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + }, + { + "call-signature": "onespace", + "index-signature": "onespace", + "parameter": "onespace", + "property-declaration": "onespace", + "variable-declaration": "onespace" + } + ], + "no-internal-module": true, + "no-trailing-whitespace": true, + "no-null-keyword": true, + "prefer-const": true, + "jsdoc-format": true + } +}