@bhandari88/express-auth 1.0.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/INSTALLATION.md +71 -0
- package/README.md +425 -0
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +7 -0
- package/dist/handlers/local-auth.d.ts +12 -0
- package/dist/handlers/local-auth.js +182 -0
- package/dist/handlers/social-auth.d.ts +15 -0
- package/dist/handlers/social-auth.js +164 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +232 -0
- package/dist/middleware/auth.middleware.d.ts +11 -0
- package/dist/middleware/auth.middleware.js +102 -0
- package/dist/types/index.d.ts +84 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/jwt.d.ts +15 -0
- package/dist/utils/jwt.js +120 -0
- package/dist/utils/password.d.ts +14 -0
- package/dist/utils/password.js +103 -0
- package/dist/utils/validator.d.ts +25 -0
- package/dist/utils/validator.js +63 -0
- package/package.json +60 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ValidatorService = exports.PasswordService = exports.JwtService = void 0;
|
|
4
|
+
var jwt_1 = require("./jwt");
|
|
5
|
+
Object.defineProperty(exports, "JwtService", { enumerable: true, get: function () { return jwt_1.JwtService; } });
|
|
6
|
+
var password_1 = require("./password");
|
|
7
|
+
Object.defineProperty(exports, "PasswordService", { enumerable: true, get: function () { return password_1.PasswordService; } });
|
|
8
|
+
var validator_1 = require("./validator");
|
|
9
|
+
Object.defineProperty(exports, "ValidatorService", { enumerable: true, get: function () { return validator_1.ValidatorService; } });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { JwtPayload, AuthConfig, AuthTokens } from '../types';
|
|
2
|
+
export declare class JwtService {
|
|
3
|
+
private accessTokenSecret;
|
|
4
|
+
private refreshTokenSecret;
|
|
5
|
+
private accessTokenExpiresIn;
|
|
6
|
+
private refreshTokenExpiresIn;
|
|
7
|
+
private enableRefreshTokens;
|
|
8
|
+
constructor(config: AuthConfig);
|
|
9
|
+
generateAccessToken(payload: Omit<JwtPayload, 'type' | 'iat' | 'exp'>): string;
|
|
10
|
+
generateRefreshToken(payload: Omit<JwtPayload, 'type' | 'iat' | 'exp'>): string;
|
|
11
|
+
generateTokens(payload: Omit<JwtPayload, 'type' | 'iat' | 'exp'>): AuthTokens;
|
|
12
|
+
verifyAccessToken(token: string): JwtPayload;
|
|
13
|
+
verifyRefreshToken(token: string): JwtPayload;
|
|
14
|
+
private getExpiresInSeconds;
|
|
15
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.JwtService = void 0;
|
|
37
|
+
const jwt = __importStar(require("jsonwebtoken"));
|
|
38
|
+
class JwtService {
|
|
39
|
+
constructor(config) {
|
|
40
|
+
this.accessTokenSecret = config.jwtSecret;
|
|
41
|
+
this.refreshTokenSecret = config.refreshTokenSecret || config.jwtSecret;
|
|
42
|
+
this.accessTokenExpiresIn = config.jwtExpiresIn || '15m';
|
|
43
|
+
this.refreshTokenExpiresIn = config.refreshTokenExpiresIn || '7d';
|
|
44
|
+
this.enableRefreshTokens = config.enableRefreshTokens ?? true;
|
|
45
|
+
}
|
|
46
|
+
generateAccessToken(payload) {
|
|
47
|
+
return jwt.sign({ ...payload, type: 'access' }, this.accessTokenSecret, { expiresIn: this.accessTokenExpiresIn });
|
|
48
|
+
}
|
|
49
|
+
generateRefreshToken(payload) {
|
|
50
|
+
if (!this.enableRefreshTokens) {
|
|
51
|
+
throw new Error('Refresh tokens are not enabled');
|
|
52
|
+
}
|
|
53
|
+
return jwt.sign({ ...payload, type: 'refresh' }, this.refreshTokenSecret, { expiresIn: this.refreshTokenExpiresIn });
|
|
54
|
+
}
|
|
55
|
+
generateTokens(payload) {
|
|
56
|
+
const accessToken = this.generateAccessToken(payload);
|
|
57
|
+
const tokens = {
|
|
58
|
+
accessToken,
|
|
59
|
+
expiresIn: this.getExpiresInSeconds(this.accessTokenExpiresIn),
|
|
60
|
+
};
|
|
61
|
+
if (this.enableRefreshTokens) {
|
|
62
|
+
tokens.refreshToken = this.generateRefreshToken(payload);
|
|
63
|
+
}
|
|
64
|
+
return tokens;
|
|
65
|
+
}
|
|
66
|
+
verifyAccessToken(token) {
|
|
67
|
+
try {
|
|
68
|
+
const decoded = jwt.verify(token, this.accessTokenSecret);
|
|
69
|
+
if (decoded.type !== 'access') {
|
|
70
|
+
throw new Error('Invalid token type');
|
|
71
|
+
}
|
|
72
|
+
return decoded;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error instanceof jwt.TokenExpiredError) {
|
|
76
|
+
throw new Error('Access token expired');
|
|
77
|
+
}
|
|
78
|
+
if (error instanceof jwt.JsonWebTokenError) {
|
|
79
|
+
throw new Error('Invalid access token');
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
verifyRefreshToken(token) {
|
|
85
|
+
if (!this.enableRefreshTokens) {
|
|
86
|
+
throw new Error('Refresh tokens are not enabled');
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const decoded = jwt.verify(token, this.refreshTokenSecret);
|
|
90
|
+
if (decoded.type !== 'refresh') {
|
|
91
|
+
throw new Error('Invalid token type');
|
|
92
|
+
}
|
|
93
|
+
return decoded;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof jwt.TokenExpiredError) {
|
|
97
|
+
throw new Error('Refresh token expired');
|
|
98
|
+
}
|
|
99
|
+
if (error instanceof jwt.JsonWebTokenError) {
|
|
100
|
+
throw new Error('Invalid refresh token');
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
getExpiresInSeconds(expiresIn) {
|
|
106
|
+
const match = expiresIn.match(/^(\d+)([smhd])$/);
|
|
107
|
+
if (!match)
|
|
108
|
+
return 900; // Default 15 minutes
|
|
109
|
+
const value = parseInt(match[1]);
|
|
110
|
+
const unit = match[2];
|
|
111
|
+
switch (unit) {
|
|
112
|
+
case 's': return value;
|
|
113
|
+
case 'm': return value * 60;
|
|
114
|
+
case 'h': return value * 60 * 60;
|
|
115
|
+
case 'd': return value * 24 * 60 * 60;
|
|
116
|
+
default: return 900;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
exports.JwtService = JwtService;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AuthConfig } from '../types';
|
|
2
|
+
export declare class PasswordService {
|
|
3
|
+
private keylen;
|
|
4
|
+
private saltLength;
|
|
5
|
+
private cost;
|
|
6
|
+
constructor(config: AuthConfig);
|
|
7
|
+
private scryptAsync;
|
|
8
|
+
hash(password: string): Promise<string>;
|
|
9
|
+
compare(password: string, hashedPassword: string): Promise<boolean>;
|
|
10
|
+
validatePassword(password: string): {
|
|
11
|
+
valid: boolean;
|
|
12
|
+
message?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PasswordService = void 0;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
38
|
+
class PasswordService {
|
|
39
|
+
constructor(config) {
|
|
40
|
+
// Use keylen instead of rounds for scrypt
|
|
41
|
+
// scrypt is memory-hard and more secure than bcrypt
|
|
42
|
+
this.keylen = 64; // 64 bytes = 512 bits
|
|
43
|
+
this.saltLength = 32; // 32 bytes = 256 bits
|
|
44
|
+
this.cost = config.bcryptRounds || 16384; // CPU/memory cost parameter (2^14)
|
|
45
|
+
}
|
|
46
|
+
async scryptAsync(password, salt, keylen, options) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
crypto.scrypt(password, salt, keylen, options || {}, (err, derivedKey) => {
|
|
49
|
+
if (err)
|
|
50
|
+
reject(err);
|
|
51
|
+
else
|
|
52
|
+
resolve(derivedKey);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async hash(password) {
|
|
57
|
+
if (!password || password.length < 6) {
|
|
58
|
+
throw new Error('Password must be at least 6 characters long');
|
|
59
|
+
}
|
|
60
|
+
// Generate random salt
|
|
61
|
+
const salt = crypto.randomBytes(this.saltLength);
|
|
62
|
+
// Hash password using scrypt
|
|
63
|
+
const hash = await this.scryptAsync(password, salt, this.keylen, { N: this.cost });
|
|
64
|
+
// Return salt:hash as base64 encoded string
|
|
65
|
+
const saltBase64 = salt.toString('base64');
|
|
66
|
+
const hashBase64 = hash.toString('base64');
|
|
67
|
+
return `${saltBase64}:${hashBase64}`;
|
|
68
|
+
}
|
|
69
|
+
async compare(password, hashedPassword) {
|
|
70
|
+
try {
|
|
71
|
+
// Extract salt and hash from stored string (format: salt:hash)
|
|
72
|
+
const [saltBase64, hashBase64] = hashedPassword.split(':');
|
|
73
|
+
if (!saltBase64 || !hashBase64) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const salt = Buffer.from(saltBase64, 'base64');
|
|
77
|
+
const storedHash = Buffer.from(hashBase64, 'base64');
|
|
78
|
+
// Hash the provided password with the extracted salt
|
|
79
|
+
const derivedHash = await this.scryptAsync(password, salt, this.keylen, { N: this.cost });
|
|
80
|
+
// Use timing-safe comparison to prevent timing attacks
|
|
81
|
+
if (storedHash.length !== derivedHash.length) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return crypto.timingSafeEqual(storedHash, derivedHash);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
validatePassword(password) {
|
|
91
|
+
if (!password) {
|
|
92
|
+
return { valid: false, message: 'Password is required' };
|
|
93
|
+
}
|
|
94
|
+
if (password.length < 6) {
|
|
95
|
+
return { valid: false, message: 'Password must be at least 6 characters long' };
|
|
96
|
+
}
|
|
97
|
+
if (password.length > 128) {
|
|
98
|
+
return { valid: false, message: 'Password must be less than 128 characters' };
|
|
99
|
+
}
|
|
100
|
+
return { valid: true };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.PasswordService = PasswordService;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare class ValidatorService {
|
|
2
|
+
static validateEmail(email: string): boolean;
|
|
3
|
+
static validatePhone(phone: string): boolean;
|
|
4
|
+
static validateUsername(username: string): boolean;
|
|
5
|
+
static sanitizeEmail(email: string): string;
|
|
6
|
+
static sanitizePhone(phone: string): string;
|
|
7
|
+
static validateLoginCredentials(credentials: {
|
|
8
|
+
email?: string;
|
|
9
|
+
username?: string;
|
|
10
|
+
phone?: string;
|
|
11
|
+
password: string;
|
|
12
|
+
}): {
|
|
13
|
+
valid: boolean;
|
|
14
|
+
message?: string;
|
|
15
|
+
};
|
|
16
|
+
static validateRegisterData(data: {
|
|
17
|
+
email?: string;
|
|
18
|
+
username?: string;
|
|
19
|
+
phone?: string;
|
|
20
|
+
password: string;
|
|
21
|
+
}): {
|
|
22
|
+
valid: boolean;
|
|
23
|
+
message?: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ValidatorService = void 0;
|
|
7
|
+
const validator_1 = __importDefault(require("validator"));
|
|
8
|
+
class ValidatorService {
|
|
9
|
+
static validateEmail(email) {
|
|
10
|
+
return validator_1.default.isEmail(email);
|
|
11
|
+
}
|
|
12
|
+
static validatePhone(phone) {
|
|
13
|
+
// Allow international format with + prefix
|
|
14
|
+
return validator_1.default.isMobilePhone(phone.replace(/\s/g, ''), 'any', { strictMode: false });
|
|
15
|
+
}
|
|
16
|
+
static validateUsername(username) {
|
|
17
|
+
// Username: 3-30 characters, alphanumeric, underscore, hyphen
|
|
18
|
+
return /^[a-zA-Z0-9_-]{3,30}$/.test(username);
|
|
19
|
+
}
|
|
20
|
+
static sanitizeEmail(email) {
|
|
21
|
+
return validator_1.default.normalizeEmail(email) || email;
|
|
22
|
+
}
|
|
23
|
+
static sanitizePhone(phone) {
|
|
24
|
+
// Remove spaces and ensure proper format
|
|
25
|
+
return phone.replace(/\s/g, '').replace(/^(\d{10})$/, '+1$1'); // Add country code if missing
|
|
26
|
+
}
|
|
27
|
+
static validateLoginCredentials(credentials) {
|
|
28
|
+
const { email, username, phone, password } = credentials;
|
|
29
|
+
// At least one identifier must be provided
|
|
30
|
+
if (!email && !username && !phone) {
|
|
31
|
+
return { valid: false, message: 'Email, username, or phone is required' };
|
|
32
|
+
}
|
|
33
|
+
// Password is required
|
|
34
|
+
if (!password) {
|
|
35
|
+
return { valid: false, message: 'Password is required' };
|
|
36
|
+
}
|
|
37
|
+
// Validate email if provided
|
|
38
|
+
if (email && !this.validateEmail(email)) {
|
|
39
|
+
return { valid: false, message: 'Invalid email format' };
|
|
40
|
+
}
|
|
41
|
+
// Validate phone if provided
|
|
42
|
+
if (phone && !this.validatePhone(phone)) {
|
|
43
|
+
return { valid: false, message: 'Invalid phone format' };
|
|
44
|
+
}
|
|
45
|
+
// Validate username if provided
|
|
46
|
+
if (username && !this.validateUsername(username)) {
|
|
47
|
+
return { valid: false, message: 'Username must be 3-30 characters and contain only letters, numbers, underscores, and hyphens' };
|
|
48
|
+
}
|
|
49
|
+
return { valid: true };
|
|
50
|
+
}
|
|
51
|
+
static validateRegisterData(data) {
|
|
52
|
+
// At least one identifier must be provided
|
|
53
|
+
if (!data.email && !data.username && !data.phone) {
|
|
54
|
+
return { valid: false, message: 'At least one of email, username, or phone is required' };
|
|
55
|
+
}
|
|
56
|
+
const validation = this.validateLoginCredentials(data);
|
|
57
|
+
if (!validation.valid) {
|
|
58
|
+
return validation;
|
|
59
|
+
}
|
|
60
|
+
return { valid: true };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.ValidatorService = ValidatorService;
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bhandari88/express-auth",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Plug-and-play authentication handler for Express.js with TypeScript supporting email, username, phone, and social login",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch",
|
|
10
|
+
"prepublishOnly": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"authentication",
|
|
14
|
+
"auth",
|
|
15
|
+
"express",
|
|
16
|
+
"jwt",
|
|
17
|
+
"typescript",
|
|
18
|
+
"social-login",
|
|
19
|
+
"passport"
|
|
20
|
+
],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"jsonwebtoken": "^9.0.2",
|
|
25
|
+
"passport": "^0.7.0",
|
|
26
|
+
"passport-google-oauth20": "^2.0.0",
|
|
27
|
+
"passport-facebook": "^3.0.0",
|
|
28
|
+
"validator": "^13.11.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/express": "^4.17.21",
|
|
32
|
+
"@types/jsonwebtoken": "^9.0.5",
|
|
33
|
+
"@types/passport": "^1.0.16",
|
|
34
|
+
"@types/passport-google-oauth20": "^2.0.14",
|
|
35
|
+
"@types/passport-facebook": "^3.0.3",
|
|
36
|
+
"@types/validator": "^13.11.7",
|
|
37
|
+
"@types/node": "^20.10.5",
|
|
38
|
+
"typescript": "^5.3.3"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"express": "^4.18.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"express": {
|
|
45
|
+
"optional": false
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": ""
|
|
54
|
+
},
|
|
55
|
+
"files": [
|
|
56
|
+
"dist/**/*",
|
|
57
|
+
"README.md",
|
|
58
|
+
"INSTALLATION.md"
|
|
59
|
+
]
|
|
60
|
+
}
|