[PS-816] Add Autofill Shortcut to MV3 Extension (#3131)

* Work on background service worker.

* Work on shortcuts

* Work on supporting service worker

* Put new background behind version check

* Fix build

* Use new storage service

* create commands from crypto service (#2995)

* Work on service worker autofill

* Got basic autofill working

* Final touches

* Work on tests

* Revert some changes

* Add modifications

* Remove unused ciphers for now

* Cleanup

* Address PR feedback

* Update lock file

* Update noop service

* Add chrome type

* Handle "/" in branch names

Updates web workflow to handle the `/` in branch names when it's a PR.

* Remove any

Co-authored-by: Jake Fink <jfink@bitwarden.com>
Co-authored-by: Micaiah Martin <77340197+mimartin12@users.noreply.github.com>
This commit is contained in:
Justin Baur 2022-08-09 21:30:26 -04:00 committed by GitHub
parent cfc8858ef9
commit 43d428b3df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 488 additions and 183 deletions

View File

@ -1,7 +1,7 @@
{
"extends": "../tsconfig",
"compilerOptions": {
"types": ["node", "jest"],
"types": ["node", "jest", "chrome"],
"allowSyntheticDefaultImports": true
},
"exclude": ["../src/test.setup.ts", "../apps/src/**/*.spec.ts", "../libs/**/*.spec.ts"],

View File

@ -1,6 +1,13 @@
import MainBackground from "./background/main.background";
import { onCommandListener } from "./listeners/onCommandListener";
const manifest = chrome.runtime.getManifest();
if (manifest.manifest_version === 3) {
chrome.commands.onCommand.addListener(onCommandListener);
} else {
const bitwardenMain = ((window as any).bitwardenMain = new MainBackground());
bitwardenMain.bootstrap().then(() => {
// Finished bootstrapping
});
}

View File

@ -81,6 +81,10 @@ export default class ContextMenusBackground {
}
private async cipherAction(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) {
if (typeof info.menuItemId !== "string") {
return;
}
const id = info.menuItemId.split("_")[1];
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {

View File

@ -14,7 +14,10 @@ export default class WebRequestBackground {
private cipherService: CipherService,
private authService: AuthService
) {
const manifest = chrome.runtime.getManifest();
if (manifest.manifest_version === 2) {
this.webRequest = (window as any).chrome.webRequest;
}
this.isFirefox = platformUtilsService.isFirefox();
}

View File

@ -0,0 +1,44 @@
import AutofillPageDetails from "../models/autofillPageDetails";
import { AutofillService } from "../services/abstractions/autofill.service";
export class AutoFillActiveTabCommand {
constructor(private autofillService: AutofillService) {}
async doAutoFillActiveTabCommand(tab: chrome.tabs.Tab) {
if (!tab.id) {
throw new Error("Tab does not have an id, cannot complete autofill.");
}
const details = await this.collectPageDetails(tab.id);
await this.autofillService.doAutoFillOnTab(
[
{
frameId: 0,
tab: tab,
details: details,
},
],
tab,
true
);
}
private async collectPageDetails(tabId: number): Promise<AutofillPageDetails> {
return new Promise((resolve, reject) => {
chrome.tabs.sendMessage(
tabId,
{
command: "collectPageDetailsImmediately",
},
(response: AutofillPageDetails) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
}
resolve(response);
}
);
});
}
}

View File

@ -39,6 +39,7 @@
6. Rename com.agilebits.* stuff to com.bitwarden.*
7. Remove "some useful globals" on window
8. Add ability to autofill span[data-bwautofill] elements
9. Add new handler, for new command that responds with page details in response callback
*/
function collect(document, undefined) {
@ -1037,6 +1038,11 @@
fill(document, msg.fillScript);
sendResponse();
return true;
} else if (msg.command === 'collectPageDetailsImmediately') {
var pageDetails = collect(document);
var pageDetailsObj = JSON.parse(pageDetails);
sendResponse(pageDetailsObj);
return true;
}
});
})();

View File

@ -0,0 +1,140 @@
import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
import { AuthService } from "@bitwarden/common/services/auth.service";
import { CipherService } from "@bitwarden/common/services/cipher.service";
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
import { EncryptService } from "@bitwarden/common/services/encrypt.service";
import NoOpEventService from "@bitwarden/common/services/noOpEvent.service";
import { SearchService } from "@bitwarden/common/services/search.service";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service";
import { WebCryptoFunctionService } from "@bitwarden/common/services/webCryptoFunction.service";
import { AutoFillActiveTabCommand } from "../commands/autoFillActiveTabCommand";
import { Account } from "../models/account";
import { StateService as AbstractStateService } from "../services/abstractions/state.service";
import AutofillService from "../services/autofill.service";
import { BrowserCryptoService } from "../services/browserCrypto.service";
import BrowserLocalStorageService from "../services/browserLocalStorage.service";
import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service";
import I18nService from "../services/i18n.service";
import { KeyGenerationService } from "../services/keyGeneration.service";
import { LocalBackedSessionStorageService } from "../services/localBackedSessionStorage.service";
import { StateService } from "../services/state.service";
export const onCommandListener = async (command: string, tab: chrome.tabs.Tab) => {
switch (command) {
case "autofill_login":
await doAutoFillLogin(tab);
break;
}
};
const doAutoFillLogin = async (tab: chrome.tabs.Tab): Promise<void> => {
const logService = new ConsoleLogService(false);
const cryptoFunctionService = new WebCryptoFunctionService(self);
const storageService = new BrowserLocalStorageService();
const secureStorageService = new BrowserLocalStorageService();
const memoryStorageService = new LocalBackedSessionStorageService(
new EncryptService(cryptoFunctionService, logService, false),
new KeyGenerationService(cryptoFunctionService)
);
const stateFactory = new StateFactory(GlobalState, Account);
const stateMigrationService = new StateMigrationService(
storageService,
secureStorageService,
stateFactory
);
const stateService: AbstractStateService = new StateService(
storageService,
secureStorageService,
memoryStorageService, // AbstractStorageService
logService,
stateMigrationService,
stateFactory
);
await stateService.init();
const platformUtils = new BrowserPlatformUtilsService(
null, // MessagingService
stateService,
null, // clipboardWriteCallback
null // biometricCallback
);
const cryptoService = new BrowserCryptoService(
cryptoFunctionService,
null, // AbstractEncryptService
platformUtils,
logService,
stateService
);
const settingsService = new SettingsService(stateService);
const i18nService = new I18nService(chrome.i18n.getUILanguage());
await i18nService.init();
// Don't love this pt.1
let searchService: SearchService = null;
const cipherService = new CipherService(
cryptoService,
settingsService,
null, // ApiService
null, // FileUploadService,
i18nService,
() => searchService, // Don't love this pt.2
logService,
stateService
);
// Don't love this pt.3
searchService = new SearchService(cipherService, logService, i18nService);
// TODO: Remove this before we encourage anyone to start using this
const eventService = new NoOpEventService();
const autofillService = new AutofillService(
cipherService,
stateService,
null, // TotpService
eventService,
logService
);
const authService = new AuthService(
cryptoService, // CryptoService
null, // ApiService
null, // TokenService
null, // AppIdService
platformUtils,
null, // MessagingService
logService,
null, // KeyConnectorService
null, // EnvironmentService
stateService,
null, // TwoFactorService
i18nService
);
const authStatus = await authService.getAuthStatus();
if (authStatus < AuthenticationStatus.Unlocked) {
// TODO: Add back in unlock on autofill
logService.info("Currently not unlocked, MV3 does not support unlock on autofill currently.");
return;
}
const command = new AutoFillActiveTabCommand(autofillService);
await command.doAutoFillActiveTabCommand(tab);
};

View File

@ -47,7 +47,8 @@
}
],
"background": {
"service_worker": "background.js"
"service_worker": "background.js",
"type": "module"
},
"action": {
"default_icon": {

View File

@ -22,4 +22,5 @@ export default class AutofillField {
selectInfo: any;
maxLength: number;
tagName: string;
[key: string]: any;
}

View File

@ -1,7 +1,7 @@
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
export default abstract class AbstractChromeStorageService implements AbstractStorageService {
protected abstract chromeStorageApi: any;
protected abstract chromeStorageApi: chrome.storage.StorageArea;
async get<T>(key: string): Promise<T> {
return new Promise((resolve) => {

View File

@ -1,7 +1,41 @@
import { CipherView } from "@bitwarden/common/models/view/cipherView";
import AutofillField from "../../models/autofillField";
import AutofillForm from "../../models/autofillForm";
import AutofillPageDetails from "../../models/autofillPageDetails";
export abstract class AutofillService {
getFormsWithPasswordFields: (pageDetails: AutofillPageDetails) => any[];
doAutoFill: (options: any) => Promise<string>;
doAutoFillActiveTab: (pageDetails: any, fromCommand: boolean) => Promise<string>;
export interface PageDetail {
frameId: number;
tab: chrome.tabs.Tab;
details: AutofillPageDetails;
}
export interface AutoFillOptions {
cipher: CipherView;
pageDetails: PageDetail[];
doc?: typeof window.document;
tab: chrome.tabs.Tab;
skipUsernameOnlyFill?: boolean;
onlyEmptyFields?: boolean;
onlyVisibleFields?: boolean;
fillNewPassword?: boolean;
skipLastUsed?: boolean;
}
export interface FormData {
form: AutofillForm;
password: AutofillField;
username: AutofillField;
passwords: AutofillField[];
}
export abstract class AutofillService {
getFormsWithPasswordFields: (pageDetails: AutofillPageDetails) => FormData[];
doAutoFill: (options: AutoFillOptions) => Promise<string>;
doAutoFillOnTab: (
pageDetails: PageDetail[],
tab: chrome.tabs.Tab,
fromCommand: boolean
) => Promise<string>;
doAutoFillActiveTab: (pageDetails: PageDetail[], fromCommand: boolean) => Promise<string>;
}

View File

@ -15,13 +15,26 @@ import AutofillPageDetails from "../models/autofillPageDetails";
import AutofillScript from "../models/autofillScript";
import { StateService } from "../services/abstractions/state.service";
import { AutofillService as AutofillServiceInterface } from "./abstractions/autofill.service";
import {
AutoFillOptions,
AutofillService as AutofillServiceInterface,
PageDetail,
FormData,
} from "./abstractions/autofill.service";
import {
AutoFillConstants,
CreditCardAutoFillConstants,
IdentityAutoFillConstants,
} from "./autofillConstants";
export interface GenerateFillScriptOptions {
skipUsernameOnlyFill: boolean;
onlyEmptyFields: boolean;
onlyVisibleFields: boolean;
fillNewPassword: boolean;
cipher: CipherView;
}
export default class AutofillService implements AutofillServiceInterface {
constructor(
private cipherService: CipherService,
@ -31,10 +44,16 @@ export default class AutofillService implements AutofillServiceInterface {
private logService: LogService
) {}
getFormsWithPasswordFields(pageDetails: AutofillPageDetails): any[] {
const formData: any[] = [];
getFormsWithPasswordFields(pageDetails: AutofillPageDetails): FormData[] {
const formData: FormData[] = [];
const passwordFields = this.loadPasswordFields(pageDetails, true, true, false, false);
const passwordFields = AutofillService.loadPasswordFields(
pageDetails,
true,
true,
false,
false
);
if (passwordFields.length === 0) {
return formData;
}
@ -64,16 +83,17 @@ export default class AutofillService implements AutofillServiceInterface {
return formData;
}
async doAutoFill(options: any) {
let totpPromise: Promise<string> = null;
async doAutoFill(options: AutoFillOptions) {
const tab = options.tab;
if (!tab || !options.cipher || !options.pageDetails || !options.pageDetails.length) {
throw new Error("Nothing to auto-fill.");
}
let totpPromise: Promise<string> = null;
const canAccessPremium = await this.stateService.getCanAccessPremium();
let didAutofill = false;
options.pageDetails.forEach((pd: any) => {
options.pageDetails.forEach((pd) => {
// make sure we're still on correct tab
if (pd.tab.id !== tab.id || pd.tab.url !== tab.url) {
return;
@ -138,12 +158,7 @@ export default class AutofillService implements AutofillServiceInterface {
}
}
async doAutoFillActiveTab(pageDetails: any, fromCommand: boolean) {
const tab = await this.getActiveTab();
if (!tab || !tab.url) {
return;
}
async doAutoFillOnTab(pageDetails: PageDetail[], tab: chrome.tabs.Tab, fromCommand: boolean) {
let cipher: CipherView;
if (fromCommand) {
cipher = await this.cipherService.getNextCipherForUrl(tab.url);
@ -186,9 +201,18 @@ export default class AutofillService implements AutofillServiceInterface {
return totpCode;
}
async doAutoFillActiveTab(pageDetails: PageDetail[], fromCommand: boolean) {
const tab = await this.getActiveTab();
if (!tab || !tab.url) {
return;
}
return await this.doAutoFillOnTab(pageDetails, tab, fromCommand);
}
// Helpers
private async getActiveTab(): Promise<any> {
private async getActiveTab(): Promise<chrome.tabs.Tab> {
const tab = await BrowserApi.getTabFromCurrentWindow();
if (!tab) {
throw new Error("No tab found.");
@ -197,7 +221,10 @@ export default class AutofillService implements AutofillServiceInterface {
return tab;
}
private generateFillScript(pageDetails: AutofillPageDetails, options: any): AutofillScript {
private generateFillScript(
pageDetails: AutofillPageDetails,
options: GenerateFillScriptOptions
): AutofillScript {
if (!pageDetails || !options.cipher) {
return null;
}
@ -209,13 +236,13 @@ export default class AutofillService implements AutofillServiceInterface {
if (fields && fields.length) {
const fieldNames: string[] = [];
fields.forEach((f: any) => {
if (this.hasValue(f.name)) {
fields.forEach((f) => {
if (AutofillService.hasValue(f.name)) {
fieldNames.push(f.name.toLowerCase());
}
});
pageDetails.fields.forEach((field: any) => {
pageDetails.fields.forEach((field) => {
// eslint-disable-next-line
if (filledFields.hasOwnProperty(field.opid)) {
return;
@ -228,10 +255,10 @@ export default class AutofillService implements AutofillServiceInterface {
const matchingIndex = this.findMatchingFieldIndex(field, fieldNames);
if (matchingIndex > -1) {
const matchingField: FieldView = fields[matchingIndex];
let val;
let val: string;
if (matchingField.type === FieldType.Linked) {
// Assumption: Linked Field is not being used to autofill a boolean value
val = options.cipher.linkedFieldValue(matchingField.linkedId);
val = options.cipher.linkedFieldValue(matchingField.linkedId) as string;
} else {
val = matchingField.value;
if (val == null && matchingField.type === FieldType.Boolean) {
@ -240,7 +267,7 @@ export default class AutofillService implements AutofillServiceInterface {
}
filledFields[field.opid] = field;
this.fillByOpid(fillScript, field, val);
AutofillService.fillByOpid(fillScript, field, val);
}
});
}
@ -269,9 +296,9 @@ export default class AutofillService implements AutofillServiceInterface {
private generateLoginFillScript(
fillScript: AutofillScript,
pageDetails: any,
pageDetails: AutofillPageDetails,
filledFields: { [id: string]: AutofillField },
options: any
options: GenerateFillScriptOptions
): AutofillScript {
if (!options.cipher.login) {
return null;
@ -285,11 +312,11 @@ export default class AutofillService implements AutofillServiceInterface {
if (!login.password || login.password === "") {
// No password for this login. Maybe they just wanted to auto-fill some custom fields?
fillScript = this.setFillScriptForFocus(filledFields, fillScript);
fillScript = AutofillService.setFillScriptForFocus(filledFields, fillScript);
return fillScript;
}
let passwordFields = this.loadPasswordFields(
let passwordFields = AutofillService.loadPasswordFields(
pageDetails,
false,
false,
@ -298,7 +325,7 @@ export default class AutofillService implements AutofillServiceInterface {
);
if (!passwordFields.length && !options.onlyVisibleFields) {
// not able to find any viewable password fields. maybe there are some "hidden" ones?
passwordFields = this.loadPasswordFields(
passwordFields = AutofillService.loadPasswordFields(
pageDetails,
true,
true,
@ -362,11 +389,11 @@ export default class AutofillService implements AutofillServiceInterface {
if (!passwordFields.length && !options.skipUsernameOnlyFill) {
// No password fields on this page. Let's try to just fuzzy fill the username.
pageDetails.fields.forEach((f: any) => {
pageDetails.fields.forEach((f) => {
if (
f.viewable &&
(f.type === "text" || f.type === "email" || f.type === "tel") &&
this.fieldIsFuzzyMatch(f, AutoFillConstants.UsernameFieldNames)
AutofillService.fieldIsFuzzyMatch(f, AutoFillConstants.UsernameFieldNames)
) {
usernames.push(f);
}
@ -380,7 +407,7 @@ export default class AutofillService implements AutofillServiceInterface {
}
filledFields[u.opid] = u;
this.fillByOpid(fillScript, u, login.username);
AutofillService.fillByOpid(fillScript, u, login.username);
});
passwords.forEach((p) => {
@ -390,18 +417,18 @@ export default class AutofillService implements AutofillServiceInterface {
}
filledFields[p.opid] = p;
this.fillByOpid(fillScript, p, login.password);
AutofillService.fillByOpid(fillScript, p, login.password);
});
fillScript = this.setFillScriptForFocus(filledFields, fillScript);
fillScript = AutofillService.setFillScriptForFocus(filledFields, fillScript);
return fillScript;
}
private generateCardFillScript(
fillScript: AutofillScript,
pageDetails: any,
pageDetails: AutofillPageDetails,
filledFields: { [id: string]: AutofillField },
options: any
options: GenerateFillScriptOptions
): AutofillScript {
if (!options.cipher.card) {
return null;
@ -409,8 +436,8 @@ export default class AutofillService implements AutofillServiceInterface {
const fillFields: { [id: string]: AutofillField } = {};
pageDetails.fields.forEach((f: any) => {
if (this.forCustomFieldsOnly(f)) {
pageDetails.fields.forEach((f) => {
if (AutofillService.forCustomFieldsOnly(f)) {
return;
}
@ -429,7 +456,7 @@ export default class AutofillService implements AutofillServiceInterface {
// ref https://developers.google.com/web/fundamentals/design-and-ux/input/forms/
if (
!fillFields.cardholderName &&
this.isFieldMatch(
AutofillService.isFieldMatch(
f[attr],
CreditCardAutoFillConstants.CardHolderFieldNames,
CreditCardAutoFillConstants.CardHolderFieldNameValues
@ -439,7 +466,7 @@ export default class AutofillService implements AutofillServiceInterface {
break;
} else if (
!fillFields.number &&
this.isFieldMatch(
AutofillService.isFieldMatch(
f[attr],
CreditCardAutoFillConstants.CardNumberFieldNames,
CreditCardAutoFillConstants.CardNumberFieldNameValues
@ -449,7 +476,7 @@ export default class AutofillService implements AutofillServiceInterface {
break;
} else if (
!fillFields.exp &&
this.isFieldMatch(
AutofillService.isFieldMatch(
f[attr],
CreditCardAutoFillConstants.CardExpiryFieldNames,
CreditCardAutoFillConstants.CardExpiryFieldNameValues
@ -459,25 +486,25 @@ export default class AutofillService implements AutofillServiceInterface {
break;
} else if (
!fillFields.expMonth &&
this.isFieldMatch(f[attr], CreditCardAutoFillConstants.ExpiryMonthFieldNames)
AutofillService.isFieldMatch(f[attr], CreditCardAutoFillConstants.ExpiryMonthFieldNames)
) {
fillFields.expMonth = f;
break;
} else if (
!fillFields.expYear &&
this.isFieldMatch(f[attr], CreditCardAutoFillConstants.ExpiryYearFieldNames)
AutofillService.isFieldMatch(f[attr], CreditCardAutoFillConstants.ExpiryYearFieldNames)
) {
fillFields.expYear = f;
break;
} else if (
!fillFields.code &&
this.isFieldMatch(f[attr], CreditCardAutoFillConstants.CVVFieldNames)
AutofillService.isFieldMatch(f[attr], CreditCardAutoFillConstants.CVVFieldNames)
) {
fillFields.code = f;
break;
} else if (
!fillFields.brand &&
this.isFieldMatch(f[attr], CreditCardAutoFillConstants.CardBrandFieldNames)
AutofillService.isFieldMatch(f[attr], CreditCardAutoFillConstants.CardBrandFieldNames)
) {
fillFields.brand = f;
break;
@ -491,7 +518,7 @@ export default class AutofillService implements AutofillServiceInterface {
this.makeScriptAction(fillScript, card, fillFields, filledFields, "code");
this.makeScriptAction(fillScript, card, fillFields, filledFields, "brand");
if (fillFields.expMonth && this.hasValue(card.expMonth)) {
if (fillFields.expMonth && AutofillService.hasValue(card.expMonth)) {
let expMonth: string = card.expMonth;
if (fillFields.expMonth.selectInfo && fillFields.expMonth.selectInfo.options) {
@ -526,10 +553,10 @@ export default class AutofillService implements AutofillServiceInterface {
}
filledFields[fillFields.expMonth.opid] = fillFields.expMonth;
this.fillByOpid(fillScript, fillFields.expMonth, expMonth);
AutofillService.fillByOpid(fillScript, fillFields.expMonth, expMonth);
}
if (fillFields.expYear && this.hasValue(card.expYear)) {
if (fillFields.expYear && AutofillService.hasValue(card.expYear)) {
let expYear: string = card.expYear;
if (fillFields.expYear.selectInfo && fillFields.expYear.selectInfo.options) {
for (let i = 0; i < fillFields.expYear.selectInfo.options.length; i++) {
@ -572,10 +599,14 @@ export default class AutofillService implements AutofillServiceInterface {
}
filledFields[fillFields.expYear.opid] = fillFields.expYear;
this.fillByOpid(fillScript, fillFields.expYear, expYear);
AutofillService.fillByOpid(fillScript, fillFields.expYear, expYear);
}
if (fillFields.exp && this.hasValue(card.expMonth) && this.hasValue(card.expYear)) {
if (
fillFields.exp &&
AutofillService.hasValue(card.expMonth) &&
AutofillService.hasValue(card.expYear)
) {
const fullMonth = ("0" + card.expMonth).slice(-2);
let fullYear: string = card.expYear;
@ -712,7 +743,7 @@ export default class AutofillService implements AutofillServiceInterface {
return fillScript;
}
private fieldAttrsContain(field: any, containsVal: string) {
private fieldAttrsContain(field: AutofillField, containsVal: string) {
if (!field) {
return false;
}
@ -734,9 +765,9 @@ export default class AutofillService implements AutofillServiceInterface {
private generateIdentityFillScript(
fillScript: AutofillScript,
pageDetails: any,
pageDetails: AutofillPageDetails,
filledFields: { [id: string]: AutofillField },
options: any
options: GenerateFillScriptOptions
): AutofillScript {
if (!options.cipher.identity) {
return null;
@ -744,8 +775,8 @@ export default class AutofillService implements AutofillServiceInterface {
const fillFields: { [id: string]: AutofillField } = {};
pageDetails.fields.forEach((f: any) => {
if (this.forCustomFieldsOnly(f)) {
pageDetails.fields.forEach((f) => {
if (AutofillService.forCustomFieldsOnly(f)) {
return;
}
@ -764,7 +795,7 @@ export default class AutofillService implements AutofillServiceInterface {
// ref https://developers.google.com/web/fundamentals/design-and-ux/input/forms/
if (
!fillFields.name &&
this.isFieldMatch(
AutofillService.isFieldMatch(
f[attr],
IdentityAutoFillConstants.FullNameFieldNames,
IdentityAutoFillConstants.FullNameFieldNameValues
@ -774,37 +805,37 @@ export default class AutofillService implements AutofillServiceInterface {
break;
} else if (
!fillFields.firstName &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.FirstnameFieldNames)
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.FirstnameFieldNames)
) {
fillFields.firstName = f;
break;
} else if (
!fillFields.middleName &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.MiddlenameFieldNames)
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.MiddlenameFieldNames)
) {
fillFields.middleName = f;
break;
} else if (
!fillFields.lastName &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.LastnameFieldNames)
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.LastnameFieldNames)
) {
fillFields.lastName = f;
break;
} else if (
!fillFields.title &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.TitleFieldNames)
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.TitleFieldNames)
) {
fillFields.title = f;
break;
} else if (
!fillFields.email &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.EmailFieldNames)
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.EmailFieldNames)
) {
fillFields.email = f;
break;
} else if (
!fillFields.address &&
this.isFieldMatch(
AutofillService.isFieldMatch(
f[attr],
IdentityAutoFillConstants.AddressFieldNames,
IdentityAutoFillConstants.AddressFieldNameValues
@ -814,61 +845,61 @@ export default class AutofillService implements AutofillServiceInterface {
break;
} else if (
!fillFields.address1 &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.Address1FieldNames)
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.Address1FieldNames)
) {
fillFields.address1 = f;
break;
} else if (
!fillFields.address2 &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.Address2FieldNames)
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.Address2FieldNames)
) {
fillFields.address2 = f;
break;
} else if (
!fillFields.address3 &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.Address3FieldNames)
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.Address3FieldNames)
) {
fillFields.address3 = f;
break;
} else if (
!fillFields.postalCode &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.PostalCodeFieldNames)
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.PostalCodeFieldNames)
) {
fillFields.postalCode = f;
break;
} else if (
!fillFields.city &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.CityFieldNames)
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.CityFieldNames)
) {
fillFields.city = f;
break;
} else if (
!fillFields.state &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.StateFieldNames)
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.StateFieldNames)
) {
fillFields.state = f;
break;
} else if (
!fillFields.country &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.CountryFieldNames)
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.CountryFieldNames)
) {
fillFields.country = f;
break;
} else if (
!fillFields.phone &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.PhoneFieldNames)
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.PhoneFieldNames)
) {
fillFields.phone = f;
break;
} else if (
!fillFields.username &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.UserNameFieldNames)
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.UserNameFieldNames)
) {
fillFields.username = f;
break;
} else if (
!fillFields.company &&
this.isFieldMatch(f[attr], IdentityAutoFillConstants.CompanyFieldNames)
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.CompanyFieldNames)
) {
fillFields.company = f;
break;
@ -923,16 +954,16 @@ export default class AutofillService implements AutofillServiceInterface {
if (fillFields.name && (identity.firstName || identity.lastName)) {
let fullName = "";
if (this.hasValue(identity.firstName)) {
if (AutofillService.hasValue(identity.firstName)) {
fullName = identity.firstName;
}
if (this.hasValue(identity.middleName)) {
if (AutofillService.hasValue(identity.middleName)) {
if (fullName !== "") {
fullName += " ";
}
fullName += identity.middleName;
}
if (this.hasValue(identity.lastName)) {
if (AutofillService.hasValue(identity.lastName)) {
if (fullName !== "") {
fullName += " ";
}
@ -942,18 +973,18 @@ export default class AutofillService implements AutofillServiceInterface {
this.makeScriptActionWithValue(fillScript, fullName, fillFields.name, filledFields);
}
if (fillFields.address && this.hasValue(identity.address1)) {
if (fillFields.address && AutofillService.hasValue(identity.address1)) {
let address = "";
if (this.hasValue(identity.address1)) {
if (AutofillService.hasValue(identity.address1)) {
address = identity.address1;
}
if (this.hasValue(identity.address2)) {
if (AutofillService.hasValue(identity.address2)) {
if (address !== "") {
address += ", ";
}
address += identity.address2;
}
if (this.hasValue(identity.address3)) {
if (AutofillService.hasValue(identity.address3)) {
if (address !== "") {
address += ", ";
}
@ -970,7 +1001,11 @@ export default class AutofillService implements AutofillServiceInterface {
return excludedTypes.indexOf(type) > -1;
}
private isFieldMatch(value: string, options: string[], containsOptions?: string[]): boolean {
private static isFieldMatch(
value: string,
options: string[],
containsOptions?: string[]
): boolean {
value = value
.trim()
.toLowerCase()
@ -1011,12 +1046,15 @@ export default class AutofillService implements AutofillServiceInterface {
filledFields: { [id: string]: AutofillField }
) {
let doFill = false;
if (this.hasValue(dataValue) && field) {
if (AutofillService.hasValue(dataValue) && field) {
if (field.type === "select-one" && field.selectInfo && field.selectInfo.options) {
for (let i = 0; i < field.selectInfo.options.length; i++) {
const option = field.selectInfo.options[i];
for (let j = 0; j < option.length; j++) {
if (this.hasValue(option[j]) && option[j].toLowerCase() === dataValue.toLowerCase()) {
if (
AutofillService.hasValue(option[j]) &&
option[j].toLowerCase() === dataValue.toLowerCase()
) {
doFill = true;
if (option.length > 1) {
dataValue = option[1];
@ -1036,11 +1074,11 @@ export default class AutofillService implements AutofillServiceInterface {
if (doFill) {
filledFields[field.opid] = field;
this.fillByOpid(fillScript, field, dataValue);
AutofillService.fillByOpid(fillScript, field, dataValue);
}
}
private loadPasswordFields(
static loadPasswordFields(
pageDetails: AutofillPageDetails,
canBeHidden: boolean,
canBeReadOnly: boolean,
@ -1049,7 +1087,7 @@ export default class AutofillService implements AutofillServiceInterface {
) {
const arr: AutofillField[] = [];
pageDetails.fields.forEach((f) => {
if (this.forCustomFieldsOnly(f)) {
if (AutofillService.forCustomFieldsOnly(f)) {
return;
}
@ -1111,7 +1149,7 @@ export default class AutofillService implements AutofillServiceInterface {
let usernameField: AutofillField = null;
for (let i = 0; i < pageDetails.fields.length; i++) {
const f = pageDetails.fields[i];
if (this.forCustomFieldsOnly(f)) {
if (AutofillService.forCustomFieldsOnly(f)) {
continue;
}
@ -1195,7 +1233,7 @@ export default class AutofillService implements AutofillServiceInterface {
private fieldPropertyIsMatch(field: any, property: string, name: string): boolean {
let fieldVal = field[property] as string;
if (!this.hasValue(fieldVal)) {
if (!AutofillService.hasValue(fieldVal)) {
return false;
}
@ -1227,33 +1265,45 @@ export default class AutofillService implements AutofillServiceInterface {
return fieldVal.toLowerCase() === name;
}
private fieldIsFuzzyMatch(field: AutofillField, names: string[]): boolean {
if (this.hasValue(field.htmlID) && this.fuzzyMatch(names, field.htmlID)) {
static fieldIsFuzzyMatch(field: AutofillField, names: string[]): boolean {
if (AutofillService.hasValue(field.htmlID) && this.fuzzyMatch(names, field.htmlID)) {
return true;
}
if (this.hasValue(field.htmlName) && this.fuzzyMatch(names, field.htmlName)) {
if (AutofillService.hasValue(field.htmlName) && this.fuzzyMatch(names, field.htmlName)) {
return true;
}
if (this.hasValue(field["label-tag"]) && this.fuzzyMatch(names, field["label-tag"])) {
if (
AutofillService.hasValue(field["label-tag"]) &&
this.fuzzyMatch(names, field["label-tag"])
) {
return true;
}
if (this.hasValue(field.placeholder) && this.fuzzyMatch(names, field.placeholder)) {
if (AutofillService.hasValue(field.placeholder) && this.fuzzyMatch(names, field.placeholder)) {
return true;
}
if (this.hasValue(field["label-left"]) && this.fuzzyMatch(names, field["label-left"])) {
if (
AutofillService.hasValue(field["label-left"]) &&
this.fuzzyMatch(names, field["label-left"])
) {
return true;
}
if (this.hasValue(field["label-top"]) && this.fuzzyMatch(names, field["label-top"])) {
if (
AutofillService.hasValue(field["label-top"]) &&
this.fuzzyMatch(names, field["label-top"])
) {
return true;
}
if (this.hasValue(field["label-aria"]) && this.fuzzyMatch(names, field["label-aria"])) {
if (
AutofillService.hasValue(field["label-aria"]) &&
this.fuzzyMatch(names, field["label-aria"])
) {
return true;
}
return false;
}
private fuzzyMatch(options: string[], value: string): boolean {
private static fuzzyMatch(options: string[], value: string): boolean {
if (options == null || options.length === 0 || value == null || value === "") {
return false;
}
@ -1272,11 +1322,11 @@ export default class AutofillService implements AutofillServiceInterface {
return false;
}
private hasValue(str: string): boolean {
static hasValue(str: string): boolean {
return str && str !== "";
}
private setFillScriptForFocus(
static setFillScriptForFocus(
filledFields: { [id: string]: AutofillField },
fillScript: AutofillScript
): AutofillScript {
@ -1304,7 +1354,7 @@ export default class AutofillService implements AutofillServiceInterface {
return fillScript;
}
private fillByOpid(fillScript: AutofillScript, field: AutofillField, value: string): void {
static fillByOpid(fillScript: AutofillScript, field: AutofillField, value: string): void {
if (field.maxLength && value && value.length > field.maxLength) {
value = value.substr(0, value.length);
}
@ -1315,7 +1365,7 @@ export default class AutofillService implements AutofillServiceInterface {
fillScript.script.push(["fill_by_opid", field.opid, value]);
}
private forCustomFieldsOnly(field: AutofillField): boolean {
static forCustomFieldsOnly(field: AutofillField): boolean {
return field.tagName === "span";
}
}

View File

@ -1,5 +1,5 @@
import AbstractChromeStorageService from "./abstractChromeStorageApi.service";
export default class BrowserLocalStorageService extends AbstractChromeStorageService {
protected chromeStorageApi: any = chrome.storage.local;
protected chromeStorageApi = chrome.storage.local;
}

View File

@ -1,5 +1,5 @@
import AbstractChromeStorageService from "./abstractChromeStorageApi.service";
export default class BrowserMemoryStorageService extends AbstractChromeStorageService {
protected chromeStorageApi: any = (chrome.storage as any).session;
protected chromeStorageApi = chrome.storage.session;
}

View File

@ -176,13 +176,6 @@ const config = {
return chunk.name === "popup/main";
},
},
commons2: {
test: /[\\/]node_modules[\\/]/,
name: "vendor",
chunks: (chunk) => {
return chunk.name === "background";
},
},
},
},
},
@ -209,4 +202,16 @@ const config = {
plugins: plugins,
};
if (manifestVersion == 2) {
// We can't use this in manifest v3
// Ideally we understand why this breaks it and we don't have to do this
config.optimization.splitChunks.cacheGroups.commons2 = {
test: /[\\/]node_modules[\\/]/,
name: "vendor",
chunks: (chunk) => {
return chunk.name === "background";
},
};
}
module.exports = config;

View File

@ -10,7 +10,7 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
templateUrl: "verify-email.component.html",
})
export class VerifyEmailComponent {
actionPromise: Promise<any>;
actionPromise: Promise<unknown>;
constructor(
private apiService: ApiService,

View File

@ -78,25 +78,4 @@ describe("ConsoleLogService", () => {
error: { 0: "this is an error message" },
});
});
it("times with output to info", async () => {
logService.time();
await new Promise((r) => setTimeout(r, 250));
const duration = logService.timeEnd();
expect(duration[0]).toBe(0);
expect(duration[1]).toBeGreaterThan(0);
expect(duration[1]).toBeLessThan(500 * 10e6);
expect(caughtMessage).toEqual(expect.arrayContaining([]));
expect(caughtMessage.log.length).toBe(1);
expect(caughtMessage.log[0]).toEqual(expect.stringMatching(/^default: \d+\.?\d*ms$/));
});
it("filters time output", async () => {
logService = new ConsoleLogService(true, () => true);
logService.time();
logService.timeEnd();
expect(caughtMessage).toEqual({});
});
});

View File

@ -6,6 +6,4 @@ export abstract class LogService {
warning: (message: string) => void;
error: (message: string) => void;
write: (level: LogLevelType, message: string) => void;
time: (label: string) => void;
timeEnd: (label: string) => [number, number];
}

View File

@ -1,17 +1,28 @@
/* eslint-disable no-useless-escape */
import * as tldjs from "tldjs";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "../abstractions/i18n.service";
const nodeURL = typeof window === "undefined" ? require("url") : null;
declare global {
/* eslint-disable-next-line no-var */
var bitwardenContainerService: BitwardenContainerService;
}
interface BitwardenContainerService {
getCryptoService: () => CryptoService;
}
export class Utils {
static inited = false;
static isNode = false;
static isBrowser = true;
static isMobileBrowser = false;
static isAppleMobileBrowser = false;
static global: any = null;
static global: typeof global = null;
static tldEndingRegex =
/.*\.(com|net|org|edu|uk|gov|ca|de|jp|fr|au|ru|ch|io|es|us|co|xyz|info|ly|mil)$/;
// Transpiled version of /\p{Emoji_Presentation}/gu using https://mothereff.in/regexpu. Used for compatability in older browsers.
@ -29,16 +40,25 @@ export class Utils {
(process as any).release != null &&
(process as any).release.name === "node";
Utils.isBrowser = typeof window !== "undefined";
Utils.isMobileBrowser = Utils.isBrowser && this.isMobile(window);
Utils.isAppleMobileBrowser = Utils.isBrowser && this.isAppleMobile(window);
Utils.global = Utils.isNode && !Utils.isBrowser ? global : window;
if (Utils.isNode) {
Utils.global = global;
} else if (Utils.isBrowser) {
Utils.global = window;
} else {
// If it's not browser or node then it must be a service worker
Utils.global = self;
}
}
static fromB64ToArray(str: string): Uint8Array {
if (Utils.isNode) {
return new Uint8Array(Buffer.from(str, "base64"));
} else {
const binaryString = window.atob(str);
const binaryString = Utils.global.atob(str);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
@ -93,7 +113,7 @@ export class Utils {
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
return Utils.global.btoa(binary);
}
}
@ -157,7 +177,7 @@ export class Utils {
if (Utils.isNode) {
return Buffer.from(utfStr, "utf8").toString("base64");
} else {
return decodeURIComponent(escape(window.btoa(utfStr)));
return decodeURIComponent(escape(Utils.global.btoa(utfStr)));
}
}
@ -169,7 +189,7 @@ export class Utils {
if (Utils.isNode) {
return Buffer.from(b64Str, "base64").toString("utf8");
} else {
return decodeURIComponent(escape(window.atob(b64Str)));
return decodeURIComponent(escape(Utils.global.atob(b64Str)));
}
}
@ -384,7 +404,7 @@ export class Utils {
return new nodeURL.URL(uriString);
} else if (typeof URL === "function") {
return new URL(uriString);
} else if (window != null) {
} else if (typeof window !== "undefined") {
const hasProtocol = uriString.indexOf("://") > -1;
if (!hasProtocol && uriString.indexOf(".") > -1) {
uriString = "http://" + uriString;

View File

@ -48,7 +48,7 @@ export class Attachment extends Domain {
if (this.key != null) {
let cryptoService: CryptoService;
const containerService = (Utils.global as any).bitwardenContainerService;
const containerService = Utils.global.bitwardenContainerService;
if (containerService) {
cryptoService = containerService.getCryptoService();
} else {

View File

@ -104,7 +104,7 @@ export class EncString implements IEncrypted {
}
let cryptoService: CryptoService;
const containerService = (Utils.global as any).bitwardenContainerService;
const containerService = Utils.global.bitwardenContainerService;
if (containerService) {
cryptoService = containerService.getCryptoService();
} else {

View File

@ -341,7 +341,7 @@ export class CipherService implements CipherServiceAbstraction {
throw new Error("No key.");
}
const promises: any[] = [];
const promises: Promise<number>[] = [];
const ciphers = await this.getAll();
ciphers.forEach(async (cipher) => {
promises.push(cipher.decrypt().then((c) => decCiphers.push(c)));

View File

@ -1,5 +1,3 @@
import * as hrtime from "browser-hrtime";
import { LogService as LogServiceAbstraction } from "../abstractions/log.service";
import { LogLevelType } from "../enums/logLevelType";
@ -56,17 +54,4 @@ export class ConsoleLogService implements LogServiceAbstraction {
break;
}
}
time(label = "default") {
if (!this.timersMap.has(label)) {
this.timersMap.set(label, hrtime());
}
}
timeEnd(label = "default"): [number, number] {
const elapsed = hrtime(this.timersMap.get(label));
this.timersMap.delete(label);
this.write(LogLevelType.Info, `${label}: ${elapsed[0] * 1000 + elapsed[1] / 10e6}ms`);
return elapsed;
}
}

View File

@ -0,0 +1,24 @@
import { EventService } from "@bitwarden/common/abstractions/event.service";
import { EventType } from "@bitwarden/common/enums/eventType";
/**
* If you want to use this, don't.
* If you think you should use that after the warning, don't.
*/
export default class NoOpEventService implements EventService {
constructor() {
if (chrome.runtime.getManifest().manifest_version !== 3) {
throw new Error("You are not allowed to use this when not in manifest_version 3");
}
}
collect(eventType: EventType, cipherId?: string, uploadImmediately?: boolean) {
return Promise.resolve();
}
uploadEvents(userId?: string) {
return Promise.resolve();
}
clearEvents(userId?: string) {
return Promise.resolve();
}
}

View File

@ -11,6 +11,8 @@ import { CipherView } from "../models/view/cipherView";
import { SendView } from "../models/view/sendView";
export class SearchService implements SearchServiceAbstraction {
private static registeredPipeline = false;
indexedEntityId?: string = null;
private indexing = false;
private index: lunr.Index = null;
@ -31,9 +33,14 @@ export class SearchService implements SearchServiceAbstraction {
}
});
// Currently have to ensure this is only done a single time. Lunr allows you to register a function
// multiple times but they will add a warning message to the console. The way they do that breaks when ran on a service worker.
if (!SearchService.registeredPipeline) {
SearchService.registeredPipeline = true;
//register lunr pipeline function
lunr.Pipeline.registerFunction(this.normalizeAccentsPipelineFunction, "normalizeAccents");
}
}
clearIndex(): void {
this.indexedEntityId = null;
@ -54,7 +61,6 @@ export class SearchService implements SearchServiceAbstraction {
return;
}
this.logService.time("search indexing");
this.indexing = true;
this.indexedEntityId = indexedEntityId;
this.index = null;
@ -95,7 +101,7 @@ export class SearchService implements SearchServiceAbstraction {
this.indexing = false;
this.logService.timeEnd("search indexing");
this.logService.info("Finished search indexing");
}
async searchCiphers(

View File

@ -610,7 +610,7 @@ export class StateService<
);
}
@withPrototypeForArrayMembers(CipherView)
@withPrototypeForArrayMembers(CipherView, CipherView.fromJSON)
async getDecryptedCiphers(options?: StorageOptions): Promise<CipherView[]> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))

View File

@ -9,7 +9,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
private crypto: Crypto;
private subtle: SubtleCrypto;
constructor(win: Window) {
constructor(win: Window | typeof global) {
this.crypto = typeof win.crypto !== "undefined" ? win.crypto : null;
this.subtle =
!!this.crypto && typeof win.crypto.subtle !== "undefined" ? win.crypto.subtle : null;

15
package-lock.json generated
View File

@ -30,7 +30,6 @@
"big-integer": "^1.6.51",
"bootstrap": "4.6.0",
"braintree-web-drop-in": "^1.33.1",
"browser-hrtime": "^1.1.8",
"bufferutil": "^4.0.6",
"chalk": "^4.1.0",
"commander": "^7.2.0",
@ -83,7 +82,7 @@
"@storybook/angular": "^6.5.7",
"@storybook/builder-webpack5": "^6.5.7",
"@storybook/manager-webpack5": "^6.5.7",
"@types/chrome": "^0.0.139",
"@types/chrome": "^0.0.190",
"@types/duo_web_sdk": "^2.7.1",
"@types/firefox-webext-browser": "^82.0.0",
"@types/inquirer": "^8.2.1",
@ -12504,9 +12503,9 @@
}
},
"node_modules/@types/chrome": {
"version": "0.0.139",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.139.tgz",
"integrity": "sha512-YZDKFlSVGFp4zldJlO+PUpxMH8N9vLke0fD6K9PA+TzXxPXu8LBLo5X2dzlOs2N/n+uMdI1lw7OPT1Emop10lQ==",
"version": "0.0.190",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.190.tgz",
"integrity": "sha512-lCwwIBfaD+PhG62qFB46mIBpD+xBIa+PedNB24KR9YnmJ0Zn9h0OwP1NQBhI8Cbu1rKwTQHTxhs7GhWGyUvinw==",
"dev": true,
"dependencies": {
"@types/filesystem": "*",
@ -52084,9 +52083,9 @@
}
},
"@types/chrome": {
"version": "0.0.139",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.139.tgz",
"integrity": "sha512-YZDKFlSVGFp4zldJlO+PUpxMH8N9vLke0fD6K9PA+TzXxPXu8LBLo5X2dzlOs2N/n+uMdI1lw7OPT1Emop10lQ==",
"version": "0.0.190",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.190.tgz",
"integrity": "sha512-lCwwIBfaD+PhG62qFB46mIBpD+xBIa+PedNB24KR9YnmJ0Zn9h0OwP1NQBhI8Cbu1rKwTQHTxhs7GhWGyUvinw==",
"dev": true,
"requires": {
"@types/filesystem": "*",

View File

@ -45,7 +45,7 @@
"@storybook/angular": "^6.5.7",
"@storybook/builder-webpack5": "^6.5.7",
"@storybook/manager-webpack5": "^6.5.7",
"@types/chrome": "^0.0.139",
"@types/chrome": "^0.0.190",
"@types/duo_web_sdk": "^2.7.1",
"@types/firefox-webext-browser": "^82.0.0",
"@types/inquirer": "^8.2.1",
@ -155,7 +155,6 @@
"big-integer": "^1.6.51",
"bootstrap": "4.6.0",
"braintree-web-drop-in": "^1.33.1",
"browser-hrtime": "^1.1.8",
"bufferutil": "^4.0.6",
"chalk": "^4.1.0",
"commander": "^7.2.0",