From 3ce211eb429905522ddba43c4bcdb581e2f2ac2c Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Tue, 12 Nov 2024 09:28:29 -0500 Subject: [PATCH 01/23] Reset selected email session value on email deletion (#11492) changelog: Upcoming Features, Partner Email Selection, Reset selected email session value on email deletion --- app/controllers/users/emails_controller.rb | 1 + .../users/emails_controller_spec.rb | 91 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/app/controllers/users/emails_controller.rb b/app/controllers/users/emails_controller.rb index 0f8681fa729..09e24410d3e 100644 --- a/app/controllers/users/emails_controller.rb +++ b/app/controllers/users/emails_controller.rb @@ -99,6 +99,7 @@ def email_address def handle_successful_delete send_delete_email_notification + user_session.delete(:selected_email_id_for_linked_identity) flash[:success] = t('email_addresses.delete.success') create_user_event(:email_deleted) end diff --git a/spec/controllers/users/emails_controller_spec.rb b/spec/controllers/users/emails_controller_spec.rb index 30b667bfc62..bfc142718af 100644 --- a/spec/controllers/users/emails_controller_spec.rb +++ b/spec/controllers/users/emails_controller_spec.rb @@ -120,4 +120,95 @@ end end end + + describe '#delete' do + subject(:response) { delete :delete, params: params } + let(:user) { create(:user, :fully_registered, :with_multiple_emails) } + let(:params) { { id: user.email_addresses.take.id } } + + before do + stub_sign_in(user) + end + + it 'redirects to account page' do + expect(response).to redirect_to(account_url) + end + + context 'with invalid submisson' do + let(:user) { create(:user, :fully_registered) } + + it 'logs analytics' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + 'Email Deletion Requested', + success: false, + errors: {}, + ) + end + + it 'flashes error' do + response + + expect(flash[:error]).to eq(t('email_addresses.delete.failure')) + end + end + + context 'with valid submission' do + it 'logs analytics' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + 'Email Deletion Requested', + success: true, + errors: {}, + ) + end + + it 'notifies all confirmed email addresses, including the deleted' do + email_addresses = user.confirmed_email_addresses.to_a + + response + + expect_delivered_email_count(email_addresses.count) + email_addresses.each do |email_address| + expect_delivered_email( + to: [email_address.email], + subject: t('user_mailer.email_deleted.subject'), + ) + end + end + + it 'flashes success' do + response + + expect(flash[:success]).to eq(t('email_addresses.delete.success')) + end + + it 'tracks user event' do + expect { response }.to change { user.events.count }.by(1) + expect(user.events.last.event_type).to eq('email_deleted') + end + + it 'deletes the email address' do + expect { response }.to change { user.email_addresses.count }.by(-1) + end + + context 'with selected email for linked identity in session' do + before do + controller.user_session[:selected_email_id_for_linked_identity] = params[:id] + end + + it 'resets session value' do + response + + expect(controller.user_session[:selected_email_id_for_linked_identity]).to be_nil + end + end + end + end end From 5b17ca13e1431f413e6ce5621de9a00fd0c200fd Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:04:33 -0500 Subject: [PATCH 02/23] LG-14888: Update Spanish reCAPTCHA strings link text (#11493) changelog: User-Facing Improvements, Localization, Improve Spanish translation for reCAPTCHA disclosure --- config/locales/es.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/locales/es.yml b/config/locales/es.yml index ac4c70204ad..fc28bbaf531 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1448,7 +1448,7 @@ notices.privacy.security_and_privacy_practices: Prácticas de seguridad y declar notices.resend_confirmation_email.success: Enviamos otro correo electrónico de confirmación. notices.session_cleared: Por su seguridad, borramos lo que usted ingresó si usted no pasa a una página nueva en %{minutes} minutos. notices.session_timedout: Cerramos su sesión. Por su seguridad, %{app_name} cierra su sesión cuando usted no pasa a una página nueva en %{minutes} minutos. -notices.sign_in.recaptcha.disclosure_statement_html: Este sitio está protegido por reCAPTCHA y se aplican %{google_policy_link_html} y %{google_tos_link_html} de Google. +notices.sign_in.recaptcha.disclosure_statement_html: Este sitio está protegido por reCAPTCHA y se aplican la %{google_policy_link_html} y las %{google_tos_link_html} de Google. notices.signed_up_and_confirmed.first_paragraph_end: con un vínculo para confirmar su dirección de correo electrónico. Siga el vínculo para continuar agregando este correo electrónico a su cuenta. notices.signed_up_and_confirmed.first_paragraph_start: Enviamos un correo electrónico a notices.signed_up_and_confirmed.no_email_sent_explanation_start: '¿No recibió un correo electrónico?' @@ -1755,9 +1755,9 @@ two_factor_authentication.piv_cac.nickname: Apodo two_factor_authentication.piv_cac.renamed: Se ha cambiado correctamente el nombre de su método PIV/CAC two_factor_authentication.please_try_again_html: Inténtelo de nuevo en %{countdown}. two_factor_authentication.read_about_two_factor_authentication: Lea sobre la autenticación de dos factores -two_factor_authentication.recaptcha.disclosure_statement_html: Este sitio está protegido por reCAPTCHA y se aplican %{google_policy_link_html} y %{google_tos_link_html} de Google. Lea %{login_tos_link_html} de %{app_name}. -two_factor_authentication.recaptcha.google_policy_link: la Política de privacidad -two_factor_authentication.recaptcha.google_tos_link: las Condiciones de servicio +two_factor_authentication.recaptcha.disclosure_statement_html: Este sitio está protegido por reCAPTCHA y se aplican la %{google_policy_link_html} y las %{google_tos_link_html} de Google. Lea %{login_tos_link_html} de %{app_name}. +two_factor_authentication.recaptcha.google_policy_link: Política de privacidad +two_factor_authentication.recaptcha.google_tos_link: Condiciones de servicio two_factor_authentication.recaptcha.login_tos_link: Condiciones de uso del servicio móvil two_factor_authentication.recommended: Recomendado two_factor_authentication.totp_header_text: Ingrese su código de la aplicación de autenticación From 47594d22a061c28886997412103c673363072755 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:40:07 -0500 Subject: [PATCH 03/23] Log A/B test buckets for Face/Touch recommend visited (#11496) * Log A/B test buckets for Face/Touch recommend visited changelog: Internal, A/B Tests, Log A/B test buckets for Face/Touch recommend visited * Include submitted --- config/initializers/ab_tests.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index 969842c2a51..952a7071214 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -91,9 +91,9 @@ def self.all RECOMMEND_WEBAUTHN_PLATFORM_FOR_SMS_USER = AbTest.new( experiment_name: 'Recommend Face or Touch Unlock for SMS users', should_log: [ - 'Multi-Factor Authentication', - 'User Registration: MFA Setup Complete', - 'User Registration: 2FA Setup', + :webauthn_platform_recommended_visited, + :webauthn_platform_recommended_submitted, + :webauthn_setup_submitted, ].to_set, buckets: { recommend_for_account_creation: From d6ff721171ad8ae24ac8e8e76944b3b5026a3f8b Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:58:33 -0500 Subject: [PATCH 04/23] LG-14754: Avoid focus loss on submit button when submitting form (#11482) * Fix aria-disabled button override styling for active buttons * Update JSDOM to latest version For ARIA reflection: https://github.com/jsdom/jsdom/pull/3655 * SpinnerButton: Disable on submission via aria-disabled changelog: Bug Fixes, Accessibility, Avoid focus loss on submit button when submitting form * SubmitButton: Disable on submission via aria-disabled * Deduplicate yarn.lock * Implement WebAuthn verification by binding submit event Rely on SubmitButtonElement to control whether submit event is emitted based on whether it's already being submitted --- app/assets/stylesheets/components/_btn.scss | 2 +- .../spinner-button-element.spec.ts | 56 +++- .../spinner-button/spinner-button-element.ts | 24 +- .../submit-button-element.spec.ts | 22 +- .../submit-button/submit-button-element.ts | 19 +- .../webauthn-verify-button-element.spec.ts | 15 +- .../webauthn-verify-button-element.ts | 23 +- package.json | 2 +- yarn.lock | 244 ++++++++---------- 9 files changed, 224 insertions(+), 183 deletions(-) diff --git a/app/assets/stylesheets/components/_btn.scss b/app/assets/stylesheets/components/_btn.scss index 452ac960179..6500169b809 100644 --- a/app/assets/stylesheets/components/_btn.scss +++ b/app/assets/stylesheets/components/_btn.scss @@ -8,7 +8,7 @@ } .usa-button:disabled.usa-button--active, -[aria-disabled='true'].usa-button--active { +.usa-button[aria-disabled='true'].usa-button--active { &:not( .usa-button--unstyled, .usa-button--secondary, diff --git a/app/javascript/packages/spinner-button/spinner-button-element.spec.ts b/app/javascript/packages/spinner-button/spinner-button-element.spec.ts index 816b81d69f1..8a45fad0911 100644 --- a/app/javascript/packages/spinner-button/spinner-button-element.spec.ts +++ b/app/javascript/packages/spinner-button/spinner-button-element.spec.ts @@ -75,18 +75,32 @@ describe('SpinnerButtonElement', () => { context('inside form', () => { it('disables button without preventing form handlers', async () => { const wrapper = createWrapper({ inForm: true }); - let didSubmit = false; - wrapper.form!.addEventListener('submit', (event) => { - didSubmit = true; - event.preventDefault(); - }); + const onSubmit = sandbox.stub().callsFake((event: SubmitEvent) => event.preventDefault()); + wrapper.form!.addEventListener('submit', onSubmit); const button = screen.getByRole('button', { name: 'Click Me' }); await userEvent.type(button, '{Enter}'); clock.tick(0); - expect(didSubmit).to.be.true(); - expect(button.hasAttribute('disabled')).to.be.true(); + expect(onSubmit).to.have.been.calledOnce(); + expect(button.ariaDisabled).to.equal('true'); + }); + + it('prevents duplicate submission', async () => { + const wrapper = createWrapper({ inForm: true }); + const onSubmit = sandbox.stub().callsFake((event: SubmitEvent) => event.preventDefault()); + wrapper.form!.addEventListener('submit', onSubmit); + const button = screen.getByRole('button', { name: 'Click Me' }); + + await userEvent.type(button, '{Enter}'); + clock.tick(0); + + expect(onSubmit).to.have.been.calledOnce(); + + await userEvent.type(button, '{Enter}'); + clock.tick(0); + + expect(onSubmit).to.have.been.calledOnce(); }); it('unbinds events when disconnected', () => { @@ -104,18 +118,32 @@ describe('SpinnerButtonElement', () => { context('with form inside (button_to)', () => { it('disables button without preventing form handlers', async () => { const wrapper = createWrapper({ isButtonTo: true }); - let didSubmit = false; - wrapper.form!.addEventListener('submit', (event) => { - didSubmit = true; - event.preventDefault(); - }); + const onSubmit = sandbox.stub().callsFake((event: SubmitEvent) => event.preventDefault()); + wrapper.form!.addEventListener('submit', onSubmit); const button = screen.getByRole('button', { name: 'Click Me' }); await userEvent.type(button, '{Enter}'); clock.tick(0); - expect(didSubmit).to.be.true(); - expect(button.hasAttribute('disabled')).to.be.true(); + expect(onSubmit).to.have.been.calledOnce(); + expect(button.ariaDisabled).to.equal('true'); + }); + + it('prevents duplicate submission', async () => { + const wrapper = createWrapper({ isButtonTo: true }); + const onSubmit = sandbox.stub().callsFake((event: SubmitEvent) => event.preventDefault()); + wrapper.form!.addEventListener('submit', onSubmit); + const button = screen.getByRole('button', { name: 'Click Me' }); + + await userEvent.type(button, '{Enter}'); + clock.tick(0); + + expect(onSubmit.callCount).equal(1); + + await userEvent.type(button, '{Enter}'); + clock.tick(0); + + expect(onSubmit.callCount).equal(1); }); }); diff --git a/app/javascript/packages/spinner-button/spinner-button-element.ts b/app/javascript/packages/spinner-button/spinner-button-element.ts index b91b052e938..3b9c06486be 100644 --- a/app/javascript/packages/spinner-button/spinner-button-element.ts +++ b/app/javascript/packages/spinner-button/spinner-button-element.ts @@ -13,6 +13,7 @@ export class SpinnerButtonElement extends HTMLElement { connectedCallback() { this.form = this.querySelector('form') || this.closest('form'); + this.button.addEventListener('click', this.#preventDefaultIfSpinning); this.addEventListener('spinner.start', () => this.toggleSpinner(true)); this.addEventListener('spinner.stop', () => this.toggleSpinner(false)); @@ -40,6 +41,10 @@ export class SpinnerButtonElement extends HTMLElement { return this.querySelector('.usa-button')!; } + get isSpinning(): boolean { + return this.classList.contains('spinner-button--spinner-active'); + } + get actionMessage(): HTMLElement { return this.querySelector('.spinner-button__action-message')!; } @@ -62,9 +67,9 @@ export class SpinnerButtonElement extends HTMLElement { this.button.classList.toggle('usa-button--active', isVisible); if (isVisible) { - this.button.setAttribute('disabled', ''); + this.button.setAttribute('aria-disabled', 'true'); } else { - this.button.removeAttribute('disabled'); + this.button.removeAttribute('aria-disabled'); } if (this.actionMessage) { @@ -73,16 +78,19 @@ export class SpinnerButtonElement extends HTMLElement { window.clearTimeout(this.#longWaitTimeout); if (isVisible && Number.isFinite(this.longWaitDurationMs)) { - this.#longWaitTimeout = window.setTimeout( - () => this.handleLongWait(), - this.longWaitDurationMs, - ); + this.#longWaitTimeout = window.setTimeout(this.#handleLongWait, this.longWaitDurationMs); } } - handleLongWait() { + #handleLongWait = () => { this.actionMessage?.classList.remove('usa-sr-only'); - } + }; + + #preventDefaultIfSpinning = (event: MouseEvent) => { + if (this.isSpinning) { + event.preventDefault(); + } + }; } declare global { diff --git a/app/javascript/packages/submit-button/submit-button-element.spec.ts b/app/javascript/packages/submit-button/submit-button-element.spec.ts index 54e3dc11f4c..49416c704cb 100644 --- a/app/javascript/packages/submit-button/submit-button-element.spec.ts +++ b/app/javascript/packages/submit-button/submit-button-element.spec.ts @@ -1,3 +1,4 @@ +import { mock } from 'node:test'; import { screen } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; import './submit-button-element'; @@ -24,10 +25,29 @@ describe('SubmitButtonElement', () => { await userEvent.click(button); - expect(button.disabled).to.be.true(); + expect(button.ariaDisabled).to.equal('true'); expect(button.classList.contains('usa-button--active')).to.be.true(); }); + it('prevents duplicate submissions', async () => { + document.body.innerHTML = ` +
+ + + +
`; + + const button = screen.getByRole('button') as HTMLButtonElement; + const form = button.closest('form') as HTMLFormElement; + const onSubmit = mock.fn((event: SubmitEvent) => event.preventDefault()); + form.addEventListener('submit', onSubmit); + + await userEvent.click(button); + expect(onSubmit.mock.callCount()).to.equal(1); + await userEvent.click(button); + expect(onSubmit.mock.callCount()).to.equal(1); + }); + it('does not activate if form validation prevents submission', async () => { document.body.innerHTML = `
diff --git a/app/javascript/packages/submit-button/submit-button-element.ts b/app/javascript/packages/submit-button/submit-button-element.ts index 0a685687267..badac160eb1 100644 --- a/app/javascript/packages/submit-button/submit-button-element.ts +++ b/app/javascript/packages/submit-button/submit-button-element.ts @@ -1,6 +1,7 @@ class SubmitButtonElement extends HTMLElement { connectedCallback() { - this.form?.addEventListener('submit', () => this.activate()); + this.button.addEventListener('click', this.#preventDefaultIfSubmitting); + this.form?.addEventListener('submit', this.#activate); } get form(): HTMLFormElement | null { @@ -11,10 +12,20 @@ class SubmitButtonElement extends HTMLElement { return this.querySelector('button')!; } - activate() { - this.button.classList.add('usa-button--active'); - this.button.disabled = true; + get isSubmitting(): boolean { + return this.button.getAttribute('aria-disabled') === 'true'; } + + #activate = () => { + this.button.classList.add('usa-button--active'); + this.button.setAttribute('aria-disabled', 'true'); + }; + + #preventDefaultIfSubmitting = (event: MouseEvent) => { + if (this.isSubmitting) { + event.preventDefault(); + } + }; } declare global { diff --git a/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts b/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts index 5101427babd..04a4e83d6e0 100644 --- a/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts @@ -50,15 +50,16 @@ describe('WebauthnVerifyButtonElement', () => { Object.assign(element.dataset, { credentials: '[]', userChallenge: '[]' }, data); const form = document.querySelector('form')!; sinon.stub(form, 'submit'); - return { form, element }; + return element; } - it('assigns button type to avoid default form submission', () => { + it('prevents default form submission', async () => { createElement(); + const button = screen.getByRole('button'); + await userEvent.click(button); - const button = screen.getByRole('button') as HTMLButtonElement; - - expect(button.type).to.equal('button'); + // This test relies on the fact that JSDOM will throw an error about not implementing form + // submission if the form submission was left unhandled. }); it('shows spinner on click', async () => { @@ -94,6 +95,10 @@ describe('WebauthnVerifyButtonElement', () => { await userEvent.click(button); expect(verifyWebauthnDevice).to.have.been.calledOnce(); + + // This test also implicitly verifies that the form would not submit on a second button click, + // since JSDOM will throw an error about not implementing form submission if the form submission + // was left unhandled. }); it('submits with error name as input on thrown expected error', async () => { diff --git a/app/javascript/packages/webauthn/webauthn-verify-button-element.ts b/app/javascript/packages/webauthn/webauthn-verify-button-element.ts index d937c879f49..bafbdb6bd69 100644 --- a/app/javascript/packages/webauthn/webauthn-verify-button-element.ts +++ b/app/javascript/packages/webauthn/webauthn-verify-button-element.ts @@ -14,8 +14,11 @@ class WebauthnVerifyButtonElement extends HTMLElement { dataset: WebauthnVerifyButtonDataset; connectedCallback() { - this.setButtonAttributes(); - this.bindEvents(); + this.form.addEventListener('submit', this.#handleSubmit, { once: true }); + } + + get form(): HTMLFormElement { + return this.closest('form')!; } get button(): HTMLButtonElement { @@ -38,17 +41,8 @@ class WebauthnVerifyButtonElement extends HTMLElement { return this.dataset.userChallenge; } - setButtonAttributes() { - this.button.type = 'button'; - } - - bindEvents() { - this.button.addEventListener('click', () => this.verify()); - } - async verify() { this.spinner.hidden = false; - this.submitButton.activate(); const { userChallenge, credentials } = this; @@ -70,13 +64,18 @@ class WebauthnVerifyButtonElement extends HTMLElement { this.setInputValue('webauthn_error', error.name); } - this.closest('form')?.submit(); + this.form.submit(); } setInputValue(name: string, value: string) { const input = this.querySelector(`[name="${name}"]`)!; input.value = value; } + + #handleSubmit = (event: SubmitEvent) => { + event.preventDefault(); + this.verify(); + }; } declare global { diff --git a/package.json b/package.json index d712d452ada..92e904c95af 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "eslint-plugin-react": "^7.31.8", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-testing-library": "^6.2.0", - "jsdom": "^22.1.0", + "jsdom": "^25.0.1", "mocha": "^10.0.0", "mq-polyfill": "^1.1.8", "msw": "^2.2.1", diff --git a/yarn.lock b/yarn.lock index dbd737fdad5..ada9a8ae35a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1398,11 +1398,6 @@ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.4.3.tgz#af975e367743fa91989cd666666aec31a8f50591" integrity sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q== -"@tootallnate/once@2": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" - integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== - "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" @@ -2048,12 +2043,12 @@ acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== +agent-base@^7.0.2, agent-base@^7.1.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== dependencies: - debug "4" + debug "^4.3.4" ajv-formats@^2.1.1: version "2.1.1" @@ -2821,12 +2816,12 @@ csso@^5.0.5: dependencies: css-tree "~2.2.0" -cssstyle@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-3.0.0.tgz#17ca9c87d26eac764bb8cfd00583cff21ce0277a" - integrity sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg== +cssstyle@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.1.0.tgz#161faee382af1bafadb6d3867a92a19bcb4aea70" + integrity sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA== dependencies: - rrweb-cssom "^0.6.0" + rrweb-cssom "^0.7.1" csstype@^3.0.2: version "3.0.2" @@ -2838,14 +2833,13 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== -data-urls@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-4.0.0.tgz#333a454eca6f9a5b7b0f1013ff89074c3f522dd4" - integrity sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g== +data-urls@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde" + integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg== dependencies: - abab "^2.0.6" - whatwg-mimetype "^3.0.0" - whatwg-url "^12.0.0" + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" date-fns@^2.30.0: version "2.30.0" @@ -3061,13 +3055,6 @@ domelementtype@^2.3.0: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== -domexception@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" - integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== - dependencies: - webidl-conversions "^7.0.0" - domhandler@^5.0.2, domhandler@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" @@ -4041,12 +4028,12 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" -html-encoding-sniffer@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" - integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== +html-encoding-sniffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448" + integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ== dependencies: - whatwg-encoding "^2.0.0" + whatwg-encoding "^3.1.1" html-entities@^2.4.0: version "2.5.2" @@ -4089,14 +4076,13 @@ http-parser-js@>=0.5.1: resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== -http-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" - integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== +http-proxy-agent@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== dependencies: - "@tootallnate/once" "2" - agent-base "6" - debug "4" + agent-base "^7.1.0" + debug "^4.3.4" http-proxy-middleware@^2.0.3: version "2.0.6" @@ -4118,12 +4104,12 @@ http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" -https-proxy-agent@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== +https-proxy-agent@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== dependencies: - agent-base "6" + agent-base "^7.0.2" debug "4" human-signals@^2.1.0: @@ -4512,34 +4498,32 @@ js-yaml@4.1.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsdom@^22.1.0: - version "22.1.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-22.1.0.tgz#0fca6d1a37fbeb7f4aac93d1090d782c56b611c8" - integrity sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw== +jsdom@^25.0.1: + version "25.0.1" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-25.0.1.tgz#536ec685c288fc8a5773a65f82d8b44badcc73ef" + integrity sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw== dependencies: - abab "^2.0.6" - cssstyle "^3.0.0" - data-urls "^4.0.0" + cssstyle "^4.1.0" + data-urls "^5.0.0" decimal.js "^10.4.3" - domexception "^4.0.0" form-data "^4.0.0" - html-encoding-sniffer "^3.0.0" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.1" + html-encoding-sniffer "^4.0.0" + http-proxy-agent "^7.0.2" + https-proxy-agent "^7.0.5" is-potential-custom-element-name "^1.0.1" - nwsapi "^2.2.4" + nwsapi "^2.2.12" parse5 "^7.1.2" - rrweb-cssom "^0.6.0" + rrweb-cssom "^0.7.1" saxes "^6.0.0" symbol-tree "^3.2.4" - tough-cookie "^4.1.2" - w3c-xmlserializer "^4.0.0" + tough-cookie "^5.0.0" + w3c-xmlserializer "^5.0.0" webidl-conversions "^7.0.0" - whatwg-encoding "^2.0.0" - whatwg-mimetype "^3.0.0" - whatwg-url "^12.0.1" - ws "^8.13.0" - xml-name-validator "^4.0.0" + whatwg-encoding "^3.1.1" + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + ws "^8.18.0" + xml-name-validator "^5.0.0" jsesc@^2.5.1: version "2.5.2" @@ -5133,10 +5117,10 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" -nwsapi@^2.2.4: - version "2.2.10" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.10.tgz#0b77a68e21a0b483db70b11fad055906e867cda8" - integrity sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ== +nwsapi@^2.2.12: + version "2.2.13" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.13.tgz#e56b4e98960e7a040e5474536587e599c4ff4655" + integrity sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ== object-assign@4.1.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" @@ -5557,12 +5541,7 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" -psl@^1.1.33: - version "1.9.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" - integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== - -punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.0: +punycode@^2.1.0, punycode@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== @@ -5574,11 +5553,6 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" -querystringify@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" - integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -5850,10 +5824,10 @@ rimraf@^5.0.5: dependencies: glob "^10.3.7" -rrweb-cssom@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1" - integrity sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw== +rrweb-cssom@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz#c73451a484b86dd7cfb1e0b2898df4b703183e4b" + integrity sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg== run-applescript@^5.0.0: version "5.0.0" @@ -6676,6 +6650,18 @@ titleize@^3.0.0: resolved "https://registry.yarnpkg.com/titleize/-/titleize-3.0.0.tgz#71c12eb7fdd2558aa8a44b0be83b8a76694acd53" integrity sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ== +tldts-core@^6.1.59: + version "6.1.59" + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.59.tgz#95d1076ed9ea36f81493be515ad9d3e916440126" + integrity sha512-EiYgNf275AQyVORl8HQYYe7rTVnmLb4hkWK7wAk/12Ksy5EiHpmUmTICa4GojookBPC8qkLMBKKwCmzNA47ZPQ== + +tldts@^6.1.32: + version "6.1.59" + resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.59.tgz#aa903f542a69429bcdf4bcd63f4f1fb4cf689312" + integrity sha512-472ilPxsRuqBBpn+KuRBHJvZhk6tTo4yTVsmODrLBNLwRYJPkDfMEHivgNwp5iEl+cbrZzzRtLKRxZs7+QKkRg== + dependencies: + tldts-core "^6.1.59" + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -6693,22 +6679,19 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== -tough-cookie@^4.1.2: - version "4.1.4" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" - integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== +tough-cookie@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.0.0.tgz#6b6518e2b5c070cf742d872ee0f4f92d69eac1af" + integrity sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q== dependencies: - psl "^1.1.33" - punycode "^2.1.1" - universalify "^0.2.0" - url-parse "^1.5.3" + tldts "^6.1.32" -tr46@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-4.1.1.tgz#281a758dcc82aeb4fe38c7dfe4d11a395aac8469" - integrity sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw== +tr46@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.0.0.tgz#3b46d583613ec7283020d79019f1335723801cec" + integrity sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g== dependencies: - punycode "^2.3.0" + punycode "^2.3.1" tree-dump@^1.0.1: version "1.0.1" @@ -6839,11 +6822,6 @@ unicode-property-aliases-ecmascript@^1.0.4: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== -universalify@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" - integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== - unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -6869,14 +6847,6 @@ uri-js@^4.2.2, uri-js@^4.4.1: dependencies: punycode "^2.1.0" -url-parse@^1.5.3: - version "1.5.10" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" - integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" @@ -6907,12 +6877,12 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -w3c-xmlserializer@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" - integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== +w3c-xmlserializer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" + integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA== dependencies: - xml-name-validator "^4.0.0" + xml-name-validator "^5.0.0" watchpack@^2.4.1: version "2.4.1" @@ -7078,24 +7048,24 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== -whatwg-encoding@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" - integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== +whatwg-encoding@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5" + integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== dependencies: iconv-lite "0.6.3" -whatwg-mimetype@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" - integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== +whatwg-mimetype@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" + integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== -whatwg-url@^12.0.0, whatwg-url@^12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-12.0.1.tgz#fd7bcc71192e7c3a2a97b9a8d6b094853ed8773c" - integrity sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ== +whatwg-url@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.0.0.tgz#00baaa7fd198744910c4b1ef68378f2200e4ceb6" + integrity sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw== dependencies: - tr46 "^4.1.1" + tr46 "^5.0.0" webidl-conversions "^7.0.0" which-boxed-primitive@^1.0.2: @@ -7194,15 +7164,15 @@ write-file-atomic@^5.0.1: imurmurhash "^0.1.4" signal-exit "^4.0.1" -ws@^8.13.0, ws@^8.16.0: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" - integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== +ws@^8.16.0, ws@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== -xml-name-validator@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" - integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== +xml-name-validator@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673" + integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg== xmlchars@^2.2.0: version "2.2.0" From f6f4d0511c9be8d5e6b0226d36f8d0b237819a67 Mon Sep 17 00:00:00 2001 From: Shane Chesnutt Date: Tue, 12 Nov 2024 11:55:32 -0500 Subject: [PATCH 05/23] LG-14836 Update translations for IPP on doc auth error page (#11483) changelog: User-Facing Improvements, In-Person Proofing, Update the translations for the IPP option on the doc auth error page. --- config/locales/en.yml | 2 +- config/locales/es.yml | 2 +- config/locales/fr.yml | 2 +- config/locales/zh.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index c0a8740407b..e6c366a31ce 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1208,7 +1208,7 @@ in_person_proofing.body.barcode.retail_hours_closed: Closed in_person_proofing.body.barcode.return_to_partner_link: Return to %{sp_name} in_person_proofing.body.barcode.what_to_expect: What to expect at the Post Office in_person_proofing.body.cta.button: Try in person -in_person_proofing.body.cta.prompt_detail: You may be able to verify your identity at a participating Post Office near you. +in_person_proofing.body.cta.prompt_detail: Most people who are unable to complete this step online are successful in verifying their identity at a participating Post Office. No appointment needed. Locations are available nationwide. in_person_proofing.body.expect.heading: What to expect after your visit in_person_proofing.body.expect.info: We’ll send you an email to let you know if your identity verification was successful or unsuccessful within 24 hours of your visit to the Post Office. in_person_proofing.body.location.distance.one: '%{count} mile away' diff --git a/config/locales/es.yml b/config/locales/es.yml index fc28bbaf531..c8dd2efd5f0 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1219,7 +1219,7 @@ in_person_proofing.body.barcode.retail_hours_closed: Cerrado in_person_proofing.body.barcode.return_to_partner_link: Volver a %{sp_name} in_person_proofing.body.barcode.what_to_expect: Qué esperar en la oficina de correos in_person_proofing.body.cta.button: Intentar en persona -in_person_proofing.body.cta.prompt_detail: Es posible que pueda verificar su identidad en una oficina de correos participante cercana. +in_person_proofing.body.cta.prompt_detail: La mayoría de las personas que no pueden hacer la verificación de su identidad en línea logran verificarla en una oficina de correos participante. No es necesario hacer cita para ello, y hay oficinas en todo el país. in_person_proofing.body.expect.heading: Qué esperar después de la visita in_person_proofing.body.expect.info: En las 24 horas siguientes a su visita a la oficina de correos, recibirá un correo electrónico para informarle si se logró o no su verificación de identidad. in_person_proofing.body.location.distance.one: A %{count} milla de distancia diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f419b9ae3d0..c078e9c4862 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1208,7 +1208,7 @@ in_person_proofing.body.barcode.retail_hours_closed: Fermé in_person_proofing.body.barcode.return_to_partner_link: Retourner à %{sp_name} in_person_proofing.body.barcode.what_to_expect: À quoi s’attendre au bureau de poste in_person_proofing.body.cta.button: Essayer en personne -in_person_proofing.body.cta.prompt_detail: Vous pourrez peut-être confirmer votre identité dans un bureau de poste participant près de chez vous. +in_person_proofing.body.cta.prompt_detail: La plupart des personnes qui ne parviennent pas à effectuer cette étape en ligne réussissent à confirmer leur identité dans un bureau de poste participant. Sans rendez-vous. Il existe des sites dans l’ensemble du pays. in_person_proofing.body.expect.heading: Que faire après votre visite in_person_proofing.body.expect.info: Nous vous enverrons un e-mail pour vous informer de la réussite ou de l’échec de la vérification de votre identité dans les 24 heures suivant votre visite au bureau de poste. in_person_proofing.body.location.distance.one: à %{count} mile diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 1530d4741cc..0349e3166ef 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1221,7 +1221,7 @@ in_person_proofing.body.barcode.retail_hours_closed: 关闭 in_person_proofing.body.barcode.return_to_partner_link: 返回 %{sp_name} in_person_proofing.body.barcode.what_to_expect: 在邮局会发生什么 in_person_proofing.body.cta.button: 尝试亲身去 -in_person_proofing.body.cta.prompt_detail: 你也许可以到附近一个参与本项目的邮局去亲身验证你的身份证件。 +in_person_proofing.body.cta.prompt_detail: 无法在网上完成这一步骤的大多数人都能在一个参与本项目的邮局成功地证实身份。去邮局无需预约。全国各地都有参与本项目的邮局。 in_person_proofing.body.expect.heading: 去邮局后会发生什么 in_person_proofing.body.expect.info: 你去邮局后 24 小时内,我们会给你发电邮,告诉你是否成功验证了身份。 in_person_proofing.body.location.distance.one: 距离你 %{count} 英里 From 530d5ace20b0d1cbb6c9831cbccd0c9c7dae3fb7 Mon Sep 17 00:00:00 2001 From: Mitchell Henke Date: Tue, 12 Nov 2024 12:05:57 -0600 Subject: [PATCH 06/23] Update identity-hostdata and redis-session-store to support Rails 8 (#11497) changelog: Internal, Maintenance, Update identity-hostdata and redis-session-store to support Rails 8 --- Gemfile | 4 ++-- Gemfile.lock | 20 ++++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index db1a2b773d9..17262773376 100644 --- a/Gemfile +++ b/Gemfile @@ -36,7 +36,7 @@ gem 'fugit' gem 'foundation_emails' gem 'good_job', '~> 4.0' gem 'http_accept_language' -gem 'identity-hostdata', github: '18F/identity-hostdata', tag: 'v4.0.0' +gem 'identity-hostdata', github: '18F/identity-hostdata', tag: 'v4.4.1' gem 'identity-logging', github: '18F/identity-logging', tag: 'v0.1.1' gem 'identity_validations', github: '18F/identity-validations', tag: 'v0.7.2' gem 'jsbundling-rails', '~> 1.1.2' @@ -66,7 +66,7 @@ gem 'rack-headers_filter' gem 'rack-timeout', require: false gem 'redacted_struct' gem 'redis', '>= 3.2.0' -gem 'redis-session-store', github: '18F/redis-session-store', tag: 'v1.0.1-18f' +gem 'redis-session-store', github: '18F/redis-session-store', tag: 'v1.0.2-18f' gem 'retries' gem 'rexml', '~> 3.3' gem 'rotp', '~> 6.3', '>= 6.3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 26f1b15d422..9fe38995467 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,12 @@ GIT remote: https://github.com/18F/identity-hostdata.git - revision: 9574e05398833c531f450c3da99a6afde4ce68fc - tag: v4.0.0 + revision: 67a19c577b8fa9305350cf9cefa572cef4a80310 + tag: v4.4.1 specs: - identity-hostdata (4.0.0) - activesupport (>= 6.1, < 8) + identity-hostdata (4.4.1) + activesupport (>= 6.1, < 9) aws-sdk-s3 (~> 1.8) + aws-sdk-secretsmanager (>= 1.91) redacted_struct (>= 2.0) GIT @@ -26,11 +27,11 @@ GIT GIT remote: https://github.com/18F/redis-session-store.git - revision: 9e3f8a22a1b5d1e835e5cba20c51e38b8965b836 - tag: v1.0.1-18f + revision: 905c146bbc1c09ce411edd036eac266c53f5b153 + tag: v1.0.2-18f specs: - redis-session-store (1.0.1.pre.18f) - actionpack (>= 6, < 8) + redis-session-store (1.0.2.pre.18f) + actionpack (>= 6, < 9) redis (>= 4.3, < 6) GIT @@ -182,6 +183,9 @@ GEM aws-sdk-core (~> 3, >= 3.179.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.6) + aws-sdk-secretsmanager (1.102.0) + aws-sdk-core (~> 3, >= 3.201.0) + aws-sigv4 (~> 1.5) aws-sdk-ses (1.44.0) aws-sdk-core (~> 3, >= 3.122.0) aws-sigv4 (~> 1.1) From fb044827eaa12dd8714cf9a7fbc5bc6db6dcb355 Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Tue, 12 Nov 2024 14:08:43 -0500 Subject: [PATCH 07/23] LG-14905: socure webhook analytics event updates (#11490) * rebased from main * update the doc for the analytics call changelog: Upcoming Features, Document Authentication, Socure webhook event attribute updates * move empty event webhook test --- app/controllers/socure_webhook_controller.rb | 14 +- app/services/analytics_events.rb | 5 +- .../socure_webhook_controller_spec.rb | 338 ++++++++---------- spec/factories/document_capture_sessions.rb | 11 + 4 files changed, 174 insertions(+), 194 deletions(-) create mode 100644 spec/factories/document_capture_sessions.rb diff --git a/app/controllers/socure_webhook_controller.rb b/app/controllers/socure_webhook_controller.rb index a4ba596cae3..1deb4a46e51 100644 --- a/app/controllers/socure_webhook_controller.rb +++ b/app/controllers/socure_webhook_controller.rb @@ -80,9 +80,10 @@ def log_webhook_receipt analytics.idv_doc_auth_socure_webhook_received( created_at: event[:created], customer_user_id: event[:customerUserId], + docv_transaction_token: event[:docvTransactionToken], event_type: event[:eventType], reference_id: event[:referenceId], - user_id: event[:customerUserId], + user_id: user&.uuid, ) end @@ -94,9 +95,8 @@ def increment_rate_limiter end def document_capture_session - token = event[:docvTransactionToken] || event[:docVTransactionToken] @document_capture_session ||= DocumentCaptureSession.find_by( - socure_docv_transaction_token: token, + socure_docv_transaction_token: docv_transaction_token, ) end @@ -117,4 +117,12 @@ def socure_params :docvTransactionToken, :docVTransactionToken], ) end + + def user + @user ||= document_capture_session&.user + end + + def docv_transaction_token + @docv_transaction_token ||= event[:docvTransactionToken] || event[:docVTransactionToken] + end end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index f9a49854a66..240bb5b40ad 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1566,13 +1566,15 @@ def idv_doc_auth_redo_ssn_submitted( # @param [String] created_at The created timestamp received from Socure # @param [String] customer_user_id The customerUserId received from Socure + # @param [String] docv_transaction_token The docvTransactionToken received from Socure # @param [String] event_type The eventType received from Socure # @param [String] reference_id The referenceId received from Socure - # @param [String] user_id The customerUserId, repackaged as user_id + # @param [String] user_id The uuid of the user using Socure def idv_doc_auth_socure_webhook_received( created_at:, customer_user_id:, event_type:, + docv_transaction_token:, reference_id:, user_id:, **extra @@ -1581,6 +1583,7 @@ def idv_doc_auth_socure_webhook_received( :idv_doc_auth_socure_webhook_received, created_at:, customer_user_id:, + docv_transaction_token:, event_type:, reference_id:, user_id:, diff --git a/spec/controllers/socure_webhook_controller_spec.rb b/spec/controllers/socure_webhook_controller_spec.rb index 479441dea9e..b4a02f11955 100644 --- a/spec/controllers/socure_webhook_controller_spec.rb +++ b/spec/controllers/socure_webhook_controller_spec.rb @@ -4,24 +4,21 @@ RSpec.describe SocureWebhookController do describe 'POST /api/webhooks/socure/event' do - let(:user) { create(:user) } - let(:socure_docv_transaction_token) { 'dummy_docv_transaction_token' } - let(:document_capture_session) do - DocumentCaptureSession.create(user:).tap do |dcs| - dcs.socure_docv_transaction_token = socure_docv_transaction_token - end - end - let(:rate_limiter) { RateLimiter.new(rate_limit_type: :idv_doc_auth, user: user) } let(:socure_secret_key) { 'this-is-a-secret' } let(:socure_secret_key_queue) { ['this-is-an-old-secret', 'this-is-an-older-secret'] } let(:socure_enabled) { true } + let(:event_type) { 'TEST_WEBHOOK' } + let(:event_docv_transaction_token) { 'TEST_WEBHOOK_TOKEN' } + let(:customer_user_id) { '#1-customer' } + let(:reference_id) { 'the-ref-id' } let(:webhook_body) do { event: { created: '2020-01-01T00:00:00Z', - customerUserId: '123', - eventType: 'TEST_WEBHOOK', - referenceId: 'abc', + customerUserId: customer_user_id, + eventType: event_type, + docvTransactionToken: event_docv_transaction_token, + referenceId: reference_id, data: { documentData: { dob: '2000-01-01', @@ -33,19 +30,6 @@ }, } end - let(:document_uploaded_webhook_body) do - { - eventGroup: 'DocvNotification', - reason: 'DOCUMENTS_UPLOADED', - event: { - created: '2020-01-01T00:00:00Z', - docvTransactionToken: socure_docv_transaction_token, - eventType: 'DOCUMENTS_UPLOADED', - message: 'Documents Upload Successful', - referenceId: user.id, - }, - } - end before do allow(IdentityConfig.store).to receive(:socure_webhook_secret_key). @@ -54,202 +38,176 @@ and_return(socure_secret_key_queue) allow(IdentityConfig.store).to receive(:socure_enabled). and_return(socure_enabled) + allow(SocureDocvResultsJob).to receive(:perform_later) stub_analytics end context 'webhook authentication' do - it 'returns OK and logs an event with a correct secret key and body' do - request.headers['Authorization'] = socure_secret_key - post :create, params: webhook_body - - expect(response).to have_http_status(:ok) - expect(@analytics).to have_logged_event( - :idv_doc_auth_socure_webhook_received, - created_at: '2020-01-01T00:00:00Z', - customer_user_id: '123', - event_type: 'TEST_WEBHOOK', - reference_id: 'abc', - user_id: '123', - ) - end - - it 'returns OK with an older secret key' do - request.headers['Authorization'] = socure_secret_key_queue.last - post :create, params: webhook_body - - expect(response).to have_http_status(:ok) - end - - it 'returns unauthorized with a bad secret key' do - request.headers['Authorization'] = 'ABC123' - post :create, params: webhook_body - - expect(response).to have_http_status(:unauthorized) - end - - it 'returns unauthorized with no secret key' do - post :create, params: webhook_body - - expect(response).to have_http_status(:unauthorized) - end - - it 'returns bad request with no event in the body' do - request.headers['Authorization'] = socure_secret_key - post :create, params: {} - - expect(response).to have_http_status(:bad_request) - end - end + context 'received with invalid webhook key' do + it 'returns unauthorized with a bad secret key' do + request.headers['Authorization'] = 'ABC123' + post :create, params: webhook_body - context 'when DOCUMENTS_UPLOADED event received' do - let(:webhook_body) do - { - id: 'a8202f22-7331-483b-a76a-546f68da062d', - origId: '45ac9531-60ae-4bc7-805e-f7823e4e5545', - eventGroup: 'DocvNotification', - reason: 'DOCUMENTS_UPLOADED', - environmentName: 'Production', - event: { - created: '2024-08-07T21:18:19.949Z', - customerUserId: '111-222-333', - docVTransactionToken: '45ac9531-60ae-4bc7-805e-f7823e4e5545', - eventType: 'DOCUMENTS_UPLOADED', - message: 'Documents Upload Successful', - referenceId: '45ac9531-60ae-4bc7-805e-f7823e4e5545', - userId: '444-555-666', - }, - } - end + expect(response).to have_http_status(:unauthorized) + end - it 'returns OK and logs an event with a correct secret key and body' do - request.headers['Authorization'] = socure_secret_key - post :create, params: document_uploaded_webhook_body - expect(response).to have_http_status(:ok) - expect(@analytics).to have_logged_event( - :idv_doc_auth_socure_webhook_received, - created_at: document_uploaded_webhook_body[:event][:created], - event_type: document_uploaded_webhook_body[:event][:eventType], - reference_id: document_uploaded_webhook_body[:event][:referenceId].to_s, - ) - end + it 'returns unauthorized with no secret key' do + post :create, params: webhook_body - context 'when document capture session exists' do - let(:user) { create(:user) } - let(:document_capture_session) do - DocumentCaptureSession.create(user:).tap do |dcs| - dcs.socure_docv_transaction_token = '45ac9531-60ae-4bc7-805e-f7823e4e5545' - end + expect(response).to have_http_status(:unauthorized) end + end + context 'with a valid webhook key' do before do request.headers['Authorization'] = socure_secret_key - allow(DocumentCaptureSession).to receive(:find_by). - and_return(document_capture_session) - allow(SocureDocvResultsJob).to receive(:perform_later) - allow(RateLimiter).to receive(:new).with( - { - user: user, - rate_limit_type: :idv_doc_auth, - }, - ).and_return(rate_limiter) - end - - it 'increments rate limiter of correct user' do - expect(rate_limiter.attempts).to eq 0 - post :create, params: document_uploaded_webhook_body - expect(rate_limiter.attempts).to eq 1 - post :create, params: document_uploaded_webhook_body - expect(rate_limiter.attempts).to eq 2 end - - it 'enqueues a SocureDocvResultsJob' do + it 'returns OK and logs an event with a correct secret key and body' do post :create, params: webhook_body - expect(SocureDocvResultsJob).to have_received(:perform_later). - with(document_capture_session_uuid: document_capture_session.uuid) - end - end - - context 'when document capture session does not exist' do - before do - allow(NewRelic::Agent).to receive(:notice_error) + expect(response).to have_http_status(:ok) + expect(@analytics).to have_logged_event( + :idv_doc_auth_socure_webhook_received, + created_at: '2020-01-01T00:00:00Z', + customer_user_id:, + docv_transaction_token: event_docv_transaction_token, + event_type:, + reference_id:, + ) end - it 'logs an error with NewRelic' do + it 'returns OK with an older secret key' do request.headers['Authorization'] = socure_secret_key_queue.last post :create, params: webhook_body - expect(NewRelic::Agent).to have_received(:notice_error) + expect(response).to have_http_status(:ok) end - end - end - context 'when socure webhook disabled' do - let(:socure_enabled) { false } + context 'when an event does not exist in the body' do + it 'returns bad request' do + post :create, params: {} - it 'the webhook route does not exist' do - request.headers['Authorization'] = socure_secret_key - post :create, params: webhook_body - - expect(response).to be_not_found - end - end + expect(response).to have_http_status(:bad_request) + end + end - context 'when SESSION_COMPLETE event received' do - let(:docv_transaction_token) { '45ac9531-60ae-4bc7-805e-f7823e4e5547' } - let(:webhook_body) do - { - id: 'a8202f22-7331-483b-a76a-546f68da062d', - origId: '45ac9531-60ae-4bc7-805e-f7823e4e5545', - eventGroup: 'DocvNotification', - reason: 'SESSION_COMPLETE', - environmentName: 'Production', - event: { - created: '2024-08-07T21:18:19.949Z', - customerUserId: '111-222-333', - docVTransactionToken: docv_transaction_token, - eventType: 'SESSION_COMPLETE', - message: 'Session Complete', - referenceId: '45ac9531-60ae-4bc7-805e-f7823e4e5545', - }, - } - end + context 'when document capture session exists' do + it 'logs the user\'s uuid' do + dcs = create(:document_capture_session, :socure) + webhook_body[:event][:docvTransactionToken] = dcs.socure_docv_transaction_token + post :create, params: webhook_body + + expect(response).to have_http_status(:ok) + expect(@analytics).to have_logged_event( + :idv_doc_auth_socure_webhook_received, + created_at: '2020-01-01T00:00:00Z', + customer_user_id:, + docv_transaction_token: dcs.socure_docv_transaction_token, + event_type:, + reference_id:, + user_id: dcs.user.uuid, + ) + end - context 'when document capture session exists' do - let(:user) { create(:user) } - let(:document_capture_session) do - DocumentCaptureSession.create(user:).tap do |dcs| - dcs.socure_docv_transaction_token = docv_transaction_token + context 'when DOCUMENTS_UPLOADED event received' do + let(:event_type) { 'DOCUMENTS_UPLOADED' } + + it 'returns OK and logs an event with a correct secret key and body' do + dcs = create(:document_capture_session, :socure) + webhook_body[:event][:docvTransactionToken] = dcs.socure_docv_transaction_token + + post :create, params: webhook_body + expect(response).to have_http_status(:ok) + expect(@analytics).to have_logged_event( + :idv_doc_auth_socure_webhook_received, + created_at: webhook_body[:event][:created], + customer_user_id:, + docv_transaction_token: dcs.socure_docv_transaction_token, + event_type:, + reference_id:, + user_id: dcs.user.uuid, + ) + end + + it 'increments rate limiter of correct user' do + dcs = create(:document_capture_session, :socure) + webhook_body[:event][:docvTransactionToken] = dcs.socure_docv_transaction_token + + i = 0 + while i < 4 + rate_limiter = RateLimiter.new( + user: dcs.user, + rate_limit_type: :idv_doc_auth, + ) + expect(rate_limiter.attempts).to eq i + i += 1 + post :create, params: webhook_body + end + end + + it 'enqueues a SocureDocvResultsJob' do + dcs = create(:document_capture_session, :socure) + webhook_body[:event][:docvTransactionToken] = dcs.socure_docv_transaction_token + + post :create, params: webhook_body + + expect(SocureDocvResultsJob).to have_received(:perform_later). + with(document_capture_session_uuid: dcs.uuid) + end + + context 'when document capture session does not exist' do + before do + allow(NewRelic::Agent).to receive(:notice_error) + end + + it 'logs an error with NewRelic' do + request.headers['Authorization'] = socure_secret_key_queue.last + post :create, params: webhook_body + + expect(NewRelic::Agent).to have_received(:notice_error) + end + end end - end - before do - request.headers['Authorization'] = socure_secret_key - allow(DocumentCaptureSession).to receive(:find_by). - and_return(document_capture_session) - allow(SocureDocvResultsJob).to receive(:perform_later) - allow(RateLimiter).to receive(:new).with( - { - user: user, - rate_limit_type: :idv_doc_auth, - }, - ).and_return(rate_limiter) - end + context 'when SESSION_COMPLETE event received' do + let(:event_type) { 'SESSION_COMPLETE' } + + it 'does not increment rate limiter of user' do + dcs = create(:document_capture_session, :socure) + webhook_body[:event][:docvTransactionToken] = dcs.socure_docv_transaction_token + + i = 0 + while i < 4 + post :create, params: webhook_body + rate_limiter = RateLimiter.new( + user: dcs.user, + rate_limit_type: :idv_doc_auth, + ) + expect(rate_limiter.attempts).to eq 0 + i += 1 + end + end + + it 'does not enqueue a SocureDocvResultsJob' do + dcs = create(:document_capture_session, :socure) + webhook_body[:event][:docvTransactionToken] = dcs.socure_docv_transaction_token + + post :create, params: webhook_body + + expect(SocureDocvResultsJob).not_to have_received(:perform_later) + end + end - it 'does not increment rate limiter of user' do - expect(rate_limiter.attempts).to eq 0 - post :create, params: webhook_body - expect(rate_limiter.attempts).to eq 0 - post :create, params: webhook_body - expect(rate_limiter.attempts).to eq 0 - end + context 'when socure webhook disabled' do + let(:socure_enabled) { false } - it 'does not enqueue a SocureDocvResultsJob' do - post :create, params: webhook_body + it 'the webhook route does not exist' do + post :create, params: webhook_body - expect(SocureDocvResultsJob).not_to have_received(:perform_later). - with(document_capture_session_uuid: document_capture_session.uuid) + expect(response).to be_not_found + end + end end end end diff --git a/spec/factories/document_capture_sessions.rb b/spec/factories/document_capture_sessions.rb new file mode 100644 index 00000000000..679cc9283fd --- /dev/null +++ b/spec/factories/document_capture_sessions.rb @@ -0,0 +1,11 @@ +FactoryBot.define do + factory :document_capture_session do + uuid { SecureRandom.uuid } + user { association :user, :fully_registered } + end + + trait :socure do + socure_docv_transaction_token { SecureRandom.uuid } + socure_docv_capture_app_url { 'https://capture-app.test' } + end +end From 02a022efe4bf6fedb18c0f74a7b8a539a55f4eed Mon Sep 17 00:00:00 2001 From: Vraj Mohan Date: Tue, 12 Nov 2024 11:48:09 -0800 Subject: [PATCH 08/23] Update port forwarding instructions for Android (#11495) changelog: Internal, Documentation, Update port forwarding instructions for Android Co-authored-by: Andrew Duthie <1779930+aduth@users.noreply.github.com> --- docs/images/port-forwarding-settings.png | Bin 0 -> 73697 bytes docs/mobile.md | 30 ++++++++++++++--------- 2 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 docs/images/port-forwarding-settings.png diff --git a/docs/images/port-forwarding-settings.png b/docs/images/port-forwarding-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..9263b0f9e590f7fb4f334df43eccea97566d30ca GIT binary patch literal 73697 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 From bb9c75f9b1169b036fade7ae9c55df7283a57c75 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:48:34 -0500 Subject: [PATCH 09/23] Fix HTML escaping for partner email sharing (#11491) * Fix HTML escaping for partner email sharing changelog: Upcoming Features, Partner Email Selection, Fix HTML escaping for partner email sharing * Add linter for html-safe translation calls * Reword spec description for showing user emails Co-authored-by: Zach Margolis * Lint controllers for HTML t view helper violations Related: https://github.com/18F/identity-idp/pull/11491#discussion_r1838576862 --------- Co-authored-by: Zach Margolis --- .rubocop.yml | 8 +++ app/views/sign_up/select_email/show.html.erb | 2 +- lib/linters/i18n_helper_html_linter.rb | 44 ++++++++++++++++ .../linters/i18n_helper_html_linter_spec.rb | 51 +++++++++++++++++++ .../selected_email/edit.html.erb_spec.rb | 6 +++ .../select_email/show.html.erb_spec.rb | 10 +++- 6 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 lib/linters/i18n_helper_html_linter.rb create mode 100644 spec/lib/linters/i18n_helper_html_linter_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 52c97e23501..00d3129d03b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,6 +7,7 @@ require: - rubocop-rails - rubocop-rspec - rubocop-performance + - ./lib/linters/i18n_helper_html_linter.rb - ./lib/linters/analytics_event_name_linter.rb - ./lib/linters/localized_validation_message_linter.rb - ./lib/linters/image_size_linter.rb @@ -45,6 +46,13 @@ Bundler/InsecureProtocolSource: Gemspec/DuplicatedAssignment: Enabled: true +IdentityIdp/I18nHelperHtmlLinter: + Enabled: true + Include: + - app/views/**/*.erb + - app/components/**/*.erb + - app/controllers/**/*.rb + IdentityIdp/AnalyticsEventNameLinter: Enabled: true Include: diff --git a/app/views/sign_up/select_email/show.html.erb b/app/views/sign_up/select_email/show.html.erb index 00a5134e774..ed505209855 100644 --- a/app/views/sign_up/select_email/show.html.erb +++ b/app/views/sign_up/select_email/show.html.erb @@ -4,7 +4,7 @@ <% c.with_header(id: 'select-email-heading') { t('titles.select_email') } %>

- <%= I18n.t('help_text.select_preferred_email_html', sp: @sp_name) %> + <%= t('help_text.select_preferred_email_html', sp: @sp_name) %>

<%= simple_form_for(@select_email_form, url: sign_up_select_email_path) do |f| %> diff --git a/lib/linters/i18n_helper_html_linter.rb b/lib/linters/i18n_helper_html_linter.rb new file mode 100644 index 00000000000..53251a13b05 --- /dev/null +++ b/lib/linters/i18n_helper_html_linter.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module IdentityIdp + # This linter checks to ensure that strings which include HTML are rendered using Rails `t` + # view helper, rather than through the I18n class. Only the Rails view helper will mark the + # content as HTML-safe. + # + # @see https://guides.rubyonrails.org/i18n.html#using-safe-html-translations + # + # @example + # # bad + # I18n.t('errors.message_html') + # + # # good + # t('errors.message_html') + # + class I18nHelperHtmlLinter < RuboCop::Cop::Base + MSG = 'Use the Rails `t` view helper for HTML-safe strings' + + RESTRICT_ON_SEND = [:t].freeze + + def_node_matcher :i18n_class_send?, <<~PATTERN + (send (const nil? :I18n) :t $...) + PATTERN + + def on_send(node) + return if !i18n_class_send?(node) || !i18n_key(node)&.end_with?('_html') + add_offense(node) + end + + private + + def i18n_key(node) + first_argument = node.arguments.first + return if first_argument.nil? + return if !first_argument.respond_to?(:value) + first_argument.value.to_s + end + end + end + end +end diff --git a/spec/lib/linters/i18n_helper_html_linter_spec.rb b/spec/lib/linters/i18n_helper_html_linter_spec.rb new file mode 100644 index 00000000000..7cd2f0ec4a0 --- /dev/null +++ b/spec/lib/linters/i18n_helper_html_linter_spec.rb @@ -0,0 +1,51 @@ +require 'rubocop' +require 'rubocop/rspec/cop_helper' +require 'rubocop/rspec/expect_offense' + +require_relative '../../../lib/linters/i18n_helper_html_linter' + +RSpec.describe RuboCop::Cop::IdentityIdp::I18nHelperHtmlLinter do + include CopHelper + include RuboCop::RSpec::ExpectOffense + + let(:config) { RuboCop::Config.new } + let(:cop) { RuboCop::Cop::IdentityIdp::I18nHelperHtmlLinter.new(config) } + + it 'registers offense when calling `t` from i18n class with key suffixed by "_html"' do + expect_offense(<<~RUBY) + I18n.t('errors.message_html') + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/I18nHelperHtmlLinter: Use the Rails `t` view helper for HTML-safe strings + RUBY + end + + it 'registers offense when calling `t` from i18n class with symbol key suffixed by "_html"' do + expect_offense(<<~RUBY) + I18n.t(:message_html) + ^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/I18nHelperHtmlLinter: Use the Rails `t` view helper for HTML-safe strings + RUBY + end + + it 'gracefully handles `I18n.t` without arguments' do + expect_no_offenses(<<~RUBY) + I18n.t + RUBY + end + + it 'gracefully handles `I18n.t` with variable key' do + expect_no_offenses(<<~RUBY) + I18n.t(key) + RUBY + end + + it 'registers no offense when calling `t` from i18n class with key not suffixed by "_html"' do + expect_no_offenses(<<~RUBY) + I18n.t('errors.message') + RUBY + end + + it 'registers no offense when calling `t` from Rails view helper' do + expect_no_offenses(<<~RUBY) + t('errors.message_html') + RUBY + end +end diff --git a/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb b/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb index 4645965a8ed..454ddb9144b 100644 --- a/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb +++ b/spec/views/accounts/connected_accounts/selected_email/edit.html.erb_spec.rb @@ -22,6 +22,12 @@ @select_email_form = SelectEmailForm.new(user:, identity:) end + it 'renders introduction text' do + expect(rendered).to have_content( + strip_tags(t('help_text.select_preferred_email_html', sp: identity.display_name)), + ) + end + it 'renders a list of the users email addresses as radio options' do allow(self).to receive(:page).and_return(Capybara.string(rendered)) inputs = page.find_all('[type="radio"]') diff --git a/spec/views/sign_up/select_email/show.html.erb_spec.rb b/spec/views/sign_up/select_email/show.html.erb_spec.rb index 9f360eddf30..30d16c34b92 100644 --- a/spec/views/sign_up/select_email/show.html.erb_spec.rb +++ b/spec/views/sign_up/select_email/show.html.erb_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' RSpec.describe 'sign_up/select_email/show.html.erb' do + subject(:rendered) { render } let(:email) { 'michael.motorist@email.com' } let(:email2) { 'michael.motorist2@email.com' } let(:user) { create(:user) } @@ -10,11 +11,16 @@ create(:email_address, email: email2, user:) @user_emails = user.confirmed_email_addresses @select_email_form = SelectEmailForm.new(user:) + @sp_name = 'Test Service Provider' end - it 'shows all of the user\'s emails' do - render + it 'renders introduction text' do + expect(rendered).to have_content( + strip_tags(t('help_text.select_preferred_email_html', sp: @sp_name)), + ) + end + it 'shows all of the emails' do expect(rendered).to include('michael.motorist@email.com') expect(rendered).to include('michael.motorist2@email.com') end From 6203156c8bca2e7618541dcd3ca83b3b97c85c6d Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:33:40 -0500 Subject: [PATCH 10/23] Replace Webpack dev server with zero-dependency alternative (#11485) * Replace Webpack dev server with zero-dependency alternative changelog: Internal, Dependencies, Replace Webpack dev server with zero-dependency alternative * Update example to conditionally include plugin for development * End server on compilation close * Remove unnecessary build promise initialization https://github.com/18F/identity-idp/pull/11485#discussion_r1835001163 * Avoid setting specific encoding for file read stream Let response deal with it as needed --- Procfile | 2 +- .../lite-webpack-dev-server/README.md | 46 + .../lite-webpack-dev-server/package.json | 7 + .../lite-webpack-dev-server/webpack-plugin.js | 85 ++ package.json | 1 - webpack.config.js | 29 +- yarn.lock | 989 +----------------- 7 files changed, 163 insertions(+), 996 deletions(-) create mode 100644 app/javascript/packages/lite-webpack-dev-server/README.md create mode 100644 app/javascript/packages/lite-webpack-dev-server/package.json create mode 100644 app/javascript/packages/lite-webpack-dev-server/webpack-plugin.js diff --git a/Procfile b/Procfile index 5bcd9cc496a..b62e7746a6d 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1,4 @@ web: WEBPACK_PORT=${WEBPACK_PORT:-3035} bundle exec rackup config.ru --port ${PORT:-3000} --host ${FOREMAN_HOST:-${HOST:-localhost}} worker: bundle exec good_job start -js: WEBPACK_PORT=${WEBPACK_PORT:-3035} yarn webpack $([ -n "$HTTPS" ] && echo "--watch" || echo "serve") +js: WEBPACK_PORT=${WEBPACK_PORT:-3035} yarn webpack --watch css: yarn build:css --watch diff --git a/app/javascript/packages/lite-webpack-dev-server/README.md b/app/javascript/packages/lite-webpack-dev-server/README.md new file mode 100644 index 00000000000..d942f744663 --- /dev/null +++ b/app/javascript/packages/lite-webpack-dev-server/README.md @@ -0,0 +1,46 @@ +# `@18f/identity-lite-webpack-dev-server` + +Minimal, zero-dependency alternative to Webpack's default development web server. + +**What does it do the same as `webpack-dev-server`?** + +- Serves static assets from the built output directory +- Pauses page loads during compilation to guarantee that a page loads with the latest JavaScript + +**What doesn't it do that `webpack-dev-server` does?** + +Most everything else! Notably, it does not: + +- Automatically reload the page when compilation finishes +- Handle anything other than JavaScript + +## Usage + +If migrating from `webpack-dev-server`: + +- Remove your `devServer` configuration from `webpack.config.js` + +Add an instance of `LiteWebpackDevServerPlugin` to your Webpack `plugins` array. The example below +shows how you might conditionally include the plugin in local development only, to avoid the server +being run in production environments. + +```ts +// webpack.config.js + +const { DEV_SERVER_PORT } = process.env; + +export default { + // ... + plugins: [ + // ... + DEV_SERVER_PORT && + new LiteWebpackDevServerPlugin({ publicPath: './public', port: Number(DEV_SERVER_PORT) }), + ] +}; +``` + +Supported options: + +- `publicPath` (`string`): Relative path to the root of the static file server +- `port` (`number`): Port on which the static file server should listen +- `headers` (`object`): Additional headers to include in every response diff --git a/app/javascript/packages/lite-webpack-dev-server/package.json b/app/javascript/packages/lite-webpack-dev-server/package.json new file mode 100644 index 00000000000..c769131bdac --- /dev/null +++ b/app/javascript/packages/lite-webpack-dev-server/package.json @@ -0,0 +1,7 @@ +{ + "name": "@18f/identity-lite-webpack-dev-server", + "version": "1.0.0", + "private": true, + "sideEffects": false, + "main": "./webpack-plugin.js" +} diff --git a/app/javascript/packages/lite-webpack-dev-server/webpack-plugin.js b/app/javascript/packages/lite-webpack-dev-server/webpack-plugin.js new file mode 100644 index 00000000000..8932db838a6 --- /dev/null +++ b/app/javascript/packages/lite-webpack-dev-server/webpack-plugin.js @@ -0,0 +1,85 @@ +const http = require('node:http'); +const { join } = require('node:path'); +const { createReadStream } = require('node:fs'); + +/** + * @typedef PluginOptions + * @prop {string} [publicPath] + * @prop {number} [port] + * @prop {Record} [headers] + */ + +/** + * Webpack plugin name. + * + * @type {string} + */ +const PLUGIN = 'LiteWebpackDevServerPlugin'; + +class LiteWebpackDevServerPlugin { + /** + * @type {string} + */ + publicPath; + + /** + * @type {number} + */ + port; + + /** + * @type {Record} + */ + headers; + + /** + * @param {PluginOptions} options + */ + constructor(options) { + Object.assign(this, { + publicPath: '.', + port: 3035, + headers: { + 'content-type': 'text/javascript', + ...options.headers, + }, + ...options, + }); + } + + /** + * @param {import('webpack').Compiler} compiler + */ + apply(compiler) { + /** @type {Promise} */ + let build; + + /** @type {() => void} */ + let onCompileFinished; + + const server = http.createServer(async (request, response) => { + for (const [key, value] of Object.entries(this.headers)) { + response.setHeader(key, value); + } + + await build; + const url = new URL(request.url ?? '', 'file:///'); + const filePath = join(process.cwd(), this.publicPath, url.pathname); + createReadStream(filePath).pipe(response); + }); + + server.listen(this.port); + + compiler.hooks.beforeCompile.tap(PLUGIN, () => { + build = new Promise((resolve) => { + onCompileFinished = resolve; + }); + }); + + compiler.hooks.afterCompile.tap(PLUGIN, () => onCompileFinished()); + + compiler.hooks.shutdown.tap(PLUGIN, () => server.close()); + } +} + +module.exports = LiteWebpackDevServerPlugin; diff --git a/package.json b/package.json index 92e904c95af..08749c75bb7 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,6 @@ "svgo": "^3.2.0", "swr": "^2.0.0", "typescript": "^5.2.2", - "webpack-dev-server": "^5.0.4", "yarn-deduplicate": "^6.0.2" }, "resolutions": { diff --git a/webpack.config.js b/webpack.config.js index fdec2e0ed53..d0492128dd9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,6 +5,7 @@ const WebpackAssetsManifest = require('webpack-assets-manifest'); const RailsI18nWebpackPlugin = require('@18f/identity-rails-i18n-webpack-plugin'); const RailsAssetsWebpackPlugin = require('@18f/identity-assets/webpack-plugin'); const UnpolyfillWebpackPlugin = require('@18f/identity-unpolyfill-webpack-plugin'); +const LiteWebpackDevServerPlugin = require('@18f/identity-lite-webpack-dev-server'); const env = process.env.NODE_ENV || process.env.RAILS_ENV || 'development'; const host = process.env.HOST || 'localhost'; @@ -22,24 +23,6 @@ module.exports = /** @type {import('webpack').Configuration} */ ({ mode, devtool, target: ['web'], - devServer: { - static: { - directory: './public', - watch: false, - }, - port: devServerPort, - headers: { - 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'no-store', - Vary: '*', - }, - client: { - overlay: { - runtimeErrors: false, - }, - }, - hot: false, - }, entry: entries.reduce((result, path) => { result[parse(path).name] = resolve(path); return result; @@ -112,5 +95,15 @@ module.exports = /** @type {import('webpack').Configuration} */ ({ }), new RailsAssetsWebpackPlugin(), new UnpolyfillWebpackPlugin(), + devServerPort && + new LiteWebpackDevServerPlugin({ + publicPath: './public', + port: Number(devServerPort), + headers: { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-store', + Vary: '*', + }, + }), ], }); diff --git a/yarn.lock b/yarn.lock index ada9a8ae35a..e993b73813d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1218,31 +1218,6 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@jsonjoy.com/base64@^1.1.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" - integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== - -"@jsonjoy.com/json-pack@^1.0.3": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.0.4.tgz#ab59c642a2e5368e8bcfd815d817143d4f3035d0" - integrity sha512-aOcSN4MeAtFROysrbqG137b7gaDDSmVrl5mpo6sT/w+kcXpWnzhMjmY/Fh/sDx26NBxyIE7MB1seqLeCAzy9Sg== - dependencies: - "@jsonjoy.com/base64" "^1.1.1" - "@jsonjoy.com/util" "^1.1.2" - hyperdyperid "^1.2.0" - thingies "^1.20.0" - -"@jsonjoy.com/util@^1.1.2": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.2.0.tgz#0fe9a92de72308c566ebcebe8b5a3f01d3149df2" - integrity sha512-4B8B+3vFsY4eo33DMKyJPlQ3sBMpPFUZK2dr3O3rXrOGKKbYG44J0XSFkDo1VOQiri5HFEhIeVvItjR2xcazmg== - -"@leichtgewicht/ip-codec@^2.0.1": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" - integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== - "@mswjs/cookies@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@mswjs/cookies/-/cookies-1.1.0.tgz#1528eb43630caf83a1d75d5332b30e75e9bb1b5b" @@ -1408,21 +1383,6 @@ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.0.tgz#14264692a9d6e2fa4db3df5e56e94b5e25647ac0" integrity sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A== -"@types/body-parser@*": - version "1.19.5" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" - integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/bonjour@^3.5.13": - version "3.5.13" - resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" - integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== - dependencies: - "@types/node" "*" - "@types/chai-as-promised@*", "@types/chai-as-promised@^7.1.5": version "7.1.5" resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.5.tgz#6e016811f6c7a64f2eed823191c3a6955094e255" @@ -1442,21 +1402,6 @@ dependencies: "@types/react" "*" -"@types/connect-history-api-fallback@^1.5.4": - version "1.5.4" - resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" - integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== - dependencies: - "@types/express-serve-static-core" "*" - "@types/node" "*" - -"@types/connect@*": - version "3.4.38" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" - integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== - dependencies: - "@types/node" "*" - "@types/cookie@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" @@ -1475,43 +1420,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": - version "4.19.5" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz#218064e321126fcf9048d1ca25dd2465da55d9c6" - integrity sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - "@types/send" "*" - -"@types/express@*", "@types/express@^4.17.21": - version "4.17.21" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" - integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.33" - "@types/qs" "*" - "@types/serve-static" "*" - "@types/grecaptcha@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/grecaptcha/-/grecaptcha-3.0.4.tgz#3de601f3b0cd0298faf052dd5bd62aff64c2be2e" integrity sha512-7l1Y8DTGXkx/r4pwU1nMVAR+yD/QC+MCHKXAyEX/7JZhwcN1IED09aZ9vCjjkcGdhSQiu/eJqcXInpl6eEEEwg== -"@types/http-errors@*": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" - integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== - -"@types/http-proxy@^1.17.8": - version "1.17.14" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.14.tgz#57f8ccaa1c1c3780644f8a94f9c6b5000b5e2eec" - integrity sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w== - dependencies: - "@types/node" "*" - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -1541,11 +1454,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/mime@^1": - version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" - integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== - "@types/mocha@^10.0.0": version "10.0.0" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.0.tgz#3d9018c575f0e3f7386c1de80ee66cc21fbb7a52" @@ -1558,13 +1466,6 @@ dependencies: "@types/node" "*" -"@types/node-forge@^1.3.0": - version "1.3.11" - resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" - integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== - dependencies: - "@types/node" "*" - "@types/node@*", "@types/node@^20.11.16", "@types/node@^20.2.5": version "20.14.7" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.7.tgz#342cada27f97509eb8eb2dbc003edf21ce8ab5a8" @@ -1577,16 +1478,6 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== -"@types/qs@*": - version "6.9.15" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" - integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== - -"@types/range-parser@*": - version "1.2.7" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" - integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== - "@types/react-dom@^17.0.11": version "17.0.15" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.15.tgz#f2c8efde11521a4b7991e076cb9c70ba3bb0d156" @@ -1610,11 +1501,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/retry@0.12.2": - version "0.12.2" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" - integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== - "@types/scheduler@*": version "0.16.2" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" @@ -1625,30 +1511,6 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339" integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A== -"@types/send@*": - version "0.17.4" - resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" - integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== - dependencies: - "@types/mime" "^1" - "@types/node" "*" - -"@types/serve-index@^1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" - integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== - dependencies: - "@types/express" "*" - -"@types/serve-static@*", "@types/serve-static@^1.15.5": - version "1.15.7" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" - integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== - dependencies: - "@types/http-errors" "*" - "@types/node" "*" - "@types/send" "*" - "@types/sinon-chai@^3.2.8": version "3.2.8" resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.8.tgz#5871d09ab50d671d8e6dd72e9073f8e738ac61dc" @@ -1669,13 +1531,6 @@ resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== -"@types/sockjs@^0.3.36": - version "0.3.36" - resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" - integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== - dependencies: - "@types/node" "*" - "@types/statuses@^2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.4.tgz#041143ba4a918e8f080f8b0ffbe3d4cb514e2315" @@ -1698,13 +1553,6 @@ resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd" integrity sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g== -"@types/ws@^8.5.10": - version "8.5.10" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" - integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== - dependencies: - "@types/node" "*" - "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -2020,14 +1868,6 @@ abab@^2.0.6: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - acorn-import-attributes@^1.9.5: version "1.9.5" resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" @@ -2101,11 +1941,6 @@ ansi-escapes@^4.3.2: dependencies: type-fest "^0.21.3" -ansi-html-community@^0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" - integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== - ansi-regex@^5.0.0, ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -2174,11 +2009,6 @@ array-buffer-byte-length@^1.0.0: call-bind "^1.0.2" is-array-buffer "^3.0.1" -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - array-includes@^3.1.5, array-includes@^3.1.6: version "3.1.6" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" @@ -2307,11 +2137,6 @@ balanced-match@^2.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== -batch@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" - integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== - big-integer@^1.6.44: version "1.6.51" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" @@ -2322,32 +2147,6 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== -body-parser@1.20.2: - version "1.20.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== - dependencies: - bytes "3.1.2" - content-type "~1.0.5" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.11.0" - raw-body "2.5.2" - type-is "~1.6.18" - unpipe "1.0.0" - -bonjour-service@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" - integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== - dependencies: - fast-deep-equal "^3.1.3" - multicast-dns "^7.2.5" - boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -2414,23 +2213,6 @@ bundle-name@^3.0.0: dependencies: run-applescript "^5.0.0" -bundle-name@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" - integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== - dependencies: - run-applescript "^7.0.0" - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== - -bytes@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - call-bind@^1.0.2, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -2612,7 +2394,7 @@ colord@^2.9.3: resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== -colorette@^2.0.10, colorette@^2.0.14: +colorette@^2.0.14: version "2.0.20" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== @@ -2649,26 +2431,6 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -compressible@~2.0.16: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" - debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" - vary "~1.1.2" - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -2689,38 +2451,11 @@ concurrently@^8.2.2: tree-kill "^1.2.2" yargs "^17.7.2" -connect-history-api-fallback@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" - integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== - -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== - dependencies: - safe-buffer "5.2.1" - -content-type@~1.0.4, content-type@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" - integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== - convert-source-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== - -cookie@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" - integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== - cookie@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" @@ -2743,11 +2478,6 @@ core-js@^3.21.1: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94" integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig== -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - cosmiconfig@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d" @@ -2848,13 +2578,6 @@ date-fns@^2.30.0: dependencies: "@babel/runtime" "^7.21.0" -debug@2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.5" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" @@ -2911,11 +2634,6 @@ default-browser-id@^3.0.0: bplist-parser "^0.2.0" untildify "^4.0.0" -default-browser-id@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.0.tgz#a1d98bf960c15082d8a3fa69e83150ccccc3af26" - integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA== - default-browser@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-4.0.0.tgz#53c9894f8810bf86696de117a6ce9085a3cbc7da" @@ -2926,21 +2644,6 @@ default-browser@^4.0.0: execa "^7.1.1" titleize "^3.0.0" -default-browser@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.2.1.tgz#7b7ba61204ff3e425b556869ae6d3e9d9f1712cf" - integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg== - dependencies: - bundle-name "^4.1.0" - default-browser-id "^5.0.0" - -default-gateway@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" - integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== - dependencies: - execa "^5.0.0" - define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -2968,31 +2671,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -depd@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== -detect-node@^2.0.4: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - diff@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" @@ -3015,13 +2698,6 @@ dirty-chai@^2.0.1: resolved "https://registry.yarnpkg.com/dirty-chai/-/dirty-chai-2.0.1.tgz#6b2162ef17f7943589da840abc96e75bda01aff3" integrity sha512-ys79pWKvDMowIDEPC6Fig8d5THiC0DJ2gmTeGzVAoEH18J8OzLud0Jh7I9IWg3NSk8x2UocznUuFmfHCXYZx9w== -dns-packet@^5.2.2: - version "5.6.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" - integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== - dependencies: - "@leichtgewicht/ip-codec" "^2.0.1" - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -3076,11 +2752,6 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - electron-to-chromium@^1.4.668: version "1.4.724" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.724.tgz#e0a86fe4d3d0e05a4d7b032549d79608078f830d" @@ -3101,11 +2772,6 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - enhanced-resolve@^5.17.1: version "5.17.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" @@ -3223,11 +2889,6 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -3458,16 +3119,6 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -eventemitter3@^4.0.0: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -3503,43 +3154,6 @@ execa@^7.1.1: signal-exit "^3.0.7" strip-final-newline "^3.0.0" -express@^4.17.3: - version "4.19.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" - integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.2" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.6.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.2.0" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.7" - qs "6.11.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3583,13 +3197,6 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -faye-websocket@^0.11.3: - version "0.11.4" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" - integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== - dependencies: - websocket-driver ">=0.5.1" - figures@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -3618,19 +3225,6 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" - find-cache-dir@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" @@ -3706,11 +3300,6 @@ focus-trap@^6.7.1: dependencies: tabbable "^5.2.1" -follow-redirects@^1.0.0: - version "1.15.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== - for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -3735,21 +3324,11 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - foundation-emails@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/foundation-emails/-/foundation-emails-2.3.1.tgz#80d77707d825966cbbe8111ddb4c790976555ff9" integrity sha512-omqS9jEaM4mehUMPDGnp9BmzZ+vWBpkt/cgvTHCNu1w5JWtvaBQop+Ds7c6Gmt77hUIvmo78Oi5N4u2x7K0Utw== -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== - fs-readdir-recursive@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" @@ -3937,7 +3516,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: +graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -3952,11 +3531,6 @@ graphql@^16.8.1: resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== -handle-thing@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" - integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== - has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -4018,16 +3592,6 @@ headers-polyfill@^4.0.2: resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-4.0.2.tgz#9115a76eee3ce8fbf95b6e3c6bf82d936785b44a" integrity sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw== -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" - integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - html-encoding-sniffer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448" @@ -4035,47 +3599,11 @@ html-encoding-sniffer@^4.0.0: dependencies: whatwg-encoding "^3.1.1" -html-entities@^2.4.0: - version "2.5.2" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" - integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== - html-tags@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" - integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== - -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -http-parser-js@>=0.5.1: - version "0.5.8" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" - integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== - http-proxy-agent@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" @@ -4084,26 +3612,6 @@ http-proxy-agent@^7.0.2: agent-base "^7.1.0" debug "^4.3.4" -http-proxy-middleware@^2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" - integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== - dependencies: - "@types/http-proxy" "^1.17.8" - http-proxy "^1.18.1" - is-glob "^4.0.1" - is-plain-obj "^3.0.0" - micromatch "^4.0.2" - -http-proxy@^1.18.1: - version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" - integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - https-proxy-agent@^7.0.5: version "7.0.5" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" @@ -4122,18 +3630,6 @@ human-signals@^4.3.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== -hyperdyperid@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b" - integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - iconv-lite@0.6.3, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -4180,16 +3676,11 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: +inherits@2: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== - ini@^1.3.5: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" @@ -4214,16 +3705,6 @@ intl-tel-input@^24.5.0: resolved "https://registry.yarnpkg.com/intl-tel-input/-/intl-tel-input-24.5.0.tgz#1a7589f046b72a97f8cd30ebae4c0615635f542d" integrity sha512-utSW+7a2JpUFdRRwo6CAiEQuAMwWxnCurPr2VDkfnW7W9g9ZprvPJPj00vxkiGV8h3GMRAD7aUtX9+oYwD/8OQ== -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -ipaddr.js@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" - integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== - is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" @@ -4318,11 +3799,6 @@ is-negative-zero@^2.0.2: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== -is-network-error@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.1.0.tgz#d26a760e3770226d11c169052f266a4803d9c997" - integrity sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g== - is-node-process@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.2.0.tgz#ea02a1b90ddb3934a19aea414e88edef7e11d134" @@ -4350,11 +3826,6 @@ is-plain-obj@^2.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== -is-plain-obj@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" - integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== - is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -4441,23 +3912,11 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" -is-wsl@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2" - integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== - dependencies: - is-inside-container "^1.0.0" - isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -4619,14 +4078,6 @@ language-tags@^1.0.5: dependencies: language-subtag-registry "~0.3.2" -launch-editor@^2.6.1: - version "2.8.0" - resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.8.0.tgz#7255d90bdba414448e2138faa770a74f28451305" - integrity sha512-vJranOAJrI/llyWGRQqiDM+adrw+k83fvmmx3+nV47g3+36xM15jE+zyZ6Ffel02+xSvuM0b2GDRosXZkbb6wA== - dependencies: - picocolors "^1.0.0" - shell-quote "^1.8.1" - levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -4845,31 +4296,11 @@ mdn-data@2.0.30: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== - -memfs@^4.6.0: - version "4.9.3" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.9.3.tgz#41a3218065fe3911d9eba836250c8f4e43f816bc" - integrity sha512-bsYSSnirtYTWi1+OPMFb0M048evMKyUYe0EbtuGQgq6BVQM1g1W8/KIUJCCvjgI/El0j6Q4WsmMiBwLUBSw8LA== - dependencies: - "@jsonjoy.com/json-pack" "^1.0.3" - "@jsonjoy.com/util" "^1.1.2" - tree-dump "^1.0.1" - tslib "^2.0.0" - meow@^13.1.0: version "13.2.0" resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f" integrity sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA== -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== - merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -4880,12 +4311,7 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== - -micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: +micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -4893,23 +4319,18 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.3" picomatch "^2.3.1" -mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": +mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.27: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -4920,11 +4341,6 @@ mimic-fn@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== -minimalistic-assert@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - minimatch@5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" @@ -4994,11 +4410,6 @@ mq-polyfill@^1.1.8: resolved "https://registry.yarnpkg.com/mq-polyfill/-/mq-polyfill-1.1.8.tgz#c144190b21214bf8d8b099e7e34e6ca2b888dc14" integrity sha1-wUQZCyEhS/jYsJnn405soriI3BQ= -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -5032,14 +4443,6 @@ msw@^2.2.1: type-fest "^4.9.0" yargs "^17.7.2" -multicast-dns@^7.2.5: - version "7.2.5" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" - integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== - dependencies: - dns-packet "^5.2.2" - thunky "^1.0.2" - mute-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" @@ -5055,11 +4458,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -5076,11 +4474,6 @@ nise@^5.1.1: just-extend "^4.0.2" path-to-regexp "^1.7.0" -node-forge@^1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" - integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== - node-modules-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" @@ -5182,23 +4575,6 @@ object.values@^1.1.5, object.values@^1.1.6: define-properties "^1.1.4" es-abstract "^1.20.4" -obuf@^1.0.0, obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -on-finished@2.4.1, on-finished@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -5220,16 +4596,6 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" -open@^10.0.3: - version "10.1.0" - resolved "https://registry.yarnpkg.com/open/-/open-10.1.0.tgz#a7795e6e5d519abe4286d9937bb24b51122598e1" - integrity sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw== - dependencies: - default-browser "^5.2.1" - define-lazy-prop "^3.0.0" - is-inside-container "^1.0.0" - is-wsl "^3.1.0" - open@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/open/-/open-9.1.0.tgz#684934359c90ad25742f5a26151970ff8c6c80b6" @@ -5292,15 +4658,6 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-retry@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-6.2.0.tgz#8d6df01af298750009691ce2f9b3ad2d5968f3bd" - integrity sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA== - dependencies: - "@types/retry" "0.12.2" - is-network-error "^1.0.0" - retry "^0.13.1" - p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -5335,11 +4692,6 @@ parse5@^7.1.2: dependencies: entities "^4.4.0" -parseurl@~1.3.2, parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -5378,11 +4730,6 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== - path-to-regexp@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" @@ -5519,11 +4866,6 @@ pretty-format@^27.0.2: ansi-styles "^5.0.0" react-is "^17.0.1" -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" @@ -5533,26 +4875,11 @@ prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" -proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - punycode@^2.1.0, punycode@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -qs@6.11.0: - version "6.11.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== - dependencies: - side-channel "^1.0.4" - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -5578,21 +4905,6 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -range-parser@^1.2.1, range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" - integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - react-dom@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" @@ -5638,28 +4950,6 @@ react@^17.0.2: loose-envify "^1.1.0" object-assign "^4.1.1" -readable-stream@^2.0.1: - version "2.3.8" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.6: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -5756,11 +5046,6 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -5800,11 +5085,6 @@ resolve@^2.0.0-next.3: is-core-module "^2.2.0" path-parse "^1.0.6" -retry@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" - integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== - reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -5836,11 +5116,6 @@ run-applescript@^5.0.0: dependencies: execa "^5.0.0" -run-applescript@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.0.0.tgz#e5a553c2bffd620e169d276c1cd8f1b64778fbeb" - integrity sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A== - run-async@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-3.0.0.tgz#42a432f6d76c689522058984384df28be379daad" @@ -5860,12 +5135,7 @@ rxjs@^7.4.0, rxjs@^7.8.1: dependencies: tslib "^2.1.0" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@^5.1.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -5879,7 +5149,7 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": +"safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -6017,7 +5287,7 @@ schema-utils@^3.1.1, schema-utils@^3.2.0, schema-utils@^3.3.0: ajv "^6.12.5" ajv-keywords "^3.5.2" -schema-utils@^4.0.0, schema-utils@^4.2.0: +schema-utils@^4.0.0: version "4.2.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== @@ -6027,19 +5297,6 @@ schema-utils@^4.0.0, schema-utils@^4.2.0: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" - integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== - -selfsigned@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" - integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== - dependencies: - "@types/node-forge" "^1.3.0" - node-forge "^1" - semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -6055,25 +5312,6 @@ semver@^7.3.7, semver@^7.5.0, semver@^7.5.4: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== - dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" - ms "2.1.3" - on-finished "2.4.1" - range-parser "~1.2.1" - statuses "2.0.1" - serialize-javascript@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" @@ -6088,29 +5326,6 @@ serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" -serve-index@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" - integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== - dependencies: - accepts "~1.3.4" - batch "0.6.1" - debug "2.6.9" - escape-html "~1.0.3" - http-errors "~1.6.2" - mime-types "~2.1.17" - parseurl "~1.3.2" - -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.18.0" - set-function-length@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -6123,16 +5338,6 @@ set-function-length@^1.2.1: gopd "^1.0.1" has-property-descriptors "^1.0.2" -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -6218,15 +5423,6 @@ smartquotes@^2.3.2: resolved "https://registry.yarnpkg.com/smartquotes/-/smartquotes-2.3.2.tgz#fb1630c49ba04e57446e1a97dc10d590072af4a6" integrity sha512-0R6YJ5hLpDH4mZR7N5eZ12oCMLspvGOHL9A9SEm2e3b/CQmQidekW4SWSKEmor/3x6m3NCBBEqLzikcZC9VJNQ== -sockjs@^0.3.24: - version "0.3.24" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" - integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== - dependencies: - faye-websocket "^0.11.3" - uuid "^8.3.2" - websocket-driver "^0.7.4" - source-list-map@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -6264,39 +5460,11 @@ spawn-command@0.0.2: resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e" integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ== -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - dependencies: - debug "^4.1.0" - detect-node "^2.0.4" - hpack.js "^2.1.6" - obuf "^1.1.2" - readable-stream "^3.0.6" - wbuf "^1.7.3" - -spdy@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" - integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== - dependencies: - debug "^4.1.0" - handle-thing "^2.0.0" - http-deceiver "^1.2.7" - select-hose "^2.0.0" - spdy-transport "^3.0.0" - -statuses@2.0.1, statuses@^2.0.1: +statuses@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -"statuses@>= 1.4.0 < 2": - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== - strict-event-emitter@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz#1602ece81c51574ca39c6815e09f1a3e8550bd93" @@ -6370,20 +5538,6 @@ string.prototype.trimstart@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -6635,16 +5789,6 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -thingies@^1.20.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/thingies/-/thingies-1.21.0.tgz#e80fbe58fd6fdaaab8fad9b67bd0a5c943c445c1" - integrity sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g== - -thunky@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" - integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== - titleize@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/titleize/-/titleize-3.0.0.tgz#71c12eb7fdd2558aa8a44b0be83b8a76694acd53" @@ -6674,11 +5818,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - tough-cookie@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.0.0.tgz#6b6518e2b5c070cf742d872ee0f4f92d69eac1af" @@ -6693,11 +5832,6 @@ tr46@^5.0.0: dependencies: punycode "^2.3.1" -tree-dump@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.1.tgz#b448758da7495580e6b7830d6b7834fca4c45b96" - integrity sha512-WCkcRBVPSlHHq1dc/px9iOfqklvzCbdRwvlNfxGZsrHqf6aZttfPrd7DJTt6oR10dwUfpFFQeVTkPbBIZxX/YA== - tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" @@ -6723,7 +5857,7 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.1.0, tslib@^2.5.0, tslib@^2.6.0: +tslib@^2.1.0, tslib@^2.5.0, tslib@^2.6.0: version "2.6.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== @@ -6762,14 +5896,6 @@ type-fest@^4.9.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.10.2.tgz#3abdb144d93c5750432aac0d73d3e85fcab45738" integrity sha512-anpAG63wSpdEbLwOqH8L84urkL6PiVIov3EMmgIhhThevh9aiMQov+6Btx0wldNcvm4wV+e2/Rt1QdDwKHFbHw== -type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - typed-array-length@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" @@ -6822,11 +5948,6 @@ unicode-property-aliases-ecmascript@^1.0.4: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== - untildify@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" @@ -6852,31 +5973,16 @@ use-sync-external-store@^1.2.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: +util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - varint@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0" integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg== -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== - w3c-xmlserializer@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" @@ -6892,13 +5998,6 @@ watchpack@^2.4.1: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" -wbuf@^1.1.0, wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - webidl-conversions@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" @@ -6936,54 +6035,6 @@ webpack-cli@^5.1.4: rechoir "^0.8.0" webpack-merge "^5.7.3" -webpack-dev-middleware@^7.1.0: - version "7.2.1" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-7.2.1.tgz#2af00538b6e4eda05f5afdd5d711dbebc05958f7" - integrity sha512-hRLz+jPQXo999Nx9fXVdKlg/aehsw1ajA9skAneGmT03xwmyuhvF93p6HUKKbWhXdcERtGTzUCtIQr+2IQegrA== - dependencies: - colorette "^2.0.10" - memfs "^4.6.0" - mime-types "^2.1.31" - on-finished "^2.4.1" - range-parser "^1.2.1" - schema-utils "^4.0.0" - -webpack-dev-server@^5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz#cb6ea47ff796b9251ec49a94f24a425e12e3c9b8" - integrity sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA== - dependencies: - "@types/bonjour" "^3.5.13" - "@types/connect-history-api-fallback" "^1.5.4" - "@types/express" "^4.17.21" - "@types/serve-index" "^1.9.4" - "@types/serve-static" "^1.15.5" - "@types/sockjs" "^0.3.36" - "@types/ws" "^8.5.10" - ansi-html-community "^0.0.8" - bonjour-service "^1.2.1" - chokidar "^3.6.0" - colorette "^2.0.10" - compression "^1.7.4" - connect-history-api-fallback "^2.0.0" - default-gateway "^6.0.3" - express "^4.17.3" - graceful-fs "^4.2.6" - html-entities "^2.4.0" - http-proxy-middleware "^2.0.3" - ipaddr.js "^2.1.0" - launch-editor "^2.6.1" - open "^10.0.3" - p-retry "^6.2.0" - rimraf "^5.0.5" - schema-utils "^4.2.0" - selfsigned "^2.4.1" - serve-index "^1.9.1" - sockjs "^0.3.24" - spdy "^4.0.2" - webpack-dev-middleware "^7.1.0" - ws "^8.16.0" - webpack-merge@^5.7.3: version "5.8.0" resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61" @@ -7034,20 +6085,6 @@ webpack@^5.94.0: watchpack "^2.4.1" webpack-sources "^3.2.3" -websocket-driver@>=0.5.1, websocket-driver@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - whatwg-encoding@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5" @@ -7164,7 +6201,7 @@ write-file-atomic@^5.0.1: imurmurhash "^0.1.4" signal-exit "^4.0.1" -ws@^8.16.0, ws@^8.18.0: +ws@^8.18.0: version "8.18.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== From 0c46c4cd730994dd7a313bd5ff6e19d083fb46f9 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:40:21 -0500 Subject: [PATCH 11/23] Improve font optimizer to exclude telephony, mailer strings (#11487) * Exclude telephony strings from font glyph scraper changelog: Internal, Automated Tooling, Exclude telephony strings from font glyph scraper * Exclude user_mailer strings from font glyph scraper * Use Hash#each_key to iterate hash keys --- Makefile | 2 ++ scripts/yaml_characters | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 1fbbab9a121..f10d272378a 100644 --- a/Makefile +++ b/Makefile @@ -120,9 +120,11 @@ lint_yaml: normalize_yaml ## Lints YAML files lint_font_glyphs: ## Lints to validate content glyphs match expectations from fonts scripts/yaml_characters \ --exclude-locale=zh \ + --exclude-path=config/locales/telephony \ --exclude-gem-path=faker \ --exclude-gem-path=good_job \ --exclude-gem-path=i18n-tasks \ + --exclude-key-scope=user_mailer \ > app/assets/fonts/glyphs.txt (! git diff --name-only | grep "glyphs\.txt$$") || (echo "Error: New character data found. Follow 'Fonts' instructions in 'docs/frontend.md' to regenerate fonts."; exit 1) diff --git a/scripts/yaml_characters b/scripts/yaml_characters index 9be80fe72b1..ba7bca65a5e 100755 --- a/scripts/yaml_characters +++ b/scripts/yaml_characters @@ -5,7 +5,9 @@ require 'pathname' require_relative '../config/environment' excluded_locales = [] -excluded_gem_paths = [] +excluded_paths = [] +excluded_key_scopes = [] + OptionParser.new do |opts| opts.banner = <<~TXT Usage @@ -22,8 +24,16 @@ OptionParser.new do |opts| excluded_locales << locale.to_sym end + opts.on('--exclude-path=PATH', 'Disregard characters from the given relative path') do |path| + excluded_paths << File.join(Dir.pwd, path, '') + end + opts.on('--exclude-gem-path=GEM', 'Disregard characters loaded by the given gem') do |gem| - excluded_gem_paths << Gem.loaded_specs[gem].full_gem_path + excluded_paths << File.join(Gem.loaded_specs[gem].full_gem_path, '') + end + + opts.on('--exclude-key-scope=SCOPE', 'Exclude keys in the given top-level key scope') do |scope| + excluded_key_scopes << scope.to_sym end opts.on('-h', '--help', 'Prints this help') do @@ -50,12 +60,13 @@ def hash_values(hash) end I18n.load_path.reject! do |load_path| - excluded_gem_paths.any? { |gem_path| load_path.start_with?(gem_path) } + excluded_paths.any? { |path| load_path.start_with?(path) } end I18n.backend.eager_load! data = I18n.backend.translations.slice(*I18n.available_locales - excluded_locales) +excluded_key_scopes.each { |scope| data.each_key { |locale| data[locale].delete(scope) } } strings = hash_values(data) joined_string = strings.join('') sanitized_string = sanitize(joined_string) From ed38d8a44573aad9b8027ea29450e7304c7c1dee Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:41:03 -0500 Subject: [PATCH 12/23] Add identifier for explicit frontend error logging (#11481) * Add identifier for explicit frontend error logging changelog: Internal, Analytics, Add identifier for explicit frontend error logging * Update frontend production errors debugging * Update FrontendLogController expected NewRelic call --- app/forms/frontend_error_form.rb | 9 ++- app/javascript/packages/analytics/README.md | 5 +- .../packages/analytics/index.spec.ts | 5 +- app/javascript/packages/analytics/index.ts | 13 ++-- .../captcha-submit-button-element.ts | 2 +- .../webauthn-verify-button-element.ts | 2 +- app/javascript/packs/track-errors.ts | 2 +- app/javascript/packs/webauthn-setup.ts | 2 +- app/services/frontend_error_logger.rb | 6 +- docs/frontend.md | 6 +- .../frontend_log_controller_spec.rb | 13 ++-- spec/forms/frontend_error_form_spec.rb | 23 +++++-- spec/services/frontend_error_logger_spec.rb | 63 +++++++++++++------ 13 files changed, 104 insertions(+), 47 deletions(-) diff --git a/app/forms/frontend_error_form.rb b/app/forms/frontend_error_form.rb index e7fcd196efc..f9ed8046632 100644 --- a/app/forms/frontend_error_form.rb +++ b/app/forms/frontend_error_form.rb @@ -7,10 +7,11 @@ class FrontendErrorForm validate :validate_filename_extension validate :validate_filename_host - attr_reader :filename + attr_reader :filename, :error_id - def submit(filename:) + def submit(filename:, error_id:) @filename = filename + @error_id = error_id FormResponse.new(success: valid?, errors:, serialize_error_details_only: true) end @@ -18,11 +19,13 @@ def submit(filename:) private def validate_filename_extension - return if File.extname(filename.to_s) == '.js' + return if error_id || File.extname(filename.to_s) == '.js' errors.add(:filename, :invalid_extension, message: t('errors.general')) end def validate_filename_host + return if error_id + begin return if URI(filename.to_s).host == IdentityConfig.store.domain_name rescue URI::InvalidURIError; end diff --git a/app/javascript/packages/analytics/README.md b/app/javascript/packages/analytics/README.md index 752f2f17633..e33fb2b669a 100644 --- a/app/javascript/packages/analytics/README.md +++ b/app/javascript/packages/analytics/README.md @@ -8,6 +8,9 @@ Utilities and custom elements for logging events and errors in the application. Track an event or error from your code using exported function members. +Since JavaScript may be bundled and minified in production environments, including an `errorId` is +required to uniquely identify the source of the error. + ```ts import { trackEvent, trackError } from '@18f/identity-analytics'; @@ -18,7 +21,7 @@ button.addEventListener('click', () => { try { doSomethingRisky(); } catch (error) { - trackError(error); + trackError(error, { errorId: 'exampleId' }); } ``` diff --git a/app/javascript/packages/analytics/index.spec.ts b/app/javascript/packages/analytics/index.spec.ts index 6a3a374c125..5253d73ed68 100644 --- a/app/javascript/packages/analytics/index.spec.ts +++ b/app/javascript/packages/analytics/index.spec.ts @@ -93,7 +93,7 @@ describe('trackError', () => { }); it('tracks event', async () => { - trackError(new Error('Oops!')); + trackError(new Error('Oops!'), { errorId: 'exampleId' }); expect(global.navigator.sendBeacon).to.have.been.calledOnce(); @@ -101,12 +101,13 @@ describe('trackError', () => { expect(actualEndpoint).to.eql(endpoint); const { event, payload } = JSON.parse(await data.text()); - const { name, message, stack } = payload; + const { name, message, stack, error_id: errorId } = payload; expect(event).to.equal('Frontend Error'); expect(name).to.equal('Error'); expect(message).to.equal('Oops!'); expect(stack).to.be.a('string'); + expect(errorId).to.equal('exampleId'); }); context('with event parameter', () => { diff --git a/app/javascript/packages/analytics/index.ts b/app/javascript/packages/analytics/index.ts index 22fa2d91308..45e546195a4 100644 --- a/app/javascript/packages/analytics/index.ts +++ b/app/javascript/packages/analytics/index.ts @@ -2,6 +2,11 @@ import { getConfigValue } from '@18f/identity-config'; export { default as isTrackableErrorEvent } from './is-trackable-error-event'; +/** + * Metadata used to identify the source of an error. + */ +type ErrorMetadata = { errorId?: never; filename: string } | { errorId: string; filename?: never }; + /** * Logs an event. * @@ -24,8 +29,8 @@ export function trackEvent(event: string, payload?: object) { * Logs an error. * * @param error Error object. - * @param event Error event, if error is caught using an `error` event handler. Including this can - * add additional resolution to the logged error, notably the filename where the error occurred. + * @param metadata Metadata used to identify the source of an error, including either the filename + * from an ErrorEvent object, or a unique identifier. */ -export const trackError = ({ name, message, stack }: Error, event?: ErrorEvent) => - trackEvent('Frontend Error', { name, message, stack, filename: event?.filename }); +export const trackError = ({ name, message, stack }: Error, { filename, errorId }: ErrorMetadata) => + trackEvent('Frontend Error', { name, message, stack, filename, error_id: errorId }); diff --git a/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts b/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts index 45f6e33afeb..6177b63732a 100644 --- a/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts +++ b/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts @@ -53,7 +53,7 @@ class CaptchaSubmitButtonElement extends HTMLElement { try { token = await this.recaptchaClient!.execute(siteKey!, { action }); } catch (error) { - trackError(error); + trackError(error, { errorId: 'recaptchaExecute' }); } this.tokenInput.value = token; diff --git a/app/javascript/packages/webauthn/webauthn-verify-button-element.ts b/app/javascript/packages/webauthn/webauthn-verify-button-element.ts index bafbdb6bd69..9acca615c1a 100644 --- a/app/javascript/packages/webauthn/webauthn-verify-button-element.ts +++ b/app/javascript/packages/webauthn/webauthn-verify-button-element.ts @@ -54,7 +54,7 @@ class WebauthnVerifyButtonElement extends HTMLElement { this.setInputValue('signature', result.signature); } catch (error) { if (!isExpectedWebauthnError(error, { isVerifying: true })) { - trackError(error); + trackError(error, { errorId: 'webauthnVerify' }); } if (isUserVerificationScreenLockError(error)) { diff --git a/app/javascript/packs/track-errors.ts b/app/javascript/packs/track-errors.ts index fb4582900ad..c0686e2ec0c 100644 --- a/app/javascript/packs/track-errors.ts +++ b/app/javascript/packs/track-errors.ts @@ -9,6 +9,6 @@ declare let window: WindowWithInitialErrors; const { _e: initialErrors } = window; const handleErrorEvent = (event: ErrorEvent) => - isTrackableErrorEvent(event) && trackError(event.error, event); + isTrackableErrorEvent(event) && trackError(event.error, { filename: event.filename }); initialErrors.forEach(handleErrorEvent); window.addEventListener('error', handleErrorEvent); diff --git a/app/javascript/packs/webauthn-setup.ts b/app/javascript/packs/webauthn-setup.ts index b0c2d0c9eff..2e28f36d8b6 100644 --- a/app/javascript/packs/webauthn-setup.ts +++ b/app/javascript/packs/webauthn-setup.ts @@ -75,7 +75,7 @@ function webauthn() { }) .catch((error: Error) => { if (!isExpectedWebauthnError(error)) { - trackError(error); + trackError(error, { errorId: 'webauthnSetup' }); } reloadWithError(error.name, { force: true }); diff --git a/app/services/frontend_error_logger.rb b/app/services/frontend_error_logger.rb index 3037954aa51..226dd49aff5 100644 --- a/app/services/frontend_error_logger.rb +++ b/app/services/frontend_error_logger.rb @@ -3,13 +3,13 @@ class FrontendErrorLogger class FrontendError < StandardError; end - def self.track_error(name:, message:, stack:, filename:) - return unless FrontendErrorForm.new.submit(filename:).success? + def self.track_error(name:, message:, stack:, filename: nil, error_id: nil) + return unless FrontendErrorForm.new.submit(filename:, error_id:).success? NewRelic::Agent.notice_error( FrontendError.new, expected: true, - custom_params: { frontend_error: { name:, message:, stack:, filename: } }, + custom_params: { frontend_error: { name:, message:, stack:, filename:, error_id: } }, ) end end diff --git a/docs/frontend.md b/docs/frontend.md index 61486c65a5e..50ea1687641 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -373,10 +373,14 @@ Each error includes a few details to help you debug: - `message`: Corresponds to [`Error#message`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/message), and is usually a good summary to group by - `name`: The subclass of the error (e.g. `TypeError`) - `stack`: A stacktrace of the individual error instance +- `filename`: The URL of the script where the error was raised, if it's an uncaught error +- `error_id`: A unique identifier for tracing caught errors explicitly tracked Note that NewRelic creates links in stack traces which are invalid, since they include the line and column number. If you encounter an "AccessDenied" error when clicking a stacktrace link, make sure to remove those details after the `.js` in your browser URL. -Debugging these stack traces can be difficult, since files in production are minified, and the stack traces include line numbers and columns for minified files. With the following steps, you can find a reference to the original code: +If an error includes `error_id`, you can use this to search in code for the corresponding call to `trackError` including that value as its `errorId` to trace where the error occurred. + +Otherwise, debugging these stack traces can be difficult, since files in production are minified, and the stack traces include line numbers and columns for minified files. With the following steps, you can find a reference to the original code: 1. Download the minified JavaScript file referenced in the stack trace - Example: https://secure.login.gov/packs/document-capture-e41c853e.digested.js diff --git a/spec/controllers/frontend_log_controller_spec.rb b/spec/controllers/frontend_log_controller_spec.rb index a717ac1ecd5..a891ce398b2 100644 --- a/spec/controllers/frontend_log_controller_spec.rb +++ b/spec/controllers/frontend_log_controller_spec.rb @@ -60,9 +60,11 @@ let(:flow_path) { 'standard' } let(:event) { 'IdV: location submitted' } let(:payload) do - { 'selected_location' => selected_location, + { + 'selected_location' => selected_location, 'flow_path' => flow_path, - 'opted_in_to_in_person_proofing' => nil } + 'opted_in_to_in_person_proofing' => nil, + } end it 'succeeds' do @@ -94,9 +96,11 @@ { opt_in_analytics_properties: true } end let(:payload) do - { 'selected_location' => selected_location, + { + 'selected_location' => selected_location, 'flow_path' => flow_path, - 'opted_in_to_in_person_proofing' => true } + 'opted_in_to_in_person_proofing' => true, + } end before do @@ -207,6 +211,7 @@ message: 'message', stack: 'stack', filename: 'filename', + error_id: nil, }, }, expected: true, diff --git a/spec/forms/frontend_error_form_spec.rb b/spec/forms/frontend_error_form_spec.rb index d800d1fe0df..189bcb1ef5b 100644 --- a/spec/forms/frontend_error_form_spec.rb +++ b/spec/forms/frontend_error_form_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' RSpec.describe FrontendErrorForm do - let(:filename) { 'https://example.com/foo.js' } - subject(:form) { described_class.new } before do @@ -10,7 +8,9 @@ end describe '#submit' do - subject(:result) { form.submit(filename:) } + subject(:result) { form.submit(filename:, error_id:) } + let(:error_id) { nil } + let(:filename) { 'https://example.com/foo.js' } context 'with valid filename' do let(:filename) { 'https://example.com/foo.js' } @@ -24,9 +24,20 @@ context 'without filename' do let(:filename) { nil } - it 'is unsuccessful' do - expect(result.success?).to eq(false) - expect(result.errors).to eq(filename: [t('errors.general'), t('errors.general')]) + context 'without error id' do + it 'is unsuccessful' do + expect(result.success?).to eq(false) + expect(result.errors).to eq(filename: [t('errors.general'), t('errors.general')]) + end + end + + context 'with error id' do + let(:error_id) { 'exampleId' } + + it 'is successful' do + expect(result.success?).to eq(true) + expect(result.errors).to eq({}) + end end end diff --git a/spec/services/frontend_error_logger_spec.rb b/spec/services/frontend_error_logger_spec.rb index 7f1922e35eb..2e23ba28f1d 100644 --- a/spec/services/frontend_error_logger_spec.rb +++ b/spec/services/frontend_error_logger_spec.rb @@ -9,26 +9,51 @@ end describe '.track_event' do - it 'notices an expected error to NewRelic with custom parameters' do - expect(NewRelic::Agent).to receive(:notice_error).with( - kind_of(FrontendErrorLogger::FrontendError), - expected: true, - custom_params: { - frontend_error: { - name: 'name', - message: 'message', - stack: 'stack', - filename: 'filename.js', + let(:payload) { { name: 'name', message: 'message', stack: 'stack' } } + subject(:result) { FrontendErrorLogger.track_error(**payload) } + + context 'with filename payload' do + let(:payload) { super().merge(filename: 'filename.js') } + + it 'notices an expected error to NewRelic with custom parameters' do + expect(NewRelic::Agent).to receive(:notice_error).with( + kind_of(FrontendErrorLogger::FrontendError), + expected: true, + custom_params: { + frontend_error: { + name: 'name', + message: 'message', + stack: 'stack', + filename: 'filename.js', + error_id: nil, + }, + }, + ) + + result + end + end + + context 'with error id payload' do + let(:payload) { super().merge(error_id: 'exampleId') } + + it 'notices an expected error to NewRelic with custom parameters' do + expect(NewRelic::Agent).to receive(:notice_error).with( + kind_of(FrontendErrorLogger::FrontendError), + expected: true, + custom_params: { + frontend_error: { + name: 'name', + message: 'message', + stack: 'stack', + filename: nil, + error_id: 'exampleId', + }, }, - }, - ) - - FrontendErrorLogger.track_error( - name: 'name', - message: 'message', - stack: 'stack', - filename: 'filename.js', - ) + ) + + result + end end context 'with unsuccessful validation of request parameters' do From 9827ee16828ce7d3bd7c19305ed5285e155dd8ba Mon Sep 17 00:00:00 2001 From: A Shukla Date: Tue, 12 Nov 2024 16:47:36 -0600 Subject: [PATCH 13/23] lg-14839 remove the customerUserID from the DocumentRequest (#11486) * changelog: Upcoming Features, socure, remove customerUserID from document request to socure * Fixing rebase * removing customerUserID from socure test responses to more accuratly represent acutal socure response --- .../idv/hybrid_mobile/socure/document_capture_controller.rb | 1 - app/controllers/idv/socure/document_capture_controller.rb | 1 - app/services/doc_auth/socure/requests/document_request.rb | 5 +---- .../hybrid_mobile/socure/document_capture_controller_spec.rb | 4 ---- .../idv/socure/document_capture_controller_spec.rb | 4 ---- .../doc_auth/socure/requests/document_request_spec.rb | 2 -- 6 files changed, 1 insertion(+), 16 deletions(-) diff --git a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb index 4cbf8f44fdf..5368b620ca8 100644 --- a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb @@ -19,7 +19,6 @@ def show # document request document_request = DocAuth::Socure::Requests::DocumentRequest.new( - document_capture_session_uuid: document_capture_session_uuid, redirect_url: idv_hybrid_mobile_socure_document_capture_url, language: I18n.locale, ) diff --git a/app/controllers/idv/socure/document_capture_controller.rb b/app/controllers/idv/socure/document_capture_controller.rb index 805b92c3851..acf6191b133 100644 --- a/app/controllers/idv/socure/document_capture_controller.rb +++ b/app/controllers/idv/socure/document_capture_controller.rb @@ -27,7 +27,6 @@ def show # document request document_request = DocAuth::Socure::Requests::DocumentRequest.new( - document_capture_session_uuid: document_capture_session_uuid, redirect_url: idv_socure_document_capture_update_url, language: I18n.locale, ) diff --git a/app/services/doc_auth/socure/requests/document_request.rb b/app/services/doc_auth/socure/requests/document_request.rb index 4a6f5b0920f..5121e72c577 100644 --- a/app/services/doc_auth/socure/requests/document_request.rb +++ b/app/services/doc_auth/socure/requests/document_request.rb @@ -4,15 +4,13 @@ module DocAuth module Socure module Requests class DocumentRequest < DocAuth::Socure::Request - attr_reader :document_type, :redirect_url, :document_capture_session_uuid, :language + attr_reader :document_type, :redirect_url, :language def initialize( - document_capture_session_uuid:, redirect_url:, language:, document_type: 'license' ) - @document_capture_session_uuid = document_capture_session_uuid @redirect_url = redirect_url @document_type = document_type @language = language @@ -39,7 +37,6 @@ def body redirect: redirect, language: lang(language), }, - customerUserId: document_capture_session_uuid, }.to_json end diff --git a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb index d87e5061368..427b42a7a7f 100644 --- a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb @@ -80,7 +80,6 @@ referenceId: '123ab45d-2e34-46f3-8d17-6f540ae90303', data: { eventId: 'zoYgIxEZUbXBoocYAnbb5DrT', - customerUserId: document_capture_session_uuid, docvTransactionToken: docv_transaction_token, qrCode: 'data:image/png;base64,iVBO......K5CYII=', url: socure_capture_app_url, @@ -98,7 +97,6 @@ it 'creates a DocumentRequest' do expect(request_class).to have_received(:new). with( - document_capture_session_uuid: document_capture_session_uuid, redirect_url: idv_hybrid_mobile_socure_document_capture_url, language: expected_language, ) @@ -125,7 +123,6 @@ }, language: expected_language, }, - customerUserId: document_capture_session_uuid, }, ), ) @@ -148,7 +145,6 @@ }, language: 'zh-cn', }, - customerUserId: document_capture_session_uuid, }, ), ) diff --git a/spec/controllers/idv/socure/document_capture_controller_spec.rb b/spec/controllers/idv/socure/document_capture_controller_spec.rb index 6ecf9d88f05..93db87d1ca9 100644 --- a/spec/controllers/idv/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/socure/document_capture_controller_spec.rb @@ -93,7 +93,6 @@ referenceId: '123ab45d-2e34-46f3-8d17-6f540ae90303', data: { eventId: 'zoYgIxEZUbXBoocYAnbb5DrT', - customerUserId: '121212', docvTransactionToken: docv_transaction_token, qrCode: 'data:image/png;base64,iVBO......K5CYII=', url: socure_capture_app_url, @@ -111,7 +110,6 @@ it 'creates a DocumentRequest' do expect(request_class).to have_received(:new). with( - document_capture_session_uuid: expected_uuid, redirect_url: idv_socure_document_capture_update_url, language: expected_language, ) @@ -138,7 +136,6 @@ }, language: :en, }, - customerUserId: expected_uuid, }, ), ) @@ -161,7 +158,6 @@ }, language: 'zh-cn', }, - customerUserId: expected_uuid, }, ), ) diff --git a/spec/services/doc_auth/socure/requests/document_request_spec.rb b/spec/services/doc_auth/socure/requests/document_request_spec.rb index 17c8032118b..6635c856550 100644 --- a/spec/services/doc_auth/socure/requests/document_request_spec.rb +++ b/spec/services/doc_auth/socure/requests/document_request_spec.rb @@ -7,7 +7,6 @@ subject(:document_request) do described_class.new( - document_capture_session_uuid:, redirect_url: redirect_url, language:, ) @@ -43,7 +42,6 @@ }, language: language, }, - customerUserId: document_capture_session_uuid, } end let(:fake_socure_status) { 200 } From b3da78f85762be98c54b894b56e90c6801ba525a Mon Sep 17 00:00:00 2001 From: KeithNava <134446588+KeithNava@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:03:44 -0500 Subject: [PATCH 14/23] LG-11857: add header post office search results (#11424) * feat: add tests for new heading in InPersonLocations component * feat: add resultsSectionHeadingComponent prop to InPersonLocation and FullAddressSearch * feat: add tests for FullAddressSearch new ResultsSectionHeading * changelog: User-Facing Improvements, In-person proofing, add optional results section heading pro for FullAddressSearch component * Update app/javascript/packages/address-search/components/full-address-search.spec.tsx Co-authored-by: Andrew Duthie <1779930+aduth@users.noreply.github.com> * changelog: User-Facing Improvements, In-person proofing, add optional results section heading pro for FullAddressSearch component * feat: remove waitFor in the full-address-search test * feat: updated changelog * feat: add expect to assertion --------- Co-authored-by: Andrew Duthie <1779930+aduth@users.noreply.github.com> --- .../packages/address-search/CHANGELOG.md | 4 ++ .../components/full-address-search.spec.tsx | 53 +++++++++++++++++++ .../components/full-address-search.tsx | 2 + .../components/in-person-locations.spec.tsx | 17 ++++++ .../components/in-person-locations.tsx | 2 + .../packages/address-search/package.json | 4 +- .../packages/address-search/types.d.ts | 2 + 7 files changed, 82 insertions(+), 2 deletions(-) diff --git a/app/javascript/packages/address-search/CHANGELOG.md b/app/javascript/packages/address-search/CHANGELOG.md index ee448b8734e..805793d16b8 100644 --- a/app/javascript/packages/address-search/CHANGELOG.md +++ b/app/javascript/packages/address-search/CHANGELOG.md @@ -1,5 +1,9 @@ # `Change Log` +## v3.4.0 (2024-11-07) + +- Added new optional resultsSectionHeading component prop to FullAddressSearch + ## v3.3.0 (2024-11-05) - Remove obsolete `is_pilot` field from PostOffice and FormattedLocation types diff --git a/app/javascript/packages/address-search/components/full-address-search.spec.tsx b/app/javascript/packages/address-search/components/full-address-search.spec.tsx index a3a7ff018de..0ec986da2c6 100644 --- a/app/javascript/packages/address-search/components/full-address-search.spec.tsx +++ b/app/javascript/packages/address-search/components/full-address-search.spec.tsx @@ -320,4 +320,57 @@ describe('FullAddressSearch', () => { await expect(handleLocationsFound).to.eventually.be.called(); }); }); + + context('Address Search with Results Section Heading', () => { + let server: SetupServer; + before(() => { + server = setupServer( + http.post(locationsURL, () => HttpResponse.json([{ name: 'Baltimore' }])), + ); + server.listen(); + }); + + after(() => { + server.close(); + }); + + it('renders the results section heading when passed in', async () => { + const handleLocationsFound = sandbox.stub(); + const onSelect = sinon.stub(); + const resultsSectionHeadingText = 'Mock Heading'; + const { findByText, getByLabelText, getByText } = render( + new Map() }}> + undefined} + handleLocationSelect={onSelect} + disabled={false} + resultsSectionHeading={() =>

{resultsSectionHeadingText}

} + /> +
, + ); + + await userEvent.type( + getByLabelText('in_person_proofing.body.location.po_search.address_label'), + '200 main', + ); + await userEvent.type( + getByLabelText('in_person_proofing.body.location.po_search.city_label'), + 'Endeavor', + ); + await userEvent.selectOptions( + getByLabelText('in_person_proofing.body.location.po_search.state_label'), + 'DE', + ); + await userEvent.type( + getByLabelText('in_person_proofing.body.location.po_search.zipcode_label'), + '17201', + ); + await userEvent.click(getByText('in_person_proofing.body.location.po_search.search_button')); + + expect(await findByText(resultsSectionHeadingText)).to.exist(); + }); + }); }); diff --git a/app/javascript/packages/address-search/components/full-address-search.tsx b/app/javascript/packages/address-search/components/full-address-search.tsx index ab2dc67b402..0a6f07bd4f5 100644 --- a/app/javascript/packages/address-search/components/full-address-search.tsx +++ b/app/javascript/packages/address-search/components/full-address-search.tsx @@ -15,6 +15,7 @@ function FullAddressSearch({ registerField, resultsHeaderComponent, usStatesTerritories, + resultsSectionHeading, }: FullAddressSearchProps) { const [apiError, setApiError] = useState(null); const [foundAddress, setFoundAddress] = useState(null); @@ -61,6 +62,7 @@ function FullAddressSearch({ address={foundAddress.address || ''} noInPersonLocationsDisplay={noInPersonLocationsDisplay} resultsHeaderComponent={resultsHeaderComponent} + resultsSectionHeading={resultsSectionHeading} /> )} diff --git a/app/javascript/packages/address-search/components/in-person-locations.spec.tsx b/app/javascript/packages/address-search/components/in-person-locations.spec.tsx index 195206b9f81..7dfdbbbd828 100644 --- a/app/javascript/packages/address-search/components/in-person-locations.spec.tsx +++ b/app/javascript/packages/address-search/components/in-person-locations.spec.tsx @@ -88,6 +88,23 @@ describe('InPersonLocations', () => { ).to.not.exist(); }); + it('renders a header at the top of the results', () => { + const headingText = 'mock heading'; + const testId = 'mock-heading'; + const { getByTestId } = render( +

{headingText}

} + />, + ); + + expect(getByTestId(testId)).to.exist(); + expect(getByTestId(testId).textContent).to.equal(headingText); + }); + context('when no locations are found', () => { it('renders the passed in noLocations component w/ address', () => { const onClick = sinon.stub(); diff --git a/app/javascript/packages/address-search/components/in-person-locations.tsx b/app/javascript/packages/address-search/components/in-person-locations.tsx index af2f807cbe7..36b65f63922 100644 --- a/app/javascript/packages/address-search/components/in-person-locations.tsx +++ b/app/javascript/packages/address-search/components/in-person-locations.tsx @@ -20,6 +20,7 @@ function InPersonLocations({ address, noInPersonLocationsDisplay: NoInPersonLocationsDisplay, resultsHeaderComponent: HeaderComponent, + resultsSectionHeading: ResultsSectionHeading, }: InPersonLocationsProps) { if (locations?.length === 0) { return ; @@ -27,6 +28,7 @@ function InPersonLocations({ return ( <> + {ResultsSectionHeading && }

{t('in_person_proofing.body.location.po_search.results_description', { address, diff --git a/app/javascript/packages/address-search/package.json b/app/javascript/packages/address-search/package.json index a02eebaee2d..1812b0240a0 100644 --- a/app/javascript/packages/address-search/package.json +++ b/app/javascript/packages/address-search/package.json @@ -1,6 +1,6 @@ { "name": "@18f/identity-address-search", - "version": "3.3.0", + "version": "3.4.0", "type": "module", "private": false, "files": [ @@ -34,4 +34,4 @@ "directory": "app/javascript/packages/address-search" }, "sideEffects": false -} +} \ No newline at end of file diff --git a/app/javascript/packages/address-search/types.d.ts b/app/javascript/packages/address-search/types.d.ts index d75c0b971be..b2b4f89d8cd 100644 --- a/app/javascript/packages/address-search/types.d.ts +++ b/app/javascript/packages/address-search/types.d.ts @@ -69,6 +69,7 @@ interface InPersonLocationsProps { noInPersonLocationsDisplay: ComponentType<{ address: string }>; onSelect; resultsHeaderComponent?: ComponentType; + resultsSectionHeading?: ComponentType; } interface LocationCollectionItemProps { @@ -98,4 +99,5 @@ interface FullAddressSearchProps { registerField: RegisterFieldCallback; resultsHeaderComponent?: ComponentType; usStatesTerritories: string[][]; + resultsSectionHeading?: ComponentType; } From 292840eed3a521329ecdc7d594e17a0a51e2fad3 Mon Sep 17 00:00:00 2001 From: Jenny Verdeyen Date: Wed, 13 Nov 2024 13:02:42 -0500 Subject: [PATCH 15/23] LG-14442: Add error handling and invalid character check to public usps locations controller (#11470) * Adding error handling and tests to public usps locations controller changelog: Internal, In-person Proofing, Adding graceful error handling and analytics in public usps locations controller --- .../public/usps_locations_controller.rb | 40 +++++++++++++ .../public/usps_locations_controller_spec.rb | 58 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/app/controllers/idv/in_person/public/usps_locations_controller.rb b/app/controllers/idv/in_person/public/usps_locations_controller.rb index b8f20a7f96d..9531141600c 100644 --- a/app/controllers/idv/in_person/public/usps_locations_controller.rb +++ b/app/controllers/idv/in_person/public/usps_locations_controller.rb @@ -3,15 +3,34 @@ module Idv module InPerson module Public + class UspsLocationsError < StandardError + def initialize + super('Unsupported characters in address field.') + end + end + class UspsLocationsController < ApplicationController skip_forgery_protection + include IppHelper + + rescue_from Faraday::Error, + StandardError, + UspsLocationsError, + Faraday::BadRequestError, + with: :handle_error + def index candidate = UspsInPersonProofing::Applicant.new( address: search_params['street_address'], city: search_params['city'], state: search_params['state'], zip_code: search_params['zip_code'] ) + + unless candidate.has_valid_address? + raise UspsLocationsError + end + locations = proofer.request_facilities(candidate, false) render json: localized_locations(locations).to_json @@ -34,6 +53,27 @@ def localized_locations(locations) end end + def handle_error(err) + remapped_error = case err + when ActionController::InvalidAuthenticityToken, + Faraday::Error, + UspsLocationsError + :unprocessable_entity + else + :internal_server_error + end + + analytics.idv_in_person_locations_request_failure( + api_status_code: Rack::Utils.status_code(remapped_error), + exception_class: err.class, + exception_message: scrub_message(err.message), + response_body_present: err.respond_to?(:response_body) && err.response_body.present?, + response_body: err.respond_to?(:response_body) && scrub_body(err.response_body), + response_status_code: err.respond_to?(:response_status) && err.response_status, + ) + render json: {}, status: remapped_error + end + def search_params params.require(:address).permit( :street_address, diff --git a/spec/controllers/idv/in_person/public/usps_locations_controller_spec.rb b/spec/controllers/idv/in_person/public/usps_locations_controller_spec.rb index 3559670d7b4..57b97ba4755 100644 --- a/spec/controllers/idv/in_person/public/usps_locations_controller_spec.rb +++ b/spec/controllers/idv/in_person/public/usps_locations_controller_spec.rb @@ -3,6 +3,10 @@ RSpec.describe Idv::InPerson::Public::UspsLocationsController do include Rails.application.routes.url_helpers + before do + stub_analytics + end + describe '#index' do subject(:action) do post :index, @@ -21,5 +25,59 @@ action expect(response).to be_ok end + + context 'with a 500 error from USPS' do + let(:server_error) { Faraday::ServerError.new } + let(:proofer) { double('Proofer') } + + before do + allow(UspsInPersonProofing::EnrollmentHelper).to receive(:usps_proofer).and_return(proofer) + allow(proofer).to receive(:request_facilities).and_raise(server_error) + end + + it 'returns an unprocessible entity client error' do + subject + expect(@analytics).to have_logged_event( + 'Request USPS IPP locations: request failed', + api_status_code: 422, + exception_class: server_error.class, + exception_message: server_error.message, + response_body_present: + server_error.response_body.present?, + ) + + status = response.status + expect(status).to eq 422 + end + end + + context 'address has unsupported characters' do + let(:locale) { nil } + let(:usps_locations_error) { Idv::InPerson::Public::UspsLocationsError.new } + + subject(:response) do + post :index, params: { locale: locale, + address: { street_address: '1600, Pennsylvania Ave', + city: 'Washington', + state: 'DC', + zip_code: '20500' } } + end + + it 'returns unprocessable entity' do + subject + + expect(@analytics).to have_logged_event( + 'Request USPS IPP locations: request failed', + api_status_code: 422, + exception_class: usps_locations_error.class, + exception_message: usps_locations_error.message, + response_body_present: false, + response_body: false, + response_status_code: false, + ) + + expect(response.status).to eq 422 + end + end end end From f727f964b4982ef32c5677cc68a2bf9c2694d4c6 Mon Sep 17 00:00:00 2001 From: Mitchell Henke Date: Wed, 13 Nov 2024 12:04:16 -0600 Subject: [PATCH 16/23] Remove review-app image build (#11501) changelog: Internal, Maintenance, Remove review-app image build --- .gitlab-ci.yml | 63 -------------------------------------------------- 1 file changed, 63 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 31f338017b9..9a360f93fb8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -93,56 +93,6 @@ install: # Build a container image async, and don't block CI tests # Cache intermediate images for 1 week (168 hours) -build-review-image: - stage: review - needs: [] - environment: - name: review/$CI_COMMIT_REF_NAME - interruptible: true - variables: - BRANCH_TAGGING_STRING: '' - rules: - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - variables: - BRANCH_TAGGING_STRING: '--destination ${ECR_REGISTRY}/identity-idp/review:main' - - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH - - if: $CI_PIPELINE_SOURCE != "merge_request_event" - when: never - tags: - - build-pool - image: - name: gcr.io/kaniko-project/executor:debug - entrypoint: [''] - script: - - mkdir -p /kaniko/.docker - - echo ${CI_ENVIRONMENT_SLUG} - - echo $CI_ENVIRONMENT_SLUG - - echo $CI_COMMIT_BRANCH - - echo $CI_COMMIT_SHA - - |- - KANIKOCFG="\"credsStore\":\"ecr-login\"" - if [ "x${http_proxy}" != "x" -o "x${https_proxy}" != "x" ]; then - KANIKOCFG="${KANIKOCFG}, \"proxies\": { \"default\": { \"httpProxy\": \"${http_proxy}\", \"httpsProxy\": \"${https_proxy}\", \"noProxy\": \"${no_proxy}\"}}" - fi - KANIKOCFG="{ ${KANIKOCFG} }" - echo "${KANIKOCFG}" > /kaniko/.docker/config.json - - >- - /kaniko/executor - --context "${CI_PROJECT_DIR}" - --dockerfile "${CI_PROJECT_DIR}/dockerfiles/idp_review_app.Dockerfile" - --destination "${ECR_REGISTRY}/identity-idp/review:${CI_COMMIT_SHA}" - ${BRANCH_TAGGING_STRING} - --cache-repo="${ECR_REGISTRY}/identity-idp/review/cache" - --cache-ttl=168h - --cache=true - --compressed-caching=false - --build-arg "http_proxy=${http_proxy}" - --build-arg "https_proxy=${https_proxy}" - --build-arg "no_proxy=${no_proxy}" - --build-arg "ARG_CI_ENVIRONMENT_SLUG=${CI_ENVIRONMENT_SLUG}" - --build-arg "ARG_CI_COMMIT_BRANCH=${CI_COMMIT_BRANCH}" - --build-arg "ARG_CI_COMMIT_SHA=${CI_COMMIT_SHA}" - build-idp-image: stage: review needs: [] @@ -672,19 +622,6 @@ secret_detection: # Export the automated ECR scan results into a format Gitlab can use # Report schema https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/container-scanning-report-format.json -ecr-scan-review-app: - extends: .container_scan_template - rules: - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH - - if: $CI_PIPELINE_SOURCE != "merge_request_event" - when: never - needs: - - job: build-review-image - stage: scan - variables: - ecr_repo: identity-idp/review - ecr-scan-ci: extends: .container_scan_template rules: From 9f9aaf8f78252be208b14d003b5f24a6652d644c Mon Sep 17 00:00:00 2001 From: Lauren George Date: Wed, 13 Nov 2024 14:13:34 -0500 Subject: [PATCH 17/23] LG-14464: Show warning CTA on ThreatMetrix API exception (#11459) * LG-14464: Show warning CTA on TMX API exception **Why** * The "internal error" view displayed during IdV is a last resort view. Prior to this change, this view was also shown when we received a ThreatMetrix API response that included an exception message. * Showing the internal error view for an unknown exception raised by the ThreatMetrix API is not useful for the subject undergoing proofing and it obfuscates the action that can be taken by either the proofing subject or the support agent investigating the identity resolution errors. **How** * Added a logic branch to the routing handling of `Idv::VerifyInfoConcern#idv_failure` that still preferences the known actionable error cases (i.e., ssn_failure, rate_limiter, etc.), which is eventually called by both `Idv::VerifyInfoController` and `Idv::InPerson::VerifyInfoController` 1. The first new case handles the ThreatMetrix API exception 2. The second new case handles when there is no exception, but the resolution check (e.g., InstantVerify) didn't pass. * Updated the Idv::VerifyInfoController spec to examine the expected routing and the expected shape of the ThreatMetrix API exception response. As the exception message is an arbitrary unstructured String, we do not test for specific values and instead ensure that the *shape* of the response in our analytics meets a minimum conformance in structure and value types. **Notes** * This is a difficult to replicate error as it relies on blackbox behavior exhibited by our vendor APIs. changelog: Internal, IdV resolution, Error routing for vendor API exceptions Link to the relevant ticket: [LG-14464](https://cm-jira.usa.gov/browse/LG-14464) --- .../concerns/idv/verify_info_concern.rb | 23 +++- .../idv/verify_info_controller_spec.rb | 124 ++++++++++++++++++ .../idv/threat_metrix_pending_spec.rb | 6 +- 3 files changed, 148 insertions(+), 5 deletions(-) diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index 78add678c47..297ac4a2a95 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -79,12 +79,27 @@ def ssn_rate_limiter def idv_failure(result) proofing_results_exception = result.extra.dig(:proofing_results, :exception) + has_exception = proofing_results_exception.present? is_mva_exception = result.extra.dig( :proofing_results, :context, :stages, :state_id, :mva_exception, + ).present? + is_threatmetrix_exception = result.extra.dig( + :proofing_results, + :context, + :stages, + :threatmetrix, + :exception, + ).present? + resolution_failed = !result.extra.dig( + :proofing_results, + :context, + :stages, + :resolution, + :success, ) if ssn_rate_limiter.limited? @@ -93,10 +108,14 @@ def idv_failure(result) elsif resolution_rate_limiter.limited? idv_failure_log_rate_limited(:idv_resolution) redirect_to rate_limited_url - elsif proofing_results_exception.present? && is_mva_exception + elsif has_exception && is_mva_exception idv_failure_log_warning redirect_to state_id_warning_url - elsif proofing_results_exception.present? + elsif (has_exception && is_threatmetrix_exception) || + (!has_exception && resolution_failed) + idv_failure_log_warning + redirect_to warning_url + elsif has_exception idv_failure_log_error redirect_to exception_url else diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index 50351d84b11..5b8e6e758c0 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -4,6 +4,7 @@ include FlowPolicyHelper let(:user) { create(:user) } + let(:analytics_hash) do { analytics_id: 'Doc Auth', @@ -144,6 +145,7 @@ context 'when proofing_device_profiling is enabled' do let(:threatmetrix_client_id) { 'threatmetrix_client' } let(:review_status) { 'pass' } + let(:idv_result) do { context: { @@ -253,6 +255,69 @@ end end + context 'when there is a threatmetrix exception' do + let(:review_status) { nil } + + let(:idv_result) do + { + context: { + device_profiling_adjudication_reason: 'device_profiling_exception', + errors: {}, + stages: { + threatmetrix: { + client: nil, + errors: {}, + exception: "Unexpected ThreatMetrix review_status value: #{review_status}", + response_body: nil, + review_status:, + success: false, + transaction_id: nil, + }, + }, + }, + success: false, + } + end + + it 'sets the review status in the idv session' do + get :show + expect(controller.idv_session.threatmetrix_review_status).to be_nil + end + + it 'redirects to warning_url' do + get :show + + expect(response).to redirect_to idv_session_errors_warning_url + + expect(@analytics).to have_logged_event( + 'IdV: doc auth warning visited', + step_name: 'verify_info', + remaining_submit_attempts: kind_of(Integer), + ) + end + + it 'logs the analytics event with the device profiling exception' do + get :show + + expect(@analytics).to have_logged_event( + 'IdV: doc auth verify proofing results', + hash_including( + success: false, + proofing_results: hash_including( + context: hash_including( + device_profiling_adjudication_reason: 'device_profiling_exception', + stages: hash_including( + threatmetrix: hash_including( + exception: match(/\S+/), + ), + ), + ), + ), + ), + ) + end + end + context 'when threatmetrix response is Reject' do let(:review_status) { 'reject' } @@ -427,6 +492,65 @@ end end + context 'when the resolution proofing job fails and there is no exception' do + before do + allow(controller).to receive(:load_async_state).and_return(async_state) + end + + let(:document_capture_session) do + DocumentCaptureSession.create(user:) + end + + let(:async_state) do + # Here we're trying to match the store to redis -> read from redis flow this data travels + adjudicated_result = Proofing::Resolution::ResultAdjudicator.new( + state_id_result: Proofing::StateIdResult.new( + success: true, + errors: {}, + exception: nil, + vendor_name: :aamva, + transaction_id: 'abc123', + verified_attributes: [], + ), + device_profiling_result: Proofing::DdpResult.new(success: true), + ipp_enrollment_in_progress: true, + residential_resolution_result: Proofing::Resolution::Result.new(success: true), + resolution_result: Proofing::Resolution::Result.new( + success: false, + errors: { + base: [ + "Verification failed with code: 'priority.scoring.model.verification.fail'", + ], + }, + ), + same_address_as_id: true, + should_proof_state_id: true, + applicant_pii: Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN, + ).adjudicated_result.to_h + + document_capture_session.create_proofing_session + + document_capture_session.store_proofing_result(adjudicated_result) + + document_capture_session.load_proofing_result + end + + it 'renders the warning page' do + get :show + expect(response).to redirect_to(idv_session_errors_warning_url) + end + + it 'logs an event' do + get :show + + expect(@analytics).to have_logged_event( + 'IdV: doc auth warning visited', + step_name: 'verify_info', + remaining_submit_attempts: kind_of(Numeric), + ) + end + end + context 'when the resolution proofing job has not completed' do let(:async_state) do ProofingSessionAsyncResult.new(status: ProofingSessionAsyncResult::IN_PROGRESS) diff --git a/spec/features/idv/threat_metrix_pending_spec.rb b/spec/features/idv/threat_metrix_pending_spec.rb index 43686859d25..3bd790c6fd0 100644 --- a/spec/features/idv/threat_metrix_pending_spec.rb +++ b/spec/features/idv/threat_metrix_pending_spec.rb @@ -104,7 +104,7 @@ end end - scenario 'users pending ThreatMetrix No Result, it results in an error', :js do + scenario 'users pending ThreatMetrix No Result, it results in an error but shows warning', :js do freeze_time do user = create(:user, :fully_registered) visit_idp_from_ial1_oidc_sp( @@ -117,8 +117,8 @@ complete_ssn_step complete_verify_step - expect(page).to have_content(t('idv.failure.sessions.exception')) - expect(page).to have_current_path(idv_session_errors_exception_path) + expect(page).to have_content(t('idv.failure.sessions.warning')) + expect(page).to have_current_path(idv_session_errors_warning_path) end end From 6203407fcc77d1506795754d25735005edcb7d0f Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:58:10 -0500 Subject: [PATCH 18/23] Improve race condition handling for slow reCAPTCHA load (#11451) * Improve race condition handling for slow reCAPTCHA load changelog: Internal, reCAPTCHA, Improve race condition handling for slow reCAPTCHA load * Add comment explainer for constant * Format numeric constant of thousands * Improve interim loader script state handling * Fix grammer "once loads" -> "once loaded" --- .../captcha-submit-button-element.spec.ts | 79 +++++++++++++++---- .../captcha-submit-button-element.ts | 29 ++++++- 2 files changed, 92 insertions(+), 16 deletions(-) diff --git a/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.spec.ts b/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.spec.ts index f0bdece5bb7..b8bc9bf6f1b 100644 --- a/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.spec.ts +++ b/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.spec.ts @@ -1,17 +1,20 @@ import quibble from 'quibble'; import type { SinonStub } from 'sinon'; -import userEvent from '@testing-library/user-event'; +import baseUserEvent from '@testing-library/user-event'; import { screen, waitFor, fireEvent } from '@testing-library/dom'; import { useSandbox, useDefineProperty } from '@18f/identity-test-helpers'; import '@18f/identity-spinner-button/spinner-button-element'; describe('CaptchaSubmitButtonElement', () => { - const sandbox = useSandbox(); + let FAILED_LOAD_DELAY_MS: number; + const sandbox = useSandbox({ useFakeTimers: true }); + const { clock } = sandbox; + const userEvent = baseUserEvent.setup({ advanceTimers: clock.tick }); const trackError = sandbox.stub(); before(async () => { quibble('@18f/identity-analytics', { trackError }); - await import('./captcha-submit-button-element'); + ({ FAILED_LOAD_DELAY_MS } = await import('./captcha-submit-button-element')); }); afterEach(() => { @@ -117,7 +120,6 @@ describe('CaptchaSubmitButtonElement', () => { await userEvent.click(button); await waitFor(() => expect((form.submit as SinonStub).called).to.be.true()); - expect(grecaptcha.ready).to.have.been.called(); expect(grecaptcha.execute).to.have.been.calledWith(RECAPTCHA_SITE_KEY, { action: RECAPTCHA_ACTION_NAME, }); @@ -126,6 +128,57 @@ describe('CaptchaSubmitButtonElement', () => { }); }); + context('with recaptcha not loaded by time of submission', () => { + beforeEach(() => { + delete (global as any).grecaptcha; + }); + + it('enqueues the challenge callback to be run once recaptcha loads', async () => { + const button = screen.getByRole('button', { name: 'Submit' }); + const form = document.querySelector('form')!; + sandbox.stub(form, 'submit'); + + await userEvent.click(button); + + expect(form.submit).not.to.have.been.called(); + /* eslint-disable no-underscore-dangle */ + expect((globalThis as any).___grecaptcha_cfg).to.have.keys('fns'); + expect((globalThis as any).___grecaptcha_cfg.fns) + .to.be.an('array') + .with.lengthOf.greaterThan(0); + (globalThis as any).___grecaptcha_cfg.fns.forEach((callback) => callback()); + /* eslint-enable no-underscore-dangle */ + + await expect(form.submit).to.eventually.be.called(); + }); + }); + + context('with only recaptcha loader script loaded by time of submission', () => { + // The loader script will define the `grecaptcha` global and `ready` function, but it will + // not define `execute`. + beforeEach(() => { + delete (global as any).grecaptcha.execute; + delete (global as any).grecaptcha.enterprise.execute; + }); + + it('enqueues the challenge callback to be run once recaptcha loads', async () => { + const button = screen.getByRole('button', { name: 'Submit' }); + const form = document.querySelector('form')!; + sandbox.stub(form, 'submit'); + + await userEvent.click(button); + + expect(grecaptcha.ready).to.have.been.called(); + + // Simulate reCAPTCHA full script loaded + (global as any).grecaptcha.execute = sandbox.stub().resolves(RECAPTCHA_TOKEN_VALUE); + const callback = (grecaptcha.ready as SinonStub).getCall(0).args[0]; + callback(); + + await expect(form.submit).to.eventually.be.called(); + }); + }); + context('with recaptcha enterprise', () => { beforeEach(() => { const element = document.querySelector('lg-captcha-submit-button')!; @@ -141,7 +194,6 @@ describe('CaptchaSubmitButtonElement', () => { await userEvent.click(button); await waitFor(() => expect((form.submit as SinonStub).called).to.be.true()); - expect(grecaptcha.enterprise.ready).to.have.been.called(); expect(grecaptcha.enterprise.execute).to.have.been.calledWith(RECAPTCHA_SITE_KEY, { action: RECAPTCHA_ACTION_NAME, }); @@ -156,19 +208,18 @@ describe('CaptchaSubmitButtonElement', () => { delete (global as any).grecaptcha; }); - it('does not prevent default form submission', async () => { + it('submits the form if recaptcha is still not loaded after reasonable delay', async () => { const button = screen.getByRole('button', { name: 'Submit' }); const form = document.querySelector('form')!; - - let didSubmit = false; - form.addEventListener('submit', (event) => { - expect(event.defaultPrevented).to.equal(false); - event.preventDefault(); - didSubmit = true; - }); + sandbox.stub(form, 'submit'); await userEvent.click(button); - await waitFor(() => expect(didSubmit).to.be.true()); + + expect(form.submit).not.to.have.been.called(); + clock.tick(FAILED_LOAD_DELAY_MS - 1); + expect(form.submit).not.to.have.been.called(); + clock.tick(1); + expect(form.submit).to.have.been.called(); }); }); diff --git a/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts b/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts index 6177b63732a..6017da75fb7 100644 --- a/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts +++ b/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts @@ -1,5 +1,11 @@ import { trackError } from '@18f/identity-analytics'; +/** + * Maximum time (in milliseconds) to wait on reCAPTCHA to finish loading once a form is submitted + * before considering reCAPTCHA as having failed to load. + */ +export const FAILED_LOAD_DELAY_MS = 5_000; + class CaptchaSubmitButtonElement extends HTMLElement { form: HTMLFormElement | null; @@ -46,7 +52,7 @@ class CaptchaSubmitButtonElement extends HTMLElement { } invokeChallenge() { - this.recaptchaClient!.ready(async () => { + this.#onReady(async () => { const { recaptchaSiteKey: siteKey, recaptchaAction: action } = this; let token; @@ -62,7 +68,7 @@ class CaptchaSubmitButtonElement extends HTMLElement { } shouldInvokeChallenge(): boolean { - return !!(this.recaptchaSiteKey && this.recaptchaClient); + return !!this.recaptchaSiteKey; } handleFormSubmit = (event: SubmitEvent) => { @@ -71,6 +77,25 @@ class CaptchaSubmitButtonElement extends HTMLElement { this.invokeChallenge(); } }; + + #onReady(callback: Parameters[0]) { + if (this.recaptchaClient) { + this.recaptchaClient.ready(callback); + } else { + // If reCAPTCHA hasn't finished loading by the time the form is submitted, we can enqueue the + // callback to be invoked once loaded by appending a callback to the ___grecaptcha_cfg global. + // + // See: https://developers.google.com/recaptcha/docs/loading + + const failedLoadTimeoutId = setTimeout(() => this.submit(), FAILED_LOAD_DELAY_MS); + const clearFailedLoadTimeout = () => clearTimeout(failedLoadTimeoutId); + + /* eslint-disable no-underscore-dangle */ + globalThis.___grecaptcha_cfg ??= { fns: [] }; + globalThis.___grecaptcha_cfg.fns.push(clearFailedLoadTimeout, callback); + /* eslint-enable no-underscore-dangle */ + } + } } declare global { From 9f0b5dff506f109992aff57e4a8006c83e567632 Mon Sep 17 00:00:00 2001 From: Alex Bradley Date: Wed, 13 Nov 2024 16:27:13 -0500 Subject: [PATCH 19/23] LG-14810 Users only see "Use your phone to take photos" for Socure (#11464) * add desktop test mode to determine if upload is disabled * check for socure doc auth vendor * add changelog changelog: Internal, Doc Auth Socure, configure upload_disabled for socure * change some logic with upload_disabled? * add tests to hybrid_handoff feature spec * linty mclinterson * test mode to desktop_selfie_test_mode * lint * Update spec/features/idv/doc_auth/hybrid_handoff_spec.rb Co-authored-by: Amir Reavis-Bey --------- Co-authored-by: Amir Reavis-Bey --- .../idv/hybrid_handoff_controller.rb | 11 ++++-- .../idv/doc_auth/hybrid_handoff_spec.rb | 35 +++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index 0702bf9b829..8d41ee040c9 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -5,6 +5,7 @@ class HybridHandoffController < ApplicationController include Idv::AvailabilityConcern include ActionView::Helpers::DateHelper include IdvStepConcern + include DocAuthVendorConcern include StepIndicatorConcern before_action :confirm_not_rate_limited @@ -12,8 +13,7 @@ class HybridHandoffController < ApplicationController before_action :confirm_hybrid_handoff_needed, only: :show def show - @upload_disabled = idv_session.selfie_check_required && - !idv_session.desktop_selfie_test_mode_enabled? + @upload_disabled = upload_disabled? @direct_ipp_with_selfie_enabled = IdentityConfig.store.in_person_doc_auth_button_enabled && Idv::InPersonConfig.enabled_for_issuer?( @@ -74,6 +74,8 @@ def self.step_info ) end + private + def handle_phone_submission return rate_limited_failure if rate_limiter.limited? rate_limiter.increment! @@ -120,6 +122,11 @@ def sp_or_app_name current_sp&.friendly_name.presence || APP_NAME end + def upload_disabled? + (doc_auth_vendor == Idp::Constants::Vendors::SOCURE || idv_session.selfie_check_required) && + !idv_session.desktop_selfie_test_mode_enabled? + end + def build_telephony_form_response(telephony_result) FormResponse.new( success: telephony_result.success?, diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb index 437db0408f3..ed52c7441fa 100644 --- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb +++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb @@ -315,11 +315,14 @@ def verify_no_upload_photos_section_and_link(page) expect(page).to_not have_content(t('doc_auth.headings.upload_from_computer')) end - context 'on a desktop device with various ipp and selfie configuration' do + context 'on a desktop device with various ipp, socure, and selfie configuration' do let(:in_person_proofing_enabled) { true } let(:sp_ipp_enabled) { true } let(:in_person_proofing_opt_in_enabled) { true } let(:facial_match_required) { true } + let(:socure_enabled) { false } + let(:doc_auth_vendor) { Idp::Constants::Vendors::MOCK } + let(:desktop_test_mode_enabled) { false } let(:user) { user_with_2fa } before do @@ -328,7 +331,10 @@ def verify_no_upload_photos_section_and_link(page) service_provider.in_person_proofing_enabled = false service_provider.save! end - allow(IdentityConfig.store).to receive(:doc_auth_selfie_desktop_test_mode).and_return(false) + allow(IdentityConfig.store).to receive(:socure_enabled).and_return(socure_enabled) + allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return(doc_auth_vendor) + allow(IdentityConfig.store).to receive(:doc_auth_selfie_desktop_test_mode). + and_return(desktop_test_mode_enabled) allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return( in_person_proofing_enabled, ) @@ -351,6 +357,31 @@ def verify_no_upload_photos_section_and_link(page) complete_doc_auth_steps_before_agreement_step complete_agreement_step end + + context 'when socure is the doc auth vendor' do + let(:facial_match_required) { false } + let(:in_person_proofing_opt_in_enabled) { false } + let(:sp_ipp_enabled) { false } + let(:socure_enabled) { true } + let(:doc_auth_vendor) { Idp::Constants::Vendors::SOCURE } + + context 'when socure desktop test mode is not enabled' do + it 'shows phone only top content no upload section' do + verify_handoff_page_non_selfie_version_content(page) + verify_no_upload_photos_section_and_link(page) + end + end + + context 'when socure desktop test mode is enabled' do + let(:desktop_test_mode_enabled) { true } + + it 'shows phone top content and desktop upload content' do + verify_handoff_page_non_selfie_version_content(page) + expect(page).to have_content(t('doc_auth.headings.upload_from_computer')) + end + end + end + context 'when ipp is available system wide' do context 'when in person proofing opt in enabled' do context 'when sp ipp is available' do From 09bc97048dff6bc178776043c47b6246a097483e Mon Sep 17 00:00:00 2001 From: Mitchell Henke Date: Wed, 13 Nov 2024 15:48:59 -0600 Subject: [PATCH 20/23] Only export stats for tables with integer id columns (#11502) changelog: Bug Fixes, Data Warehouse, Only export stats for tables with integer id columns --- app/jobs/data_warehouse/table_summary_stats_export_job.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/jobs/data_warehouse/table_summary_stats_export_job.rb b/app/jobs/data_warehouse/table_summary_stats_export_job.rb index 3f74bb85198..7fef9cabee5 100644 --- a/app/jobs/data_warehouse/table_summary_stats_export_job.rb +++ b/app/jobs/data_warehouse/table_summary_stats_export_job.rb @@ -29,7 +29,9 @@ def max_ids_and_counts(timestamp) end def table_has_id_column?(table) - ActiveRecord::Base.connection.columns(table).map(&:name).include?('id') + ActiveRecord::Base.connection.columns(table).any? do |column| + column.name == 'id' && column.type == :integer + end end def fetch_max_id_and_count(table, timestamp) From e3e6f327f1b838a8f481c3418257a54ff7e7256f Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:54:15 -0500 Subject: [PATCH 21/23] Avoid setting reCAPTCHA token on failed execute (#11503) * Avoid setting reCAPTCHA token on failed execute changelog: Internal, Anti-Fraud, Avoid setting reCAPTCHA token on failed execute * Set token TypeScript type If this were set previously, we'd get TypeScript warnings on attempting to assign potentially-undefined value to input field * Remove unnecessary await --- .../captcha-submit-button-element.spec.ts | 3 +++ .../captcha-submit-button/captcha-submit-button-element.ts | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.spec.ts b/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.spec.ts index b8bc9bf6f1b..657a7548e4a 100644 --- a/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.spec.ts +++ b/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.spec.ts @@ -239,6 +239,9 @@ describe('CaptchaSubmitButtonElement', () => { await userEvent.click(button); await expect(form.submit).to.eventually.be.called(); + expect(Object.fromEntries(new window.FormData(form))).to.deep.equal({ + recaptcha_token: '', + }); }); it('tracks error', async () => { diff --git a/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts b/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts index 6017da75fb7..5c97855f1cd 100644 --- a/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts +++ b/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts @@ -55,14 +55,14 @@ class CaptchaSubmitButtonElement extends HTMLElement { this.#onReady(async () => { const { recaptchaSiteKey: siteKey, recaptchaAction: action } = this; - let token; + let token: string | undefined; try { token = await this.recaptchaClient!.execute(siteKey!, { action }); + this.tokenInput.value = token; } catch (error) { trackError(error, { errorId: 'recaptchaExecute' }); } - this.tokenInput.value = token; this.submit(); }); } From 218c65c1196376d055f8f2f9a7a212ba46e9c087 Mon Sep 17 00:00:00 2001 From: A Shukla Date: Wed, 13 Nov 2024 16:21:41 -0600 Subject: [PATCH 22/23] LG-14807 reset the socure docV url when the CaptureApp session has ended (#11498) * changelog: Upcoming Features, socure, reset socure docv url * fixing test and lint * simplfying test, resolving pr comments * adding contexts and tests to existing contexts * resolving pr comments * Redusing test prep before webhook session complete and expired calls --- app/controllers/socure_webhook_controller.rb | 9 +++ .../socure_webhook_controller_spec.rb | 58 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/app/controllers/socure_webhook_controller.rb b/app/controllers/socure_webhook_controller.rb index 1deb4a46e51..212f01e02e8 100644 --- a/app/controllers/socure_webhook_controller.rb +++ b/app/controllers/socure_webhook_controller.rb @@ -26,6 +26,8 @@ def process_webhook_event when 'DOCUMENTS_UPLOADED' increment_rate_limiter fetch_results + when 'SESSION_EXPIRED', 'SESSION_COMPLETE' + reset_docv_url end end @@ -94,6 +96,13 @@ def increment_rate_limiter # Logic to throw an error when no DocumentCaptureSession found will be done in ticket LG-14905 end + def reset_docv_url + if document_capture_session.present? + document_capture_session.socure_docv_capture_app_url = nil + document_capture_session.save + end + end + def document_capture_session @document_capture_session ||= DocumentCaptureSession.find_by( socure_docv_transaction_token: docv_transaction_token, diff --git a/spec/controllers/socure_webhook_controller_spec.rb b/spec/controllers/socure_webhook_controller_spec.rb index b4a02f11955..ac6ea6373fc 100644 --- a/spec/controllers/socure_webhook_controller_spec.rb +++ b/spec/controllers/socure_webhook_controller_spec.rb @@ -156,6 +156,14 @@ with(document_capture_session_uuid: dcs.uuid) end + it 'does not reset socure_docv_capture_app_url value' do + dcs = create(:document_capture_session, :socure) + webhook_body[:event][:docvTransactionToken] = dcs.socure_docv_transaction_token + post :create, params: webhook_body + dcs.reload + expect(dcs.socure_docv_capture_app_url).not_to be_nil + end + context 'when document capture session does not exist' do before do allow(NewRelic::Agent).to receive(:notice_error) @@ -197,6 +205,56 @@ expect(SocureDocvResultsJob).not_to have_received(:perform_later) end + + it 'resets socure_docv_capture_app_url to nil' do + dcs = create(:document_capture_session, :socure) + webhook_body[:event][:docvTransactionToken] = dcs.socure_docv_transaction_token + expect(dcs.socure_docv_capture_app_url). + not_to be_nil + post :create, params: webhook_body + dcs.reload + expect(dcs.socure_docv_capture_app_url).to be_nil + end + end + + context 'when SESSION_EXPIRED event received' do + let(:event_type) { 'SESSION_EXPIRED' } + + it 'does not increment rate limiter of user' do + dcs = create(:document_capture_session, :socure) + webhook_body[:event][:docvTransactionToken] = dcs.socure_docv_transaction_token + + i = 0 + while i < 4 + post :create, params: webhook_body + + rate_limiter = RateLimiter.new( + user: dcs.user, + rate_limit_type: :idv_doc_auth, + ) + expect(rate_limiter.attempts).to eq 0 + i += 1 + end + end + + it 'does not enqueue a SocureDocvResultsJob' do + dcs = create(:document_capture_session, :socure) + webhook_body[:event][:docvTransactionToken] = dcs.socure_docv_transaction_token + + post :create, params: webhook_body + + expect(SocureDocvResultsJob).not_to have_received(:perform_later) + end + + it 'resets socure_docv_capture_app_url to nil' do + dcs = create(:document_capture_session, :socure) + webhook_body[:event][:docvTransactionToken] = dcs.socure_docv_transaction_token + expect(dcs.socure_docv_capture_app_url). + not_to be_nil + post :create, params: webhook_body + dcs.reload + expect(dcs.socure_docv_capture_app_url).to be_nil + end end context 'when socure webhook disabled' do From 91563d6e1f76770b087d2c529ad83032b4176aa4 Mon Sep 17 00:00:00 2001 From: Tim Spencer Date: Wed, 13 Nov 2024 16:44:16 -0800 Subject: [PATCH 23/23] add nginx image and build (#11480) * add nginx image and build * changelog: Internal, Adding nginx image for k8s deployment * Adding changelog changelog: Internal, Adding nginx image for k8s deployment * generate self-signed certs here in the image * add openssl so we can gen certs * making sure CI pipeline doesn't break changelog: Internal, Containerization, Adding nginx image for k8s deployment * add nginx image to review app patch --- .gitlab-ci.yml | 51 ++++++++ dockerfiles/application.yaml | 3 + dockerfiles/nginx-prod.conf | 235 +++++++++++++++++++++++++++++++++++ dockerfiles/nginx.Dockerfile | 19 +++ dockerfiles/status-map.conf | 80 ++++++++++++ dockerfiles/update-ips.sh | 21 ++++ 6 files changed, 409 insertions(+) create mode 100644 dockerfiles/nginx-prod.conf create mode 100644 dockerfiles/nginx.Dockerfile create mode 100644 dockerfiles/status-map.conf create mode 100755 dockerfiles/update-ips.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9a360f93fb8..207557da566 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -145,6 +145,57 @@ build-idp-image: --build-arg "LARGE_FILES_USER=${LARGE_FILES_USER}" --build-arg "SERVICE_PROVIDERS_KEY=${SERVICE_PROVIDERS_KEY}" +build-nginx-image: + stage: review + needs: [] + interruptible: true + variables: + BRANCH_TAGGING_STRING: '' + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + variables: + BRANCH_TAGGING_STRING: '--destination ${ECR_REGISTRY}/identity-idp/nginx:main' + - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH + - if: $CI_PIPELINE_SOURCE != "merge_request_event" + when: never + tags: + - build-pool + image: + name: gcr.io/kaniko-project/executor:debug + entrypoint: [''] + script: + - mkdir -p /kaniko/.docker + - echo ${CI_ENVIRONMENT_SLUG} + - echo $CI_ENVIRONMENT_SLUG + - echo $CI_COMMIT_BRANCH + - echo $CI_COMMIT_SHA + - |- + KANIKOCFG="\"credsStore\":\"ecr-login\"" + if [ "x${http_proxy}" != "x" -o "x${https_proxy}" != "x" ]; then + KANIKOCFG="${KANIKOCFG}, \"proxies\": { \"default\": { \"httpProxy\": \"${http_proxy}\", \"httpsProxy\": \"${https_proxy}\", \"noProxy\": \"${no_proxy}\"}}" + fi + KANIKOCFG="{ ${KANIKOCFG} }" + echo "${KANIKOCFG}" > /kaniko/.docker/config.json + - >- + /kaniko/executor + --context "${CI_PROJECT_DIR}" + --dockerfile "${CI_PROJECT_DIR}/dockerfiles/nginx.Dockerfile" + --destination "${ECR_REGISTRY}/identity-idp/nginx:${CI_COMMIT_SHA}" + ${BRANCH_TAGGING_STRING} + --cache-repo="${ECR_REGISTRY}/identity-idp/idp/cache" + --cache-ttl=168h + --cache=true + --snapshot-mode=redo + --compressed-caching=false + --build-arg "http_proxy=${http_proxy}" + --build-arg "https_proxy=${https_proxy}" + --build-arg "no_proxy=${no_proxy}" + --build-arg "ARG_CI_ENVIRONMENT_SLUG=${CI_ENVIRONMENT_SLUG}" + --build-arg "ARG_CI_COMMIT_BRANCH=${CI_COMMIT_BRANCH}" + --build-arg "ARG_CI_COMMIT_SHA=${CI_COMMIT_SHA}" + --build-arg "LARGE_FILES_TOKEN=${LARGE_FILES_TOKEN}" + --build-arg "LARGE_FILES_USER=${LARGE_FILES_USER}" + --build-arg "SERVICE_PROVIDERS_KEY=${SERVICE_PROVIDERS_KEY}" check_changelog: stage: test diff --git a/dockerfiles/application.yaml b/dockerfiles/application.yaml index 9803a90beb8..67ad0b394b4 100644 --- a/dockerfiles/application.yaml +++ b/dockerfiles/application.yaml @@ -489,6 +489,9 @@ spec: patch: |- - op: replace path: /spec/template/spec/containers/0/image + value: {{ECR_REGISTRY}}/identity-idp/nginx:{{IDP_CONTAINER_TAG}} + - op: replace + path: /spec/template/spec/containers/1/image value: {{ECR_REGISTRY}}/identity-idp/idp:{{IDP_CONTAINER_TAG}} - op: replace path: /spec/template/spec/containers/0/imagePullPolicy diff --git a/dockerfiles/nginx-prod.conf b/dockerfiles/nginx-prod.conf new file mode 100644 index 00000000000..e5b041464ac --- /dev/null +++ b/dockerfiles/nginx-prod.conf @@ -0,0 +1,235 @@ +# user nginx; +worker_processes 2; +worker_rlimit_nofile 2048; +pid /var/run/nginx.pid; +daemon off; +load_module /usr/lib/nginx/modules/ngx_http_headers_more_filter_module.so; + + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + sendfile on; + tcp_nopush off; + keepalive_timeout 60 50; + gzip on; + gzip_types text/plain text/css application/xml application/javascript application/json image/jpg image/jpeg image/png image/gif image/svg+xml font/woff2 woff2; + + # Timeouts definition + client_body_timeout 10; + client_header_timeout 10; + send_timeout 10; + # Set buffer size limits + client_body_buffer_size 1k; + client_header_buffer_size 1k; + client_max_body_size 20k; + large_client_header_buffers 2 20k; + # Limit connections + limit_conn addr 20; + limit_conn_status 429; + limit_conn_zone $binary_remote_addr zone=addr:5m; + # Disable sending server info and versions + server_tokens off; + more_clear_headers Server; + more_clear_headers X-Powered-By; + # Prevent clickJacking attack + add_header X-Frame-Options SAMEORIGIN; + # Disable content-type sniffing + add_header X-Content-Type-Options nosniff; + # Enable XSS filter + add_header X-XSS-Protection "1; mode=block"; + + # Enables nginx to check multiple set_real_ip_from lines + real_ip_recursive on; + + real_ip_header X-Forwarded-For; + + # Exclude all private IPv4 space from client source calculation when + # processing the X-Forewarded-For header + set_real_ip_from 10.0.0.0/8; + set_real_ip_from 100.64.0.0/10; + set_real_ip_from 172.16.0.0/12; + set_real_ip_from 192.168.0.0/16; + # TODO - IPv6 CIDR for VPCs will require autoconfiguration + + # Add CloudFront source address ranges to trusted CIDR range for real ip computation + include /etc/nginx/cloudfront-ips.conf; + + # logging + access_log /dev/stdout; + error_log /dev/stdout info; + + # Specify a key=value format useful for machine parsing + log_format kv escape=json + '{' + '"time": "$time_local", ' + '"hostname": "$host", ' + '"dest_port": "$server_port", ' + '"dest_ip": "$server_addr", ' + '"src": "$remote_addr", ' + '"src_ip": "$realip_remote_addr", ' + '"user": "$remote_user", ' + '"protocol": "$server_protocol", ' + '"http_method": "$request_method", ' + '"status": "$status", ' + '"bytes_out": "$body_bytes_sent", ' + '"bytes_in": "$request_length", ' + '"http_referer": "$http_referer", ' + '"http_user_agent": "$http_user_agent", ' + '"nginx_version": "$nginx_version", ' + '"http_cloudfront_viewer_address": "$http_cloudfront_viewer_address", ' + '"http_cloudfront_viewer_http_version": "$http_cloudfront_viewer_http_version", ' + '"http_cloudfront_viewer_tls": "$http_cloudfront_viewer_tls", ' + '"http_cloudfront_viewer_country": "$http_cloudfront_viewer_country", ' + '"http_cloudfront_viewer_country_region": "$http_cloudfront_viewer_country_region", ' + '"http_x_forwarded_for": "$http_x_forwarded_for", ' + '"http_x_amzn_trace_id": "$http_x_amzn_trace_id", ' + '"response_time": "$upstream_response_time", ' + '"request_time": "$request_time", ' + '"request": "$request", ' + '"tls_protocol": "$ssl_protocol", ' + '"tls_cipher": "$ssl_cipher", ' + '"uri_path": "$uri", ' + '"uri_query": "$query_string",' + '"log_filename": "nginx_access.log"' + '}'; + + # Get $status_reason variable, a human readable version of $status + include status-map.conf; + + # Set HSTS header only if not already set by app. Some clients get unhappy if + # you set multiple Strict-Transport-Security headers. + # https://serverfault.com/a/598106 + map $upstream_http_strict_transport_security $sts_value { + '' "max-age=31536000; preload"; + } + + # Always add a HSTS header - This is still inside the http block, so will not + # conflict with headers set in nginx.conf + add_header Strict-Transport-Security $sts_value always; + + server { + listen 8443 ssl; + server_name _; + access_log /dev/stdout kv; + + ssl_certificate /keys/localhost.crt; + ssl_certificate_key /keys/localhost.key; + ssl_verify_client optional_no_ca; # on; + ssl_verify_depth 10; + + ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; + ssl_prefer_server_ciphers on; + ssl_protocols TLSv1.2; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 5m; + ssl_stapling on; + ssl_stapling_verify on; + resolver_timeout 5s; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Host $host; + proxy_buffer_size 32k; + proxy_buffers 8 32k; + proxy_busy_buffers_size 64k; + + if ($request_method !~ ^(DELETE|GET|HEAD|OPTIONS|POST|PUT)$ ) { + return 405; + } + + # Content expiry rules + # Content pages set to no-cache + location ~* \.(?:manifest|appcache|html?|xml|json)$ { + expires -1; + proxy_pass https://0.0.0.0:3000; + } + + # Mutable assets set to no-cache + location ~* /AcuantImageProcessingWorker\.min\.js$ { + expires -1; + add_header Strict-Transport-Security $sts_value; + proxy_pass https://0.0.0.0:3000; + } + + # Media: images, icons, video, audio, HTC + location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ { + expires 1M; + add_header Strict-Transport-Security $sts_value; + add_header Cache-Control "public"; + proxy_pass https://0.0.0.0:3000; + } + + # CSS and Javascript + location ~* \.(?:css|js)$ { + expires 1M; + add_header Strict-Transport-Security $sts_value; + add_header Cache-Control "public"; + proxy_pass https://0.0.0.0:3000; + } + + # WebFonts + location ~* \.(?:ttf|ttc|otf|eot|woff|woff2)$ { + types {font/woff2 woff2;} + expires 1M; + add_header Strict-Transport-Security $sts_value; + add_header Access-Control-Allow-Origin https://$host; + add_header Access-Control-Allow-Methods 'GET'; + proxy_pass https://0.0.0.0:3000; + } + + # TODO - The following matches result in inconsistent headers! Should refactor to use include blocks + + ## Very large upload limits + + # Combined async image uploads - Require very large cap + location /api/verify/images { + add_header Strict-Transport-Security $sts_value; + client_max_body_size 30M; + proxy_pass https://0.0.0.0:3000; + } + + # Fallback (noscript) combined uploads + location ~ ^(/(en|es|fr))?/verify/(capture_doc|doc_auth)/document_capture { + add_header Strict-Transport-Security $sts_value; + client_max_body_size 30M; + proxy_pass https://0.0.0.0:3000; + } + + ## Large upload limits + + location ~ /api/v1/(facematch|liveness) { + add_header Strict-Transport-Security $sts_value; + client_max_body_size 10M; + proxy_pass https://0.0.0.0:3000; + } + + location ^~ /AssureIDService/Document/ { + add_header Strict-Transport-Security $sts_value; + client_max_body_size 10M; + proxy_pass https://0.0.0.0:3000; + } + + location ^~ /api/service_provider { + add_header Strict-Transport-Security $sts_value; + client_max_body_size 30k; + proxy_pass https://0.0.0.0:3000; + } + + location /service_providers { + add_header Strict-Transport-Security $sts_value; + client_max_body_size 2M; + error_page 413 =303 https://$host/413_logo.html; + proxy_pass https://0.0.0.0:3000; + } + + location / { + # Avoid ssl stripping attack + add_header Strict-Transport-Security $sts_value; + proxy_pass https://0.0.0.0:3000; + } + } +} diff --git a/dockerfiles/nginx.Dockerfile b/dockerfiles/nginx.Dockerfile new file mode 100644 index 00000000000..c96304c1ffa --- /dev/null +++ b/dockerfiles/nginx.Dockerfile @@ -0,0 +1,19 @@ +FROM public.ecr.aws/docker/library/alpine:3 + +RUN apk upgrade --no-cache +RUN apk add --no-cache jq curl nginx nginx-mod-http-headers-more openssl + +COPY ./dockerfiles/update-ips.sh /update-ips.sh +COPY ./dockerfiles/nginx-prod.conf /etc/nginx/nginx.conf +COPY ./dockerfiles/status-map.conf /etc/nginx/ +RUN /update-ips.sh + +# Generate and place SSL certificates for nginx (used only by ALB) +RUN mkdir /keys +RUN openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 1825 \ + -keyout /keys/localhost.key \ + -out /keys/localhost.crt \ + -subj "/C=US/ST=Fake/L=Fakerton/O=Dis/CN=localhost" && \ + chmod 644 /keys/localhost.key /keys/localhost.crt + +ENTRYPOINT ["/usr/sbin/nginx"] diff --git a/dockerfiles/status-map.conf b/dockerfiles/status-map.conf new file mode 100644 index 00000000000..2d08bec5ee6 --- /dev/null +++ b/dockerfiles/status-map.conf @@ -0,0 +1,80 @@ +# Create $status_reason, a human-friendly version of $status. +# This file must be included from inside an http { } block. +map $status $status_reason { + default "-"; + 100 "Continue"; + 101 "Switching Protocols"; + 102 "Processing"; + + 200 "OK"; + 201 "Created"; + 202 "Accepted"; + 203 "Non-Authoritative Information"; + 204 "No Content"; + 205 "Reset Content"; + 206 "Partial Content"; + 207 "Multi-Status"; + 208 "Already Reported"; + 226 "IM Used"; + + 300 "Multiple Choices"; + 301 "Moved Permanently"; + 302 "Found"; + 303 "See Other"; + 304 "Not Modified"; + 305 "Use Proxy"; + 306 "Switch Proxy"; + 307 "Temporary Redirect"; + 308 "Permanent Redirect"; + + 400 "Bad Request"; + 401 "Unauthorized"; + 402 "Payment Required"; + 403 "Forbidden"; + 404 "Not Found"; + 405 "Method Not Allowed"; + 406 "Not Acceptable"; + 407 "Proxy Authentication Required"; + 408 "Request Timeout"; + 409 "Conflict"; + 410 "Gone"; + 411 "Length Required"; + 412 "Precondition Failed"; + 413 "Payload Too Large"; + 414 "URI Too Long"; + 415 "Unsupported Media Type"; + 416 "Range Not Satisfiable"; + 417 "Expectation Failed"; + 418 "I'm A Teapot"; + 421 "Too Many Connections From This IP"; + 422 "Unprocessable Entity"; + 423 "Locked"; + 424 "Failed Dependency"; + 425 "Unordered Collection"; + 426 "Upgrade Required"; + 428 "Precondition Required"; + 429 "Too Many Requests"; + 431 "Request Header Fields Too Large"; + 449 "Retry With"; + 450 "Blocked By Windows Parental Controls"; + + # nginx + 444 "No Response"; + 495 "SSL Certificate Error"; + 496 "SSL Certificate Required"; + 497 "HTTP Request Sent to HTTPS Port"; + 499 "Client Closed Request"; + + 500 "Internal Server Error"; + 501 "Not Implemented"; + 502 "Bad Gateway"; + 503 "Service Unavailable"; + 504 "Gateway Timeout"; + 505 "HTTP Version Not Supported"; + 506 "Variant Also Negotiates"; + 507 "Insufficient Storage"; + 508 "Loop Detected"; + 509 "Bandwidth Limit Exceeded"; + 510 "Not Extended"; + 511 "Network Authentication Required"; +} diff --git a/dockerfiles/update-ips.sh b/dockerfiles/update-ips.sh new file mode 100755 index 00000000000..102c2ee8665 --- /dev/null +++ b/dockerfiles/update-ips.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# +# This script updates the ips.conf file so that we have +# up-to-date cloudfront IP information. +# +set -e + +IPS_CONF="/etc/nginx/cloudfront-ips.conf" +echo "Updating $IPS_CONF" + +rm -f "$IPS_CONF" +echo '# cloudfront IP ranges' > $IPS_CONF +echo '# ' >> $IPS_CONF + +curl -s https://ip-ranges.amazonaws.com/ip-ranges.json | jq -r '.prefixes[] | select(.service=="CLOUDFRONT_ORIGIN_FACING") | .ip_prefix' | while read i ; do + echo "set_real_ip_from $i;" >> $IPS_CONF +done + +curl -s https://ip-ranges.amazonaws.com/ip-ranges.json | jq -r '.ipv6_prefixes[] | select(.service=="CLOUDFRONT") | .ipv6_prefix' | while read i ; do + echo "set_real_ip_from $i;" >> $IPS_CONF +done