From afa0a4cc0ae75d0909ae9cf4e9d2db550a271764 Mon Sep 17 00:00:00 2001 From: Mokhtar Date: Wed, 6 Nov 2024 09:35:12 +0000 Subject: [PATCH 1/2] feat(#9489): collect telemetry for offline freetext searching (#9525) --- .../search/src/generate-search-requests.js | 2 +- ...egnancy-danger-sign-follow-up.wdio-spec.js | 2 +- .../forms/select_contact_telemetry.xml | 39 +++++ .../default/telemetry/telemetry.wdio-spec.js | 156 +++++++++++++++--- .../default/enketo/generic-form.wdio.page.js | 22 ++- .../contacts/contacts-content.component.ts | 60 +++---- .../reports/reports-content.component.ts | 50 ++++-- .../ts/services/search-telemetry.service.ts | 81 +++++++++ .../src/ts/services/select2-search.service.ts | 81 +++++---- .../contacts-content.component.spec.ts | 31 ++++ .../reports/reports-content.component.spec.ts | 55 +++++- .../services/search-telemetry.service.spec.ts | 144 ++++++++++++++++ 12 files changed, 620 insertions(+), 103 deletions(-) create mode 100644 tests/e2e/default/telemetry/forms/select_contact_telemetry.xml create mode 100644 webapp/src/ts/services/search-telemetry.service.ts create mode 100644 webapp/tests/karma/ts/services/search-telemetry.service.spec.ts diff --git a/shared-libs/search/src/generate-search-requests.js b/shared-libs/search/src/generate-search-requests.js index 81469003221..dcb936001e3 100644 --- a/shared-libs/search/src/generate-search-requests.js +++ b/shared-libs/search/src/generate-search-requests.js @@ -191,7 +191,7 @@ const sortByLastVisitedDate = () => { const makeCombinedParams = (freetextRequest, typeKey) => { const type = typeKey[0]; const params = {}; - if (freetextRequest.key) { + if (freetextRequest.params.key) { params.key = [ type, freetextRequest.params.key[0] ]; } else { params.startkey = [ type, freetextRequest.params.startkey[0] ]; diff --git a/tests/e2e/default/enketo/pregnancy-danger-sign-follow-up.wdio-spec.js b/tests/e2e/default/enketo/pregnancy-danger-sign-follow-up.wdio-spec.js index 31af1645301..8009c06c66e 100644 --- a/tests/e2e/default/enketo/pregnancy-danger-sign-follow-up.wdio-spec.js +++ b/tests/e2e/default/enketo/pregnancy-danger-sign-follow-up.wdio-spec.js @@ -11,7 +11,7 @@ describe('Pregnancy danger sign follow-up form', () => { const person = personFactory.build(); const fillPregnancyDangerSignFollowUpForm = async (attendToVisit, hasDangerSigns) => { - await genericForm.selectContact(person.name); + await genericForm.selectContact(person.name, 'What is the patient\'s name?'); await genericForm.nextPage(); await commonEnketoPage.selectRadioButton('Did the woman visit the health facility as recommended?', attendToVisit); await commonEnketoPage.selectRadioButton('Is she still experiencing any danger signs?', hasDangerSigns); diff --git a/tests/e2e/default/telemetry/forms/select_contact_telemetry.xml b/tests/e2e/default/telemetry/forms/select_contact_telemetry.xml new file mode 100644 index 00000000000..74872f333aa --- /dev/null +++ b/tests/e2e/default/telemetry/forms/select_contact_telemetry.xml @@ -0,0 +1,39 @@ + + + + Select contact by type and without type + + + + + Select the contact by type + + + Select the contact without type + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/e2e/default/telemetry/telemetry.wdio-spec.js b/tests/e2e/default/telemetry/telemetry.wdio-spec.js index 9e0e69f64f7..2ee5d183239 100644 --- a/tests/e2e/default/telemetry/telemetry.wdio-spec.js +++ b/tests/e2e/default/telemetry/telemetry.wdio-spec.js @@ -1,3 +1,4 @@ +const { Key } = require('webdriverio'); const commonPage = require('@page-objects/default/common/common.wdio.page'); const utils = require('@utils'); const moment = require('moment'); @@ -6,46 +7,82 @@ const placeFactory = require('@factories/cht/contacts/place'); const personFactory = require('@factories/cht/contacts/person'); const { faker: Faker } = require('@faker-js/faker'); const userFactory = require('@factories/cht/users/users'); +const searchPage = require('@page-objects/default/search/search.wdio.page'); +const contactPage = require('@page-objects/default/contacts/contacts.wdio.page'); +const reportsPage = require('@page-objects/default/reports/reports.wdio.page'); +const pregnancyFactory = require('@factories/cht/reports/pregnancy'); +const genericForm = require('@page-objects/default/enketo/generic-form.wdio.page'); +const fs = require('fs'); const { BRANCH, TAG } = process.env; -const setupUser = () => { +describe('Telemetry', () => { const places = placeFactory.generateHierarchy(); - const districtHospital = places.get('district_hospital'); + const clinic = places.get('clinic'); + const patient = personFactory.build({ + phone: '+12068881234', + parent: { _id: clinic._id, parent: clinic.parent }, + }); + + const healthCenter = places.get('health_center'); const contact = personFactory.build({ - name: Faker.name.firstName(), - parent: { _id: districtHospital._id }, + name: Faker.person.firstName(), + parent: { _id: healthCenter._id }, + phone: '+9779841299392', }); const user = userFactory.build({ username: Faker.internet.userName().toLowerCase().replace(/[^0-9a-zA-Z_]/g, ''), password: 'Secret_1', - place: districtHospital._id, + place: healthCenter._id, contact: contact._id, known: true, }); + const pregnancyReport = pregnancyFactory.build({ + fields: { + patient_id: patient.patient_id, + patient_uuid: patient._id, + name: patient.name, + }, + contact: { + _id: contact._id, + parent: contact.parent, + }, + from: contact.phone, + }); + let reportDocs; - return { - docs: [ - ...places.values(), - contact, - ], - user, - }; -}; - -describe('Telemetry', () => { const DATE_FORMAT = 'YYYY-MM-DD'; const TELEMETRY_PREFIX = 'telemetry'; - let user; - let docs; + const todayDBName = `${TELEMETRY_PREFIX}-${moment().format(DATE_FORMAT)}-${user.username}`; before(async () => { - ({ docs, user } = setupUser()); - await utils.saveDocs(docs); + const selectContactTelemetryForm = utils.deepFreeze({ + _id: 'form:select_contact_telemetry', + internalId: 'select_contact_telemetry', + title: 'Select contact by type and without type', + type: 'form', + _attachments: { + xml: { + content_type: 'application/octet-stream', + data: Buffer + .from(fs.readFileSync(`${__dirname}/forms/select_contact_telemetry.xml`, 'utf8')) + .toString('base64'), + }, + }, + }); + + await utils.saveDocIfNotExists(selectContactTelemetryForm); + await utils.saveDocs([...places.values(), contact, patient]); + reportDocs = await utils.saveDocs([pregnancyReport]); await utils.createUsers([user]); await loginPage.login(user); await commonPage.waitForPageLoaded(); }); + after(async () => { + await utils.deleteUsers([user]); + await utils.revertDb([/^form:(?!select_contact_telemetry)/], true); + }); + it('should record telemetry', async () => { const yesterday = moment().subtract(1, 'day'); const yesterdayDBName = `${TELEMETRY_PREFIX}-${yesterday.format(DATE_FORMAT)}-${user.username}`; @@ -82,4 +119,85 @@ describe('Telemetry', () => { const version = TAG || utils.escapeBranchName(BRANCH) || clientDdoc.build_info.base_version; expect(clientDdoc.build_info.version).to.include(version); }); + + describe('search matches telemetry', () => { + afterEach(async () => { + // eslint-disable-next-line no-undef + await browser.execute((dbName) => window.PouchDB(dbName).destroy(), todayDBName); + }); + + const getTelemetryEntryByKey = async (key) => { + const todayTelemetryDocs = await browser.execute(async (dbName) => { + // eslint-disable-next-line no-undef + const docs = await window.PouchDB(dbName).allDocs({ include_docs: true }); + return docs.rows.filter(row => row.doc.key.startsWith('search_match')); + }, todayDBName); + return todayTelemetryDocs.filter(row => row.doc.key === key); + }; + + it('should record telemetry for contact searches', async () => { + await commonPage.goToPeople(); + + const [firstName, lastName] = patient.name.split(' '); + const phone = patient.phone; + const patient_id = patient.patient_id; + const searchTerms = [firstName, lastName, phone, patient_id, `patient_id:${patient_id}`]; + for (const searchTerm of searchTerms) { + await searchPage.performSearch(searchTerm); + await contactPage.selectLHSRowByText(patient.name, false); + await searchPage.clearSearch(); + } + + expect(await getTelemetryEntryByKey('search_match:contacts_by_freetext:name')).to.have.lengthOf(2); + expect(await getTelemetryEntryByKey('search_match:contacts_by_freetext:phone')).to.have.lengthOf(1); + expect(await getTelemetryEntryByKey('search_match:contacts_by_freetext:patient_id')).to.have.lengthOf(2); + }); + + it('should record telemetry for reports searches', async () => { + await commonPage.goToReports(); + + const [firstName, lastName] = patient.name.split(' '); + const phone = contact.phone; + const patient_id = patient.patient_id; + const searchTerms = [firstName, lastName, phone, patient_id, `patient_id:${patient_id}`]; + for (const searchTerm of searchTerms) { + await searchPage.performSearch(searchTerm); + await reportsPage.openReport(reportDocs[0].id); + await searchPage.clearSearch(); + } + + expect(await getTelemetryEntryByKey('search_match:reports_by_freetext:fields.name')).to.have.lengthOf(2); + expect(await getTelemetryEntryByKey('search_match:reports_by_freetext:from')).to.have.lengthOf(1); + expect(await getTelemetryEntryByKey('search_match:reports_by_freetext:fields.patient_id')).to.have.lengthOf(2); + }); + + it('should record telemetry for contact searches from the select2 component', async () => { + await browser.url(`/#/contacts/${patient._id}/report/select_contact_telemetry`); + await commonPage.waitForPageLoaded(); + + const [firstName, lastName] = patient.name.split(' '); + const searchTerms = [firstName, lastName, patient.phone, `phone:${patient.phone}`]; + + for (const searchTerm of searchTerms) { + await genericForm.selectContact(patient.name, 'Select the contact by type', searchTerm); + await genericForm.clearSelectedContact('Select the contact by type'); + } + + expect(await getTelemetryEntryByKey('search_match:contacts_by_type_freetext:name')).to.have.lengthOf(2); + expect(await getTelemetryEntryByKey('search_match:contacts_by_type_freetext:phone')).to.have.lengthOf(2); + + const searchField = await $('.select2-search__field'); + if (await searchField.isDisplayed()) { + await browser.keys(Key.Escape); + } + + for (const searchTerm of searchTerms) { + await genericForm.selectContact(patient.name, 'Select the contact without type', searchTerm); + await genericForm.clearSelectedContact('Select the contact without type'); + } + + expect(await getTelemetryEntryByKey('search_match:contacts_by_freetext:name')).to.have.lengthOf(2); + expect(await getTelemetryEntryByKey('search_match:contacts_by_freetext:phone')).to.have.lengthOf(2); + }); + }); }); diff --git a/tests/page-objects/default/enketo/generic-form.wdio.page.js b/tests/page-objects/default/enketo/generic-form.wdio.page.js index 2b7ef15f2ab..7eed056b39f 100644 --- a/tests/page-objects/default/enketo/generic-form.wdio.page.js +++ b/tests/page-objects/default/enketo/generic-form.wdio.page.js @@ -11,6 +11,7 @@ const validationErrors = () => $$('.invalid-required'); const waitForValidationErrorsToDisappear = () => browser.waitUntil(async () => !(await validationErrors()).length); const waitForValidationErrors = () => browser.waitUntil(async () => (await validationErrors()).length); const fieldByName = (formId, name) => $(`#report-form [name="/${formId}/${name}"]`); +const select2Selection = (label) => $(`label*=${label}`).$('.select2-selection'); const nextPage = async (numberOfPages = 1, waitForLoad = true) => { if (waitForLoad) { @@ -29,19 +30,27 @@ const nextPage = async (numberOfPages = 1, waitForLoad = true) => { } }; -const selectContact = async (contactName) => { - const select2Selection = () => $('label*=What is the patient\'s name?').$('.select2-selection'); - await (await select2Selection()).click(); +const selectContact = async (contactName, label, searchTerm = '') => { const searchField = await $('.select2-search__field'); - await searchField.setValue(contactName); - const contact = await $('.name'); + if (!await searchField.isDisplayed()) { + await (await select2Selection(label)).click(); + } + + await searchField.setValue(searchTerm || contactName); + await $('.select2-results__option.loading-results').waitForDisplayed({ reverse: true }); + const contact = await $(`.name*=${contactName}`); await contact.waitForDisplayed(); await contact.click(); + await browser.waitUntil(async () => { - return (await (await select2Selection()).getText()).toLowerCase().endsWith(contactName.toLowerCase()); + return (await (await select2Selection(label)).getText()).toLowerCase().endsWith(contactName.toLowerCase()); }); }; +const clearSelectedContact = async (label) => { + await (await select2Selection(label)).$('.select2-selection__clear').click(); +}; + const submitForm = async ({ waitForPageLoaded = true, ignoreValidationErrors = false } = {}) => { await formTitle().click(); if (!ignoreValidationErrors) { @@ -101,6 +110,7 @@ module.exports = { nameField, fieldByName, selectContact, + clearSelectedContact, cancelForm, submitForm, currentFormView, diff --git a/webapp/src/ts/modules/contacts/contacts-content.component.ts b/webapp/src/ts/modules/contacts/contacts-content.component.ts index 0bcfe6f356d..837400657d2 100644 --- a/webapp/src/ts/modules/contacts/contacts-content.component.ts +++ b/webapp/src/ts/modules/contacts/contacts-content.component.ts @@ -16,12 +16,12 @@ import { ContactsMutedComponent } from '@mm-modals/contacts-muted/contacts-muted import { SendMessageComponent } from '@mm-modals/send-message/send-message.component'; import { ModalService } from '@mm-services/modal.service'; import { ContactTypesService } from '@mm-services/contact-types.service'; -import { UserSettingsService } from '@mm-services/user-settings.service'; import { SettingsService } from '@mm-services/settings.service'; import { SessionService } from '@mm-services/session.service'; import { MutingTransition } from '@mm-services/transitions/muting.transition'; import { ContactMutedService } from '@mm-services/contact-muted.service'; import { FastAction, FastActionButtonService } from '@mm-services/fast-action-button.service'; +import { SearchTelemetryService } from '@mm-services/search-telemetry.service'; @Component({ selector: 'contacts-content', @@ -55,21 +55,21 @@ export class ContactsContentComponent implements OnInit, OnDestroy { DISPLAY_LIMIT = 50; constructor( - private store: Store, - private route: ActivatedRoute, - private router: Router, - private changesService: ChangesService, - private contactChangeFilterService: ContactChangeFilterService, - private xmlFormsService: XmlFormsService, - private modalService: ModalService, - private contactTypesService: ContactTypesService, - private settingsService: SettingsService, - private userSettingsService: UserSettingsService, - private responsiveService: ResponsiveService, - private fastActionButtonService: FastActionButtonService, - private sessionService: SessionService, - private mutingTransition: MutingTransition, - private contactMutedService: ContactMutedService, + private readonly store: Store, + private readonly route: ActivatedRoute, + private readonly router: Router, + private readonly changesService: ChangesService, + private readonly contactChangeFilterService: ContactChangeFilterService, + private readonly xmlFormsService: XmlFormsService, + private readonly modalService: ModalService, + private readonly contactTypesService: ContactTypesService, + private readonly settingsService: SettingsService, + private readonly responsiveService: ResponsiveService, + private readonly fastActionButtonService: FastActionButtonService, + private readonly sessionService: SessionService, + private readonly mutingTransition: MutingTransition, + private readonly contactMutedService: ContactMutedService, + private readonly searchTelemetryService: SearchTelemetryService, ) { this.globalActions = new GlobalActions(store); this.contactsActions = new ContactsActions(store); @@ -112,15 +112,28 @@ export class ContactsContentComponent implements OnInit, OnDestroy { this.subscriptions.add(subscription); } + private async recordSearchTelemetry(selectedContact, nextSelectedContact, nextFilters) { + if (!nextFilters?.search || !nextSelectedContact) { + return; + } + + const hasSelectedNewContact = selectedContact === null || selectedContact._id !== nextSelectedContact._id; + if (!hasSelectedNewContact) { + return; + } + + await this.searchTelemetryService.recordContactSearch(nextSelectedContact.doc, nextFilters.search); + } + private subscribeToStore() { - const reduxSubscription = combineLatest( + const reduxSubscription = combineLatest([ this.store.select(Selectors.getSelectedContact), this.store.select(Selectors.getForms), this.store.select(Selectors.getLoadingContent), this.store.select(Selectors.getLoadingSelectedContactReports), this.store.select(Selectors.getContactsLoadingSummary), this.store.select(Selectors.getFilters), - ).subscribe(([ + ]).subscribe(([ selectedContact, forms, loadingContent, @@ -128,6 +141,7 @@ export class ContactsContentComponent implements OnInit, OnDestroy { contactsLoadingSummary, filters, ]) => { + void this.recordSearchTelemetry(this.selectedContact, selectedContact, filters); if (this.selectedContact?._id !== selectedContact?._id) { // reset view when selected contact changes this.resetTaskAndReportsFilter(); @@ -266,16 +280,6 @@ export class ContactsContentComponent implements OnInit, OnDestroy { })); } - private setUserSettings() { - if (this.userSettings) { - return; - } - return this.userSettingsService - .get() - .then(userSettings => this.userSettings = userSettings) - .catch(error => console.error('Error fetching user settings', error)); - } - private setSettings() { if (this.settings) { return; diff --git a/webapp/src/ts/modules/reports/reports-content.component.ts b/webapp/src/ts/modules/reports/reports-content.component.ts index 585732ba5eb..59abe4bdd60 100644 --- a/webapp/src/ts/modules/reports/reports-content.component.ts +++ b/webapp/src/ts/modules/reports/reports-content.component.ts @@ -16,6 +16,7 @@ import { ResponsiveService } from '@mm-services/responsive.service'; import { FastAction, FastActionButtonService } from '@mm-services/fast-action-button.service'; import { SendMessageComponent } from '@mm-modals/send-message/send-message.component'; import { DbService } from '@mm-services/db.service'; +import { SearchTelemetryService } from '@mm-services/search-telemetry.service'; @Component({ templateUrl: './reports-content.component.html' @@ -33,16 +34,17 @@ export class ReportsContentComponent implements OnInit, OnDestroy { fastActionList?: FastAction[]; constructor( - private changesService:ChangesService, - private store:Store, - private dbService: DbService, - private route:ActivatedRoute, - private router:Router, - private searchFiltersService:SearchFiltersService, - private fastActionButtonService:FastActionButtonService, - private messageStateService:MessageStateService, - private responsiveService:ResponsiveService, - private modalService:ModalService, + private readonly changesService:ChangesService, + private readonly store:Store, + private readonly dbService: DbService, + private readonly route:ActivatedRoute, + private readonly router:Router, + private readonly searchFiltersService:SearchFiltersService, + private readonly fastActionButtonService:FastActionButtonService, + private readonly messageStateService:MessageStateService, + private readonly responsiveService:ResponsiveService, + private readonly modalService:ModalService, + private readonly searchTelemetryService: SearchTelemetryService, ) { this.globalActions = new GlobalActions(store); this.reportsActions = new ReportsActions(store); @@ -68,14 +70,38 @@ export class ReportsContentComponent implements OnInit, OnDestroy { this.reportsActions.setSelectedReport(); } + private hasSelectedNewReport(selectedReport, nextSelectedReport): boolean { + const hadNoReportSelected = selectedReport === null || selectedReport.length === 0; + const hadDifferentReportSelected = Array.isArray(selectedReport) && + selectedReport.length === 1 && + selectedReport[0]._id !== nextSelectedReport._id; + + return hadNoReportSelected || hadDifferentReportSelected; + } + + private async recordSearchTelemetry(selectedReport, nextSelectedReport, nextFilters) { + if (!nextFilters?.search || !nextSelectedReport) { + return; + } + + if (!this.hasSelectedNewReport(selectedReport, nextSelectedReport)) { + return; + } + + await this.searchTelemetryService.recordReportSearch(nextSelectedReport.doc, nextFilters.search); + } + private subscribeToStore() { - const reportsSubscription = combineLatest( + const reportsSubscription = combineLatest([ this.store.select(Selectors.getSelectedReport), this.store.select(Selectors.getSelectedReports), - ).subscribe(([ + this.store.select(Selectors.getFilters), + ]).subscribe(([ selectedReport, selectedReports, + filters ]) => { + void this.recordSearchTelemetry(this.selectedReports, selectedReport, filters); if (selectedReport) { this.selectedReports = [ selectedReport ]; } else { diff --git a/webapp/src/ts/services/search-telemetry.service.ts b/webapp/src/ts/services/search-telemetry.service.ts new file mode 100644 index 00000000000..e32769843aa --- /dev/null +++ b/webapp/src/ts/services/search-telemetry.service.ts @@ -0,0 +1,81 @@ +import { Injectable } from '@angular/core'; + +import { TelemetryService } from '@mm-services/telemetry.service'; + +@Injectable({ + providedIn: 'root', +}) +export class SearchTelemetryService { + private readonly CONTACT_SKIPPED_PROPERTIES = ['_id', '_rev', 'type', 'refid', 'geolocation']; + private readonly REPORT_SKIPPED_PROPERTIES = ['_id', '_rev', 'type', 'refid', 'content']; + + constructor( + private readonly telemetryService: TelemetryService, + ) { + } + + public async recordContactSearch(contactDoc: Record, search: string) { + const matchingProperties = await this.findMatchingProperties(contactDoc, search, this.CONTACT_SKIPPED_PROPERTIES); + + await Promise.all( + matchingProperties.map(key => this.telemetryService.record(`search_match:contacts_by_freetext:${key}`)), + ); + } + + public async recordContactByTypeSearch(contactDoc: Record, search: string) { + const matchingProperties = await this.findMatchingProperties(contactDoc, search, this.CONTACT_SKIPPED_PROPERTIES); + + await Promise.all( + matchingProperties.map(key => this.telemetryService.record(`search_match:contacts_by_type_freetext:${key}`)), + ); + } + + public async recordReportSearch(reportDoc: Record, search: string) { + const [matches, fieldsMatches] = await Promise.all([ + this.findMatchingProperties(reportDoc, search, this.REPORT_SKIPPED_PROPERTIES), + this.findMatchingProperties(reportDoc.fields, search, this.REPORT_SKIPPED_PROPERTIES, 'fields'), + ]); + const matchingProperties = Array.from(new Set([...matches, ...fieldsMatches])); + + await Promise.all( + matchingProperties.map(key => this.telemetryService.record(`search_match:reports_by_freetext:${key}`)), + ); + } + + private async findMatchingProperties( + doc: Record, + search: string, + skip: string[], + basePropertyPath = '', + ) { + const matchingProperties = new Set(); + const _search = search.toLowerCase(); + Object.entries(doc).forEach(([key, value]) => { + const _key = key.toLowerCase(); + if (skip.includes(_key) || _key.endsWith('_date')) { + return; + } + + if (typeof value !== 'string') { + return; + } + + if (!value.toLowerCase().split(' ').some(word => word.startsWith(_search))) { + return; + } + + const propertyPath = basePropertyPath ? `${basePropertyPath}.${key}` : key; + matchingProperties.add(propertyPath); + }); + + if (matchingProperties.size === 0) { + // we might have a key:value search, look for matching properties with just the value + const colonSearch = search.split(':'); + if (colonSearch[1]?.length > 0) { + return this.findMatchingProperties(doc, colonSearch[1], skip, basePropertyPath); + } + } + + return Array.from(matchingProperties); + } +} diff --git a/webapp/src/ts/services/select2-search.service.ts b/webapp/src/ts/services/select2-search.service.ts index 221d43f0fab..9723b428645 100644 --- a/webapp/src/ts/services/select2-search.service.ts +++ b/webapp/src/ts/services/select2-search.service.ts @@ -10,21 +10,22 @@ import { SessionService } from '@mm-services/session.service'; import { SettingsService } from '@mm-services/settings.service'; import { ContactMutedService } from '@mm-services/contact-muted.service'; import { TranslateService } from '@mm-services/translate.service'; +import { SearchTelemetryService } from '@mm-services/search-telemetry.service'; @Injectable({ providedIn: 'root' }) export class Select2SearchService { - constructor( - private route: ActivatedRoute, - private formatProvider: FormatProvider, - private translateService: TranslateService, - private lineageModelGeneratorService: LineageModelGeneratorService, - private searchService: SearchService, - private sessionService: SessionService, - private settingsService: SettingsService, - private contactMutedService: ContactMutedService + private readonly route: ActivatedRoute, + private readonly formatProvider: FormatProvider, + private readonly translateService: TranslateService, + private readonly lineageModelGeneratorService: LineageModelGeneratorService, + private readonly searchService: SearchService, + private readonly sessionService: SessionService, + private readonly settingsService: SettingsService, + private readonly contactMutedService: ContactMutedService, + private readonly searchTelemetryService: SearchTelemetryService, ) { } private defaultTemplateResult(row) { @@ -58,8 +59,6 @@ export class Select2SearchService { } private query(params, successCb, failureCb, options, types) { - const currentQuery = params.data.q; - const searchOptions = { limit: options.pageSize, skip: this.calculateSkip(params.data.page, options.pageSize), @@ -77,20 +76,14 @@ export class Select2SearchService { this.searchService .search('contacts', filters, searchOptions) .then((documents) => { - if (currentQuery === params.data.q) { - successCb({ - results: options.sendMessageExtras(this.prepareRows(documents)), - pagination: { - more: documents.length === options.pageSize - } - }); - } + successCb({ + results: options.sendMessageExtras(this.prepareRows(documents)), + pagination: { + more: documents.length === options.pageSize + } + }); }) - .catch((err) => { - if (currentQuery === params.data.q) { - failureCb(err); - } - }); + .catch((err) => failureCb(err)); } private getDoc(id) { @@ -177,21 +170,40 @@ export class Select2SearchService { // Hydrate and re-set real doc on change // !tags -> only support single values, until there is a use-case if (!options.tags) { - selectEl.on('select2:select', (e) => { - const docId = e.params && e.params.data && e.params.data.id; - - if (docId) { - this.getDoc(docId) - .then(doc => { - this.setDoc(selectEl, doc); - selectEl.trigger('change'); - }) - .catch(err => console.error('Select2 failed to get document', err)); + let search: string | undefined; + selectEl.on('select2:selecting', () => search = selectEl.data('select2').dropdown.$search.val()); + selectEl.on('select2:select', async (event) => { + const docId = event.params?.data?.id; + + if (!docId) { + return; + } + + try { + const doc = await this.getDoc(docId); + this.setDoc(selectEl, doc); + selectEl.trigger('change'); + + void this.recordSearchTelemetry(doc, search, types); + } catch (error) { + console.error('Select2 failed to get document', error); } }); } } + private async recordSearchTelemetry(doc, search?: string, types?) { + if (!search) { + return; + } + + if (!types || types.length === 0) { + return this.searchTelemetryService.recordContactSearch(doc, search); + } + + return this.searchTelemetryService.recordContactByTypeSearch(doc, search); + } + private setDoc(selectEl, doc) { if (doc) { selectEl.select2('data')[0].doc = doc; // Set the value @@ -237,5 +249,4 @@ export class Select2SearchService { return this.resolveInitialValue(selectEl, options, settings); } - } diff --git a/webapp/tests/karma/ts/modules/contacts/contacts-content.component.spec.ts b/webapp/tests/karma/ts/modules/contacts/contacts-content.component.spec.ts index 6e1e0986453..b000e6d0623 100644 --- a/webapp/tests/karma/ts/modules/contacts/contacts-content.component.spec.ts +++ b/webapp/tests/karma/ts/modules/contacts/contacts-content.component.spec.ts @@ -29,6 +29,7 @@ import { FastActionButtonComponent } from '@mm-components/fast-action-button/fas import { AuthService } from '@mm-services/auth.service'; import { MatBottomSheet } from '@angular/material/bottom-sheet'; import { MatDialog } from '@angular/material/dialog'; +import { SearchTelemetryService } from '@mm-services/search-telemetry.service'; describe('Contacts content component', () => { let component: ContactsContentComponent; @@ -49,6 +50,7 @@ describe('Contacts content component', () => { let responsiveService; let contactMutedService; let fastActionButtonService; + let searchTelemetryService; let mutingTransition; let settings; @@ -87,6 +89,7 @@ describe('Contacts content component', () => { mutingTransition = { isUnmuteForm: sinon.stub() }; contactMutedService = { getMuted: sinon.stub() }; fastActionButtonService = { getContactRightSideActions: sinon.stub() }; + searchTelemetryService = { recordContactSearch: sinon.stub() }; selectedContact = { doc: {}, @@ -140,6 +143,7 @@ describe('Contacts content component', () => { { provide: ContactMutedService, useValue: contactMutedService }, { provide: MutingTransition, useValue: mutingTransition }, { provide: FastActionButtonService, useValue: fastActionButtonService }, + { provide: SearchTelemetryService, useValue: searchTelemetryService }, { provide: AuthService, useValue: { has: sinon.stub() } }, { provide: MatBottomSheet, useValue: { open: sinon.stub() } }, { provide: MatDialog, useValue: { open: sinon.stub() } }, @@ -241,6 +245,33 @@ describe('Contacts content component', () => { expect(!!component.summaryErrorStack).to.be.false; })); + it('should collect telemetry for the selected search results', fakeAsync(() => { + // perform search + const search = 'abc-1234'; + store.overrideSelector(Selectors.getFilters, { search }); + store.refreshState(); + expect(searchTelemetryService.recordContactSearch.callCount).to.equal(0); + + // select contact, collect telemetry + const contact = { _id: 'contact_id', doc: { case_id: 'abc-1234' } }; + store.overrideSelector(Selectors.getSelectedContact, contact); + store.refreshState(); + expect(searchTelemetryService.recordContactSearch.callCount).to.equal(1); + expect(searchTelemetryService.recordContactSearch.getCall(0).args).to.deep.equal([contact.doc, search]); + + // re-select same contact, don't re-collect telemetry + store.overrideSelector(Selectors.getSelectedContact, contact); + store.refreshState(); + expect(searchTelemetryService.recordContactSearch.callCount).to.equal(1); + + // select different contact, collect telemetry + const otherContact = { id_: 'other_contact_id', doc: { not_case_id: 'abc-1234' } }; + store.overrideSelector(Selectors.getSelectedContact, otherContact); + store.refreshState(); + expect(searchTelemetryService.recordContactSearch.callCount).to.equal(2); + expect(searchTelemetryService.recordContactSearch.getCall(1).args).to.deep.equal([otherContact.doc, search]); + })); + describe('Change feed process', () => { let change; diff --git a/webapp/tests/karma/ts/modules/reports/reports-content.component.spec.ts b/webapp/tests/karma/ts/modules/reports/reports-content.component.spec.ts index c4d57ca2ed0..31626c80cbf 100644 --- a/webapp/tests/karma/ts/modules/reports/reports-content.component.spec.ts +++ b/webapp/tests/karma/ts/modules/reports/reports-content.component.spec.ts @@ -30,6 +30,9 @@ import { FastActionButtonComponent } from '@mm-components/fast-action-button/fas import { CommonModule } from '@angular/common'; import { AuthService } from '@mm-services/auth.service'; import { SessionService } from '@mm-services/session.service'; +import { SearchTelemetryService } from '@mm-services/search-telemetry.service'; +import { SenderComponent } from '@mm-components/sender/sender.component'; +import { ReportVerifyInvalidIconComponent } from '@mm-components/status-icons/status-icons.template'; describe('Reports Content Component', () => { let component: ReportsContentComponent; @@ -47,6 +50,7 @@ describe('Reports Content Component', () => { let modalService; let authService; let sessionService; + let searchTelemetryService; beforeEach(waitForAsync(() => { const mockedSelectors = [ @@ -74,6 +78,7 @@ describe('Reports Content Component', () => { has: sinon.stub() }; sessionService = { isAdmin: sinon.stub() }; + searchTelemetryService = { recordReportSearch: sinon.stub() }; return TestBed .configureTestingModule({ @@ -88,7 +93,9 @@ describe('Reports Content Component', () => { FormIconPipe, TitlePipe, RelativeDatePipe, - FastActionButtonComponent + FastActionButtonComponent, + SenderComponent, + ReportVerifyInvalidIconComponent, ], providers: [ provideMockStore({ selectors: mockedSelectors }), @@ -105,6 +112,7 @@ describe('Reports Content Component', () => { { provide: DbService, useValue: dbService }, { provide: AuthService, useValue: authService }, { provide: SessionService, useValue: sessionService }, + { provide: SearchTelemetryService, useValue: searchTelemetryService }, { provide: MatBottomSheet, useValue: { open: sinon.stub() } }, { provide: MatDialog, useValue: { open: sinon.stub() } }, ] @@ -490,5 +498,50 @@ describe('Reports Content Component', () => { expect(component.selectMode).to.be.false; expect(searchFiltersService.freetextSearch.calledOnce).to.be.true; })); + + it('should collect telemetry for the selected search results', fakeAsync(() => { + store.overrideSelector(Selectors.getSelectMode, true); + store.refreshState(); + fixture.detectChanges(); + + // search for a report + expect(searchTelemetryService.recordReportSearch.callCount).to.equal(0); + const search = 'case_id:abc-1234'; + store.overrideSelector(Selectors.getFilters, { search }); + store.refreshState(); + fixture.detectChanges(); + expect(searchTelemetryService.recordReportSearch.callCount).to.equal(0); + + // select a report, collect telemetry + const report = { _id: 'report_id', doc: { case_id: 'abc-1234' } }; + store.overrideSelector(Selectors.getSelectedReport, report); + store.refreshState(); + fixture.detectChanges(); + expect(searchTelemetryService.recordReportSearch.callCount).to.equal(1); + expect(searchTelemetryService.recordReportSearch.getCall(0).args).to.deep.equal([report.doc, search]); + + // re-select same report, don't re-collect telemetry + store.overrideSelector(Selectors.getSelectedReport, report); + store.refreshState(); + fixture.detectChanges(); + expect(searchTelemetryService.recordReportSearch.callCount).to.equal(1); + + // select a different report, collect telemetry + const otherReport = { _id: 'other_report_id', doc: { other_case_id: 'abc-1234' } }; + store.overrideSelector(Selectors.getSelectedReport, otherReport); + store.refreshState(); + fixture.detectChanges(); + expect(searchTelemetryService.recordReportSearch.callCount).to.equal(2); + expect(searchTelemetryService.recordReportSearch.getCall(1).args).to.deep.equal([otherReport.doc, search]); + + // un-select report and select all reports, don't collect telemetry + store.overrideSelector(Selectors.getSelectedReport, undefined); + store.refreshState(); + fixture.detectChanges(); + store.overrideSelector(Selectors.getSelectedReports, [report, otherReport]); + store.refreshState(); + fixture.detectChanges(); + expect(searchTelemetryService.recordReportSearch.callCount).to.equal(2); + })); }); }); diff --git a/webapp/tests/karma/ts/services/search-telemetry.service.spec.ts b/webapp/tests/karma/ts/services/search-telemetry.service.spec.ts new file mode 100644 index 00000000000..17487c5f087 --- /dev/null +++ b/webapp/tests/karma/ts/services/search-telemetry.service.spec.ts @@ -0,0 +1,144 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { TestBed } from '@angular/core/testing'; + +import { SearchTelemetryService } from '@mm-services/search-telemetry.service'; +import { TelemetryService } from '@mm-services/telemetry.service'; + +describe('SearchTelemetryService', () => { + let service: SearchTelemetryService; + let telemetryService; + + beforeEach(() => { + telemetryService = { + record: sinon.stub(), + }; + + TestBed.configureTestingModule({ + providers: [ + { provide: TelemetryService, useValue: telemetryService }, + ], + }); + service = TestBed.inject(SearchTelemetryService); + }); + + afterEach(() => sinon.restore()); + + describe('recordContactSearch', () => { + const contact = { + _id: 'john_id', + name: 'john', + patient_id: '12345', + type: 'person', + field: 'value', + custom_field: 'johanna', + }; + + it('should emit a telemetry entry for each matching field', async () => { + const search = 'jo'; + await service.recordContactSearch(contact, search); + + expect(telemetryService.record.callCount).to.equal(2); + expect(telemetryService.record.getCall(0).args[0]).to.equal('search_match:contacts_by_freetext:name'); + expect(telemetryService.record.getCall(1).args[0]).to.equal('search_match:contacts_by_freetext:custom_field'); + }); + + it('should emit a telemetry entry for key:value searches', async () => { + const search = 'patient_id:12345'; + await service.recordContactSearch(contact, search); + + expect(telemetryService.record.callCount).to.equal(1); + expect(telemetryService.record.getCall(0).args[0]).to.equal( + 'search_match:contacts_by_freetext:patient_id', + ); + }); + + it('should not emit telemetry entries if no field matched the query (should not be happening)', async () => { + const search = 'unrelated search'; + await service.recordContactSearch(contact, search); + + expect(telemetryService.record.callCount).to.equal(0); + }); + }); + + describe('recordContactByTypeSearch', () => { + const contact = { + _id: 'john_id', + name: 'john', + patient_id: '12345', + type: 'person', + field: 'value', + custom_field: 'johanna', + }; + + it('should emit a telemetry entry for each matching field', async () => { + const search = 'jo'; + await service.recordContactByTypeSearch(contact, search); + + expect(telemetryService.record.callCount).to.equal(2); + expect(telemetryService.record.getCall(0).args[0]).to.equal('search_match:contacts_by_type_freetext:name'); + expect(telemetryService.record.getCall(1).args[0]).to.equal( + 'search_match:contacts_by_type_freetext:custom_field', + ); + }); + + it('should emit a telemetry entry for key:value searches', async () => { + const search = 'patient_id:12345'; + await service.recordContactByTypeSearch(contact, search); + + expect(telemetryService.record.callCount).to.equal(1); + expect(telemetryService.record.getCall(0).args[0]).to.equal( + 'search_match:contacts_by_type_freetext:patient_id', + ); + }); + + it('should not emit telemetry entries if no field matched the query (should not be happening)', async () => { + const search = 'unrelated search'; + await service.recordContactByTypeSearch(contact, search); + + expect(telemetryService.record.callCount).to.equal(0); + }); + }); + + describe('recordReportSearch', () => { + const report = { + _id: 'REF_REF_V1', + form: 'RR', + type: 'data_record', + from: '+123456789', + fields: { patient_id: '12345', name: 'John', custom_field: 'johanna' }, + custom_patient_name: 'Johnny', + }; + + it('should emit a telemetry entry for each matching field', async () => { + const search = 'jo'; + await service.recordReportSearch(report, search); + + expect(telemetryService.record.callCount).to.equal(3); + expect(telemetryService.record.getCall(0).args[0]).to.equal( + 'search_match:reports_by_freetext:custom_patient_name', + ); + expect(telemetryService.record.getCall(1).args[0]).to.equal('search_match:reports_by_freetext:fields.name'); + expect(telemetryService.record.getCall(2).args[0]).to.equal( + 'search_match:reports_by_freetext:fields.custom_field', + ); + }); + + it('should emit a telemetry entry for key:value searches', async () => { + const search = 'patient_id:12345'; + await service.recordReportSearch(report, search); + + expect(telemetryService.record.callCount).to.equal(1); + expect(telemetryService.record.getCall(0).args[0]).to.equal( + 'search_match:reports_by_freetext:fields.patient_id', + ); + }); + + it('should not emit telemetry entries if no field matched the query (should not be happening)', async () => { + const search = 'unrelated search'; + await service.recordReportSearch(report, search); + + expect(telemetryService.record.callCount).to.equal(0); + }); + }); +}); From 32bd6261a40488683c7749a1122463af88be8e01 Mon Sep 17 00:00:00 2001 From: Diana Barsan <35681649+dianabarsan@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:39:01 +0300 Subject: [PATCH 2/2] chore(#9614): update cht-deploy with new chart version (#9615) Updates how install gets variables. #9614 --- scripts/deploy/package-lock.json | 1965 +++++++++++++++++++++++++++++ scripts/deploy/src/certificate.js | 21 +- scripts/deploy/src/config.js | 2 +- scripts/deploy/src/install.js | 70 +- 4 files changed, 2016 insertions(+), 42 deletions(-) create mode 100644 scripts/deploy/package-lock.json diff --git a/scripts/deploy/package-lock.json b/scripts/deploy/package-lock.json new file mode 100644 index 00000000000..7e328f6108a --- /dev/null +++ b/scripts/deploy/package-lock.json @@ -0,0 +1,1965 @@ +{ + "name": "@medic/cht-deploy", + "version": "1.0.10", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@medic/cht-deploy", + "version": "1.0.10", + "license": "AGPL-3.0-only", + "dependencies": { + "js-yaml": "^4.1.0", + "node-fetch": "^3.3.2", + "semver": "^7.6.2" + }, + "bin": { + "cht-deploy": "cht-deploy", + "describe-deployment": "troubleshooting/describe-deployment", + "get-all-logs": "troubleshooting/get-all-logs", + "list-all-resources": "troubleshooting/list-all-resources", + "list-deployments": "troubleshooting/list-deployments", + "restart-deployment": "troubleshooting/restart-deployment", + "upload": "cht-deploy", + "view-logs": "troubleshooting/view-logs" + }, + "devDependencies": { + "@kubernetes/client-node": "^0.19.0", + "@sinonjs/fake-timers": "^11.2.2", + "chai": "^4.4.1", + "chai-as-promised": "^7.1.2", + "mocha": "^10.4.0", + "sinon": "^17.0.1" + }, + "engines": { + "node": ">=20.11.0", + "npm": ">=10.2.4" + } + }, + "node_modules/@kubernetes/client-node": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.19.0.tgz", + "integrity": "sha512-WTOjGuFQ8yeW3+qD6JrAYhpwpoQbe9R8cA/61WCyFrNawSTUgLstHu7EsZRYEs39er3jDn3wCEaczz+VOFlc2Q==", + "dev": true, + "dependencies": { + "@types/js-yaml": "^4.0.1", + "@types/node": "^20.1.1", + "@types/request": "^2.47.1", + "@types/ws": "^8.5.3", + "byline": "^5.0.0", + "isomorphic-ws": "^5.0.0", + "js-yaml": "^4.1.0", + "jsonpath-plus": "^7.2.0", + "request": "^2.88.0", + "rfc4648": "^1.3.0", + "stream-buffers": "^3.0.2", + "tar": "^6.1.11", + "tslib": "^2.4.1", + "ws": "^8.11.0" + }, + "optionalDependencies": { + "openid-client": "^5.3.0" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz", + "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "dev": true + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.17.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz", + "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "dev": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chai-as-promised": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.2.tgz", + "integrity": "sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==", + "dev": true, + "dependencies": { + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 6" + } + }, + "node_modules/chai/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz", + "integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "dev": true, + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "dev": true, + "optional": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/jsonpath-plus": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", + "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "dev": true, + "optional": true, + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openid-client": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.0.tgz", + "integrity": "sha512-4GCCGZt1i2kTHpwvaC/sCpTpQqDnBzDzuJcJMbH+y1Q5qI8U8RBvoSh28svarXszZHR5BAMXbJPX1PGPRE3VOA==", + "dev": true, + "optional": true, + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rfc4648": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.3.tgz", + "integrity": "sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==", + "dev": true + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stream-buffers": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.3.tgz", + "integrity": "sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/scripts/deploy/src/certificate.js b/scripts/deploy/src/certificate.js index 6d2cd8598c7..ae6cc6cde70 100644 --- a/scripts/deploy/src/certificate.js +++ b/scripts/deploy/src/certificate.js @@ -13,9 +13,9 @@ const CERT_SOURCES = { const cert = config.CERT_FILE; const key = config.KEY_FILE; -const obtainCertificateAndKey = async function(values) { +const obtainCertificateAndKey = async (values) => { console.log('Obtaining certificate...'); - const certSource = values.cert_source || ''; + const certSource = values?.cert_source || ''; const handlers = { [CERT_SOURCES.FILE]: () => handleFileSource(values), @@ -31,7 +31,7 @@ const obtainCertificateAndKey = async function(values) { await handler(); }; -const handleFileSource = function({ certificate_crt_file_path, certificate_key_file_path }) { +const handleFileSource = ({ certificate_crt_file_path, certificate_key_file_path }) => { if (!certificate_crt_file_path || !certificate_key_file_path) { throw new CertificateError('certificate_crt_file_path and certificate_key_file_path must be set for file source'); } @@ -40,7 +40,7 @@ const handleFileSource = function({ certificate_crt_file_path, certificate_key_f copyFile(certificate_key_file_path, key); }; -const handleMyIpSource = async function() { +const handleMyIpSource = async () => { const [crtData, keyData] = await Promise.all([ fetchData(`${config.CERT_API_URL}/fullchain`), fetchData(`${config.CERT_API_URL}/key`) @@ -50,7 +50,7 @@ const handleMyIpSource = async function() { writeFile(key, keyData); }; -const copyFile = function(src, dest) { +const copyFile = (src, dest) => { try { fs.copyFileSync(src, dest); } catch (error) { @@ -58,7 +58,7 @@ const copyFile = function(src, dest) { } }; -const writeFile = function(filename, data) { +const writeFile = (filename, data) => { try { fs.writeFileSync(filename, data); } catch (error) { @@ -66,11 +66,14 @@ const writeFile = function(filename, data) { } }; -const fetchData = async function(url) { +const fetchData = async (url) => { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), config.FETCH_TIMEOUT); const response = await fetch(url, { signal: controller.signal }); + if (!response.ok) { + throw response; + } clearTimeout(timeoutId); return await response.text(); } catch (error) { @@ -104,12 +107,12 @@ const createSecret = async function (namespace, values) { cleanupFiles(); }; -const cleanupFiles = function() { +const cleanupFiles = () => { deleteFile(cert); deleteFile(key); }; -const deleteFile = function(filename) { +const deleteFile = (filename) => { try { if (fs.existsSync(filename)) { fs.unlinkSync(filename); diff --git a/scripts/deploy/src/config.js b/scripts/deploy/src/config.js index e2a9c1090bb..018395076dc 100644 --- a/scripts/deploy/src/config.js +++ b/scripts/deploy/src/config.js @@ -2,7 +2,7 @@ export default { MEDIC_REPO_NAME: process.env.MEDIC_REPO_NAME || 'medic', MEDIC_REPO_URL: process.env.MEDIC_REPO_URL || 'https://docs.communityhealthtoolkit.org/helm-charts', CHT_CHART_NAME: process.env.CHT_CHART_NAME || 'medic/cht-chart-4x', - DEFAULT_CHART_VERSION: process.env.DEFAULT_CHART_VERSION || '1.0.*', + DEFAULT_CHART_VERSION: process.env.DEFAULT_CHART_VERSION || '1.*.*', IMAGE_TAG_API_URL: process.env.IMAGE_TAG_API_URL || 'https://staging.dev.medicmobile.org/_couch/builds_4', CERT_FILE: process.env.CERT_FILE || 'certificate.crt', KEY_FILE: process.env.KEY_FILE || 'private.key', diff --git a/scripts/deploy/src/install.js b/scripts/deploy/src/install.js index 082c0427e3e..0b02dd414c7 100644 --- a/scripts/deploy/src/install.js +++ b/scripts/deploy/src/install.js @@ -4,10 +4,8 @@ import fetch from 'node-fetch'; import yaml from 'js-yaml'; import path from 'path'; -const MEDIC_REPO_NAME = 'medic'; -const MEDIC_REPO_URL = 'https://docs.communityhealthtoolkit.org/helm-charts'; -const CHT_CHART_NAME = `${MEDIC_REPO_NAME}/cht-chart-4x`; -const DEFAULT_CHART_VERSION = '1.1.*'; +import config from './config.js'; +const { MEDIC_REPO_NAME, MEDIC_REPO_URL, CHT_CHART_NAME, DEFAULT_CHART_VERSION, IMAGE_TAG_API_URL } = config; import { fileURLToPath } from 'url'; import { obtainCertificateAndKey, createSecret } from './certificate.js'; @@ -16,7 +14,7 @@ import { UserRuntimeError } from './error.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const readFile = function(f) { +const readFile = (f) => { try { return yaml.load(fs.readFileSync(f, 'utf8')); } catch (err) { @@ -25,14 +23,14 @@ const readFile = function(f) { } }; -const prepare = function(f) { +const prepare = (f) => { const values = readFile(f); const environment = values.environment || ''; const scriptPath = path.join(__dirname, 'prepare.sh'); child_process.execSync(`${scriptPath} ${environment}`, { stdio: 'inherit' }); //NoSONAR }; -const loadValues = function(f) { +const loadValues = (f) => { if (!f) { console.error('No values file provided. Please specify a values file using -f '); process.exit(1); @@ -40,7 +38,7 @@ const loadValues = function(f) { return readFile(f); }; -const determineNamespace = function(values) { +const determineNamespace = (values) => { const namespace = values.namespace || ''; if (!namespace) { console.error('Namespace is not specified.'); @@ -49,43 +47,48 @@ const determineNamespace = function(values) { return namespace; }; -const getImageTag = async function(chtversion) { - const response = await fetch(`https://staging.dev.medicmobile.org/_couch/builds_4/medic:medic:${chtversion}`); +const getImageTag = async (chtVersion) => { + const response = await fetch(`${IMAGE_TAG_API_URL}/medic:medic:${chtVersion}`); const data = await response.json(); - const tag = data.tags && data.tags[0]; + const tag = data.tags?.[0]; if (!tag) { return Promise.reject(new UserRuntimeError('cht image tag not found')); } return tag.image.split(':').pop(); }; -const getChartVersion = function(values) { +const getChartVersion = (values) => { return values.cht_chart_version || DEFAULT_CHART_VERSION; }; const helmCmd = (action, positionalArgs, params) => { - const flagsArray = Object.entries(params).map(([key, value]) => { - if (value === true) { - return `--${key}`; - } - if (value) { - return `--${key} ${value}`; - } - return ''; //If value is falsy, don't include the flag - }).filter(Boolean); + const flagsArray = Object + .entries(params) + .map(([key, value]) => { + if (value === true) { + return `--${key}`; + } + if (value) { + return `--${key} ${value}`; + } + return ''; //If value is falsy, don't include the flag + }) + .filter(Boolean); const command = `helm ${action} ${positionalArgs.join(' ')} ${flagsArray.join(' ')}`; - return child_process.execSync(command, { stdio: 'inherit' }); //NoSONAR + return child_process.execSync(command, { stdio: 'inherit' }); }; -const helmInstallOrUpdate = function(valuesFile, namespace, values, imageTag) { +const helmInstallOrUpdate = (valuesFile, namespace, values, imageTag) => { const chartVersion = getChartVersion(values); ensureMedicHelmRepo(); const projectName = values.project_name || ''; const namespaceExists = checkNamespaceExists(namespace); try { - const releaseExists = child_process.execSync(`helm list -n ${namespace}`).toString() //NoSONAR + const releaseExists = child_process + .execSync(`helm list -n ${namespace}`) + .toString() .includes(projectName); const commonOpts = { @@ -121,29 +124,32 @@ const helmInstallOrUpdate = function(valuesFile, namespace, values, imageTag) { } }; -const checkNamespaceExists = function(namespace) { +const checkNamespaceExists = (namespace) => { try { - const result = child_process.execSync(`kubectl get namespace ${namespace}`).toString(); //NoSONAR - return result.includes(namespace); //NoSONAR + const result = child_process.execSync(`kubectl get namespace ${namespace}`).toString(); + return result.includes(namespace); } catch (err) { return false; } }; -const ensureMedicHelmRepo = function() { +const ensureMedicHelmRepo = () => { try { const repoList = child_process.execSync(`helm repo list -o json`).toString(); const repos = JSON.parse(repoList); const medicRepo = repos.find(repo => repo.name === MEDIC_REPO_NAME); + if (!medicRepo) { console.log(`Helm repo ${MEDIC_REPO_NAME} not found, adding..`); - child_process.execSync(`helm repo add ${MEDIC_REPO_NAME} ${MEDIC_REPO_URL}`, { stdio: 'inherit' }); //NoSONAR + child_process.execSync(`helm repo add ${MEDIC_REPO_NAME} ${MEDIC_REPO_URL}`, { stdio: 'inherit' }); return; - } else if (medicRepo.url.replace(/\/$/, '') !== MEDIC_REPO_URL) { + } + + if (medicRepo.url.replace(/\/$/, '') !== MEDIC_REPO_URL) { throw new UserRuntimeError(`Medic repo found but url not matching '${MEDIC_REPO_URL}', see: helm repo list`); } // Get the latest - child_process.execSync(`helm repo update ${MEDIC_REPO_NAME}`, { stdio: 'inherit' }); //NoSONAR + child_process.execSync(`helm repo update ${MEDIC_REPO_NAME}`, { stdio: 'inherit' }); } catch (err) { console.error(err.message); if (err.stack) { @@ -153,7 +159,7 @@ const ensureMedicHelmRepo = function() { } }; -const install = async function(f) { +const install = async (f) => { prepare(f); const values = loadValues(f); const namespace = determineNamespace(values);