@accounts/password
Advanced tools
Comparing version 0.1.0-alpha.737c05ed to 0.1.0-alpha.f9ef5e7f
@@ -1,3 +0,2 @@ | ||
import { CreateUserType, UserObjectType, HashAlgorithm } from '@accounts/common'; | ||
import { PasswordCreateUserType, PasswordLoginType, PasswordType } from './types'; | ||
import { CreateUserType, UserObjectType, HashAlgorithm, PasswordLoginType } from '@accounts/common'; | ||
export declare const isEmail: (email?: string) => boolean; | ||
@@ -15,19 +14,10 @@ export interface AccountsPasswordOptions { | ||
export default class AccountsPassword { | ||
private serviceName; | ||
private options; | ||
private db; | ||
private server; | ||
constructor(options: AccountsPasswordOptions); | ||
authenticate(params: PasswordLoginType): Promise<UserObjectType>; | ||
findUserByEmail(email: string): Promise<UserObjectType>; | ||
findUserByUsername(username: string): Promise<UserObjectType>; | ||
addEmail(userId: string, newEmail: string, verified: boolean): Promise<void>; | ||
createUser(user: CreateUserType): Promise<string>; | ||
setUsername(userId: string, newUsername: string): Promise<void>; | ||
addEmail(userId: string, newEmail: string, verified?: boolean): Promise<void>; | ||
removeEmail(userId: string, email: string): Promise<void>; | ||
verifyEmail(token: string): Promise<void>; | ||
resetPassword(token: string, newPassword: PasswordType): Promise<void>; | ||
setPassword(userId: string, newPassword: string): Promise<void>; | ||
sendVerificationEmail(address?: string): Promise<void>; | ||
sendResetPasswordEmail(address: string): Promise<void>; | ||
sendEnrollmentEmail(address: string): Promise<void>; | ||
createUser(user: PasswordCreateUserType): Promise<string>; | ||
private passwordAuthenticator(user, password); | ||
@@ -34,0 +24,0 @@ private hashAndBcryptPassword(password); |
@@ -47,4 +47,2 @@ "use strict"; | ||
var lodash_1 = require("lodash"); | ||
var server_1 = require("@accounts/server"); | ||
var utils_1 = require("@accounts/server/lib/utils"); | ||
var encryption_1 = require("./encryption"); | ||
@@ -101,169 +99,2 @@ exports.isEmail = function (email) { | ||
}; | ||
AccountsPassword.prototype.findUserByEmail = function (email) { | ||
return this.db.findUserByEmail(email); | ||
}; | ||
AccountsPassword.prototype.findUserByUsername = function (username) { | ||
return this.db.findUserByUsername(username); | ||
}; | ||
AccountsPassword.prototype.addEmail = function (userId, newEmail, verified) { | ||
return this.db.addEmail(userId, newEmail, verified); | ||
}; | ||
AccountsPassword.prototype.removeEmail = function (userId, email) { | ||
return this.db.removeEmail(userId, email); | ||
}; | ||
AccountsPassword.prototype.verifyEmail = function (token) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var user, verificationTokens, tokenRecord, emailRecord; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4, this.db.findUserByEmailVerificationToken(token)]; | ||
case 1: | ||
user = _a.sent(); | ||
if (!user) { | ||
throw new Error('Verify email link expired'); | ||
} | ||
verificationTokens = lodash_1.get(user, ['services', 'email', 'verificationTokens'], []); | ||
tokenRecord = lodash_1.find(verificationTokens, function (t) { return t.token === token; }); | ||
if (!tokenRecord) { | ||
throw new Error('Verify email link expired'); | ||
} | ||
emailRecord = lodash_1.find(user.emails, function (e) { return e.address === tokenRecord.address; }); | ||
if (!emailRecord) { | ||
throw new Error('Verify email link is for unknown address'); | ||
} | ||
return [4, this.db.verifyEmail(user.id, emailRecord.address)]; | ||
case 2: | ||
_a.sent(); | ||
return [2]; | ||
} | ||
}); | ||
}); | ||
}; | ||
AccountsPassword.prototype.resetPassword = function (token, newPassword) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var user, resetTokens, resetTokenRecord, emails, password; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4, this.db.findUserByResetPasswordToken(token)]; | ||
case 1: | ||
user = _a.sent(); | ||
if (!user) { | ||
throw new Error('Reset password link expired'); | ||
} | ||
resetTokens = lodash_1.get(user, ['services', 'password', 'reset']); | ||
resetTokenRecord = lodash_1.find(resetTokens, function (t) { return t.token === token; }); | ||
if (this.server.isTokenExpired(token, resetTokenRecord)) { | ||
throw new Error('Reset password link expired'); | ||
} | ||
emails = user.emails || []; | ||
if (!lodash_1.includes(emails.map(function (email) { return email.address; }), resetTokenRecord.address)) { | ||
throw new Error('Token has invalid email address'); | ||
} | ||
return [4, this.hashAndBcryptPassword(newPassword)]; | ||
case 2: | ||
password = _a.sent(); | ||
return [4, this.db.setResetPassword(user.id, resetTokenRecord.address, password, token)]; | ||
case 3: | ||
_a.sent(); | ||
this.db.invalidateAllSessions(user.id); | ||
return [2]; | ||
} | ||
}); | ||
}); | ||
}; | ||
AccountsPassword.prototype.setPassword = function (userId, newPassword) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var password; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4, encryption_1.bcryptPassword(newPassword)]; | ||
case 1: | ||
password = _a.sent(); | ||
return [2, this.db.setPassword(userId, password)]; | ||
} | ||
}); | ||
}); | ||
}; | ||
AccountsPassword.prototype.sendVerificationEmail = function (address) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var user, email, emails, token, resetPasswordMail; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4, this.db.findUserByEmail(address)]; | ||
case 1: | ||
user = _a.sent(); | ||
if (!user) { | ||
throw new Error('User not found'); | ||
} | ||
if (!address) { | ||
email = lodash_1.find(user.emails, function (e) { return !e.verified; }); | ||
address = email && email.address; | ||
} | ||
emails = user.emails || []; | ||
if (!address || !lodash_1.includes(emails.map(function (email) { return email.address; }), address)) { | ||
throw new Error('No such email address for user'); | ||
} | ||
token = server_1.generateRandomToken(); | ||
return [4, this.db.addEmailVerificationToken(user.id, address, token)]; | ||
case 2: | ||
_a.sent(); | ||
resetPasswordMail = this.server.prepareMail(address, token, this.server.sanitizeUser(user), 'verify-email', this.server.options.emailTemplates.verifyEmail, this.server.options.emailTemplates.from); | ||
return [4, this.server.email.sendMail(resetPasswordMail)]; | ||
case 3: | ||
_a.sent(); | ||
return [2]; | ||
} | ||
}); | ||
}); | ||
}; | ||
AccountsPassword.prototype.sendResetPasswordEmail = function (address) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var user, token, resetPasswordMail; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4, this.db.findUserByEmail(address)]; | ||
case 1: | ||
user = _a.sent(); | ||
if (!user) { | ||
throw new Error('User not found'); | ||
} | ||
address = utils_1.getFirstUserEmail(user, address); | ||
token = server_1.generateRandomToken(); | ||
return [4, this.db.addResetPasswordToken(user.id, address, token)]; | ||
case 2: | ||
_a.sent(); | ||
resetPasswordMail = this.server.prepareMail(address, token, this.server.sanitizeUser(user), 'reset-password', this.server.options.emailTemplates.resetPassword, this.server.options.emailTemplates.from); | ||
return [4, this.server.email.sendMail(resetPasswordMail)]; | ||
case 3: | ||
_a.sent(); | ||
return [2]; | ||
} | ||
}); | ||
}); | ||
}; | ||
AccountsPassword.prototype.sendEnrollmentEmail = function (address) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var user, token, enrollmentMail; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4, this.db.findUserByEmail(address)]; | ||
case 1: | ||
user = _a.sent(); | ||
if (!user) { | ||
throw new Error('User not found'); | ||
} | ||
address = utils_1.getFirstUserEmail(user, address); | ||
token = server_1.generateRandomToken(); | ||
return [4, this.db.addResetPasswordToken(user.id, address, token, 'enroll')]; | ||
case 2: | ||
_a.sent(); | ||
enrollmentMail = this.server.prepareMail(address, token, this.server.sanitizeUser(user), 'enroll-account', this.server.options.emailTemplates.enrollAccount, this.server.options.emailTemplates.from); | ||
return [4, this.server.email.sendMail(enrollmentMail)]; | ||
case 3: | ||
_a.sent(); | ||
return [2]; | ||
} | ||
}); | ||
}); | ||
}; | ||
AccountsPassword.prototype.createUser = function (user) { | ||
@@ -327,2 +158,12 @@ return __awaiter(this, void 0, void 0, function () { | ||
}; | ||
AccountsPassword.prototype.setUsername = function (userId, newUsername) { | ||
return this.db.setUsername(userId, newUsername); | ||
}; | ||
AccountsPassword.prototype.addEmail = function (userId, newEmail, verified) { | ||
if (verified === void 0) { verified = false; } | ||
return this.db.addEmail(userId, newEmail, verified); | ||
}; | ||
AccountsPassword.prototype.removeEmail = function (userId, email) { | ||
return this.db.removeEmail(userId, email); | ||
}; | ||
AccountsPassword.prototype.passwordAuthenticator = function (user, password) { | ||
@@ -329,0 +170,0 @@ return __awaiter(this, void 0, void 0, function () { |
@@ -1,4 +0,3 @@ | ||
import { PasswordType } from './types'; | ||
export declare const bcryptPassword: (password: string) => Promise<string>; | ||
export declare const hashPassword: (password: PasswordType, algorithm: string) => string; | ||
export declare const hashPassword: (password: any, algorithm: string) => any; | ||
export declare const verifyPassword: (password: string, hash: string) => Promise<boolean>; |
{ | ||
"name": "@accounts/password", | ||
"version": "0.1.0-alpha.737c05ed", | ||
"version": "0.1.0-alpha.f9ef5e7f", | ||
"license": "MIT", | ||
"main": "lib/index.js", | ||
"scripts": { | ||
"clean": "rimraf lib", | ||
"start": "tsc --watch", | ||
"precompile": "npm run clean", | ||
"compile": "tsc", | ||
"prepublish": "npm run compile", | ||
"testonly": "jest --coverage", | ||
"testonly": "jest", | ||
"coverage": "jest --coverage" | ||
}, | ||
"jest": { | ||
"transform": { | ||
".(ts|tsx)": "<rootDir>/../../node_modules/ts-jest/preprocessor.js" | ||
}, | ||
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx)$", | ||
"moduleFileExtensions": [ | ||
"ts", | ||
"js" | ||
], | ||
"mapCoverage": true | ||
}, | ||
"dependencies": { | ||
@@ -31,4 +17,2 @@ "bcryptjs": "^2.4.3", | ||
"devDependencies": { | ||
"@accounts/common": "^0.1.0-alpha.737c05ed", | ||
"@accounts/server": "^0.1.0-alpha.737c05ed", | ||
"@types/bcryptjs": "^2.4.0", | ||
@@ -38,5 +22,4 @@ "@types/lodash": "^4.14.66" | ||
"peerDependencies": { | ||
"@accounts/common": "^0.1.0-alpha.737c05ed", | ||
"@accounts/server": "^0.1.0-alpha.737c05ed" | ||
"@accounts/common": "^0.1.0" | ||
} | ||
} |
@@ -0,31 +1,12 @@ | ||
import { trim, isEmpty, isFunction, isString, isPlainObject } from 'lodash'; | ||
import { | ||
trim, | ||
isEmpty, | ||
isFunction, | ||
isString, | ||
isPlainObject, | ||
get, | ||
find, | ||
includes, | ||
} from 'lodash'; | ||
import { | ||
CreateUserType, | ||
UserObjectType, | ||
HashAlgorithm, | ||
LoginUserIdentityType, | ||
EmailRecord, | ||
TokenRecord, | ||
PasswordType, | ||
DBInterface, | ||
PasswordLoginType, | ||
PasswordLoginUserIdentityType, | ||
} from '@accounts/common'; | ||
import { | ||
DBInterface, | ||
AccountsServer, | ||
generateRandomToken, | ||
} from '@accounts/server'; | ||
import { getFirstUserEmail } from '@accounts/server/lib/utils'; | ||
import { hashPassword, bcryptPassword, verifyPassword } from './encryption'; | ||
import { | ||
PasswordCreateUserType, | ||
PasswordLoginType, | ||
PasswordType, | ||
} from './types'; | ||
@@ -69,6 +50,4 @@ export const isEmail = (email?: string) => { | ||
export default class AccountsPassword { | ||
private serviceName: string; | ||
private options: AccountsPasswordOptions; | ||
private db: DBInterface; | ||
private server: AccountsServer; | ||
@@ -108,231 +87,2 @@ constructor(options: AccountsPasswordOptions) { | ||
/** | ||
* @description Find a user by one of his emails. | ||
* @param {string} email - User email. | ||
* @returns {Promise<Object>} - Return a user or null if not found. | ||
*/ | ||
public findUserByEmail(email: string): Promise<UserObjectType> { | ||
return this.db.findUserByEmail(email); | ||
} | ||
/** | ||
* @description Find a user by his username. | ||
* @param {string} username - User username. | ||
* @returns {Promise<Object>} - Return a user or null if not found. | ||
*/ | ||
public findUserByUsername(username: string): Promise<UserObjectType> { | ||
return this.db.findUserByUsername(username); | ||
} | ||
/** | ||
* @description Add an email address for a user. | ||
* Use this instead of directly updating the database. | ||
* @param {string} userId - User id. | ||
* @param {string} newEmail - A new email address for the user. | ||
* @param {boolean} [verified] - Whether the new email address should be marked as verified. | ||
* Defaults to false. | ||
* @returns {Promise<void>} - Return a Promise. | ||
*/ | ||
public addEmail( | ||
userId: string, | ||
newEmail: string, | ||
verified: boolean | ||
): Promise<void> { | ||
return this.db.addEmail(userId, newEmail, verified); | ||
} | ||
/** | ||
* @description Remove an email address for a user. | ||
* Use this instead of directly updating the database. | ||
* @param {string} userId - User id. | ||
* @param {string} email - The email address to remove. | ||
* @returns {Promise<void>} - Return a Promise. | ||
*/ | ||
public removeEmail(userId: string, email: string): Promise<void> { | ||
return this.db.removeEmail(userId, email); | ||
} | ||
/** | ||
* @description Marks the user's email address as verified. | ||
* @param {string} token - The token retrieved from the verification URL. | ||
* @returns {Promise<void>} - Return a Promise. | ||
*/ | ||
public async verifyEmail(token: string): Promise<void> { | ||
const user = await this.db.findUserByEmailVerificationToken(token); | ||
if (!user) { | ||
throw new Error('Verify email link expired'); | ||
} | ||
const verificationTokens: TokenRecord[] = get( | ||
user, | ||
['services', 'email', 'verificationTokens'], | ||
[] | ||
); | ||
const tokenRecord = find( | ||
verificationTokens, | ||
(t: TokenRecord) => t.token === token | ||
); | ||
if (!tokenRecord) { | ||
throw new Error('Verify email link expired'); | ||
} | ||
// TODO check time for expiry date | ||
const emailRecord = find( | ||
user.emails, | ||
(e: EmailRecord) => e.address === tokenRecord.address | ||
); | ||
if (!emailRecord) { | ||
throw new Error('Verify email link is for unknown address'); | ||
} | ||
await this.db.verifyEmail(user.id, emailRecord.address); | ||
} | ||
/** | ||
* @description Reset the password for a user using a token received in email. | ||
* @param {string} token - The token retrieved from the reset password URL. | ||
* @param {string} newPassword - A new password for the user. | ||
* @returns {Promise<void>} - Return a Promise. | ||
*/ | ||
public async resetPassword( | ||
token: string, | ||
newPassword: PasswordType | ||
): Promise<void> { | ||
const user = await this.db.findUserByResetPasswordToken(token); | ||
if (!user) { | ||
throw new Error('Reset password link expired'); | ||
} | ||
// TODO move this getter into a password service module | ||
const resetTokens = get(user, ['services', 'password', 'reset']); | ||
const resetTokenRecord = find(resetTokens, t => t.token === token); | ||
if (this.server.isTokenExpired(token, resetTokenRecord)) { | ||
throw new Error('Reset password link expired'); | ||
} | ||
const emails = user.emails || []; | ||
if ( | ||
!includes( | ||
emails.map((email: EmailRecord) => email.address), | ||
resetTokenRecord.address | ||
) | ||
) { | ||
throw new Error('Token has invalid email address'); | ||
} | ||
const password = await this.hashAndBcryptPassword(newPassword); | ||
// Change the user password and remove the old token | ||
await this.db.setResetPassword( | ||
user.id, | ||
resetTokenRecord.address, | ||
password, | ||
token | ||
); | ||
// Changing the password should invalidate existing sessions | ||
this.db.invalidateAllSessions(user.id); | ||
} | ||
/** | ||
* @description Change the password for a user. | ||
* @param {string} userId - User id. | ||
* @param {string} newPassword - A new password for the user. | ||
* @returns {Promise<void>} - Return a Promise. | ||
*/ | ||
public async setPassword(userId: string, newPassword: string): Promise<void> { | ||
const password = await bcryptPassword(newPassword); | ||
return this.db.setPassword(userId, password); | ||
} | ||
/** | ||
* @description Send an email with a link the user can use verify their email address. | ||
* @param {string} [address] - Which address of the user's to send the email to. | ||
* This address must be in the user's emails list. | ||
* Defaults to the first unverified email in the list. | ||
* @returns {Promise<void>} - Return a Promise. | ||
*/ | ||
public async sendVerificationEmail(address?: string): Promise<void> { | ||
const user = await this.db.findUserByEmail(address); | ||
if (!user) { | ||
throw new Error('User not found'); | ||
} | ||
// If no address provided find the first unverified email | ||
if (!address) { | ||
const email = find(user.emails, e => !e.verified); | ||
address = email && email.address; | ||
} | ||
// Make sure the address is valid | ||
const emails = user.emails || []; | ||
if (!address || !includes(emails.map(email => email.address), address)) { | ||
throw new Error('No such email address for user'); | ||
} | ||
const token = generateRandomToken(); | ||
await this.db.addEmailVerificationToken(user.id, address, token); | ||
const resetPasswordMail = this.server.prepareMail( | ||
address, | ||
token, | ||
this.server.sanitizeUser(user), | ||
'verify-email', | ||
this.server.options.emailTemplates.verifyEmail, | ||
this.server.options.emailTemplates.from | ||
); | ||
await this.server.email.sendMail(resetPasswordMail); | ||
} | ||
/** | ||
* @description Send an email with a link the user can use to reset their password. | ||
* @param {string} [address] - Which address of the user's to send the email to. | ||
* This address must be in the user's emails list. | ||
* Defaults to the first email in the list. | ||
* @returns {Promise<void>} - Return a Promise. | ||
*/ | ||
public async sendResetPasswordEmail(address: string): Promise<void> { | ||
const user = await this.db.findUserByEmail(address); | ||
if (!user) { | ||
throw new Error('User not found'); | ||
} | ||
address = getFirstUserEmail(user, address); | ||
const token = generateRandomToken(); | ||
await this.db.addResetPasswordToken(user.id, address, token); | ||
const resetPasswordMail = this.server.prepareMail( | ||
address, | ||
token, | ||
this.server.sanitizeUser(user), | ||
'reset-password', | ||
this.server.options.emailTemplates.resetPassword, | ||
this.server.options.emailTemplates.from | ||
); | ||
await this.server.email.sendMail(resetPasswordMail); | ||
} | ||
/** | ||
* @description Send an email with a link the user can use to set their initial password. | ||
* @param {string} [address] - Which address of the user's to send the email to. | ||
* This address must be in the user's emails list. | ||
* Defaults to the first email in the list. | ||
* @returns {Promise<void>} - Return a Promise. | ||
*/ | ||
public async sendEnrollmentEmail(address: string): Promise<void> { | ||
const user = await this.db.findUserByEmail(address); | ||
if (!user) { | ||
throw new Error('User not found'); | ||
} | ||
address = getFirstUserEmail(user, address); | ||
const token = generateRandomToken(); | ||
await this.db.addResetPasswordToken(user.id, address, token, 'enroll'); | ||
const enrollmentMail = this.server.prepareMail( | ||
address, | ||
token, | ||
this.server.sanitizeUser(user), | ||
'enroll-account', | ||
this.server.options.emailTemplates.enrollAccount, | ||
this.server.options.emailTemplates.from | ||
); | ||
await this.server.email.sendMail(enrollmentMail); | ||
} | ||
/** | ||
* @description Create a new user. | ||
@@ -342,3 +92,3 @@ * @param user - The user object. | ||
*/ | ||
public async createUser(user: PasswordCreateUserType): Promise<string> { | ||
public async createUser(user: CreateUserType): Promise<string> { | ||
if ( | ||
@@ -382,4 +132,40 @@ !this.options.validateUsername(user.username) && | ||
/** | ||
* @description Change a user's username. | ||
* Use this instead of directly updating the database. | ||
* @param userId - User id. | ||
* @param newUsername - A new email address for the user. | ||
*/ | ||
public setUsername(userId: string, newUsername: string): Promise<void> { | ||
return this.db.setUsername(userId, newUsername); | ||
} | ||
/** | ||
* @description Add an email address for a user. | ||
* Use this instead of directly updating the database. | ||
* @param userId - User id. | ||
* @param newEmail - A new email address for the user. | ||
* @param [verified=false] - Whether the new email address should be marked as verified. | ||
* Defaults to false. | ||
*/ | ||
public addEmail( | ||
userId: string, | ||
newEmail: string, | ||
verified: boolean = false | ||
): Promise<void> { | ||
return this.db.addEmail(userId, newEmail, verified); | ||
} | ||
/** | ||
* @description Remove an email address for a user. | ||
* Use this instead of directly updating the database. | ||
* @param userId - User id. | ||
* @param email - The email address to remove. | ||
*/ | ||
public removeEmail(userId: string, email: string): Promise<void> { | ||
return this.db.removeEmail(userId, email); | ||
} | ||
private async passwordAuthenticator( | ||
user: string | LoginUserIdentityType, | ||
user: string | PasswordLoginUserIdentityType, | ||
password: PasswordType | ||
@@ -407,3 +193,2 @@ ): Promise<any> { | ||
} | ||
const hash = await this.db.findPasswordHash(foundUser.id); | ||
@@ -410,0 +195,0 @@ if (!hash) { |
import * as bcrypt from 'bcryptjs'; | ||
import * as crypto from 'crypto'; | ||
import { PasswordType } from './types'; | ||
import { PasswordType } from '@accounts/common'; | ||
@@ -5,0 +5,0 @@ export const bcryptPassword = async (password: string): Promise<string> => { |
@@ -6,8 +6,3 @@ { | ||
"outDir": "./lib" | ||
}, | ||
"exclude": [ | ||
"node_modules", | ||
"__tests__", | ||
"lib" | ||
] | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
3
2
1
32254
15
578