diff --git a/.gitattributes b/.gitattributes index f92f66167fa..9b5b818951e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -122,15 +122,16 @@ tsconfig.json jsonc # -------------------------------------------------------------------- # PROJECT-SPEFICIC RULES -# --------------------------------------------------------------------# +# --------------------------------------------------------------------# ## Lock files -knapsack_rspec_report.json lockfile package-lock.json lockfile pnpm-lock.yaml lockfile -### Force text diff for Gemfile.lock +### Force text diff for specific lockfiles Gemfile.lock diff +yarn.lock diff +knapsack_rspec_report.json jsonc ## Executables and runtimes *.wasm binary diff --git a/Makefile b/Makefile index a46dd25d3fd..eaa8c63d553 100644 --- a/Makefile +++ b/Makefile @@ -147,6 +147,8 @@ lint_gemfile_lock: Gemfile Gemfile.lock ## Lints the Gemfile and its lockfile lint_yarn_lock: package.json yarn.lock ## Lints the package.json and its lockfile @yarn install --ignore-scripts @(! git diff --name-only | grep yarn.lock) || (echo "Error: There are uncommitted changes after running 'yarn install'"; exit 1) + @yarn yarn-deduplicate + @(! git diff --name-only | grep yarn.lock) || (echo "Error: There are duplicate JS dependencies that were removed after running 'yarn yarn-deduplicate'"; exit 1) lint_lockfiles: lint_gemfile_lock lint_yarn_lock ## Lints to ensure lockfiles are in sync diff --git a/app/assets/images/email/real-id.png b/app/assets/images/email/real-id.png new file mode 100644 index 00000000000..51ee939e29d Binary files /dev/null and b/app/assets/images/email/real-id.png differ diff --git a/app/assets/images/email/state-id-and-fair-evidence-documents.png b/app/assets/images/email/state-id-and-fair-evidence-documents.png new file mode 100644 index 00000000000..269abdb95b8 Binary files /dev/null and b/app/assets/images/email/state-id-and-fair-evidence-documents.png differ diff --git a/app/assets/images/email/state-id-and-military-id.png b/app/assets/images/email/state-id-and-military-id.png new file mode 100644 index 00000000000..04691a5b205 Binary files /dev/null and b/app/assets/images/email/state-id-and-military-id.png differ diff --git a/app/assets/images/email/state-id-and-passport.png b/app/assets/images/email/state-id-and-passport.png new file mode 100644 index 00000000000..56f5dc4f7c6 Binary files /dev/null and b/app/assets/images/email/state-id-and-passport.png differ diff --git a/app/assets/stylesheets/email.css.scss b/app/assets/stylesheets/email.css.scss index 23df51c0b80..fd485da9ec4 100644 --- a/app/assets/stylesheets/email.css.scss +++ b/app/assets/stylesheets/email.css.scss @@ -177,8 +177,8 @@ h6 { @include u-border(1px); } -.border-top-width-0 { - border-top: 0; +.border-top-0 { + @include u-border-top(0); } .border-primary-light { diff --git a/app/components/alert_component.rb b/app/components/alert_component.rb index 18d55b3782a..10a5e73b58e 100644 --- a/app/components/alert_component.rb +++ b/app/components/alert_component.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true class AlertComponent < BaseComponent - VALID_TYPES = %i[info success warning error emergency other].freeze + VALID_TYPES = [nil, :info, :success, :warning, :error, :emergency].freeze attr_reader :type, :message, :tag_options, :text_tag - def initialize(type: :info, text_tag: 'p', message: nil, **tag_options) + def initialize(type: nil, text_tag: 'p', message: nil, **tag_options) if !VALID_TYPES.include?(type) raise ArgumentError, "`type` #{type} is invalid, expected one of #{VALID_TYPES}" end diff --git a/app/components/captcha_submit_button_component.html.erb b/app/components/captcha_submit_button_component.html.erb index 175257297aa..88e7033c4ac 100644 --- a/app/components/captcha_submit_button_component.html.erb +++ b/app/components/captcha_submit_button_component.html.erb @@ -25,13 +25,14 @@ <% end %> <% else %> - <%= f.input(:recaptcha_token, as: :hidden) %> + <%= f.input(:recaptcha_token, as: :hidden, input_html: { value: '' }) %> <% end %> <%= render SpinnerButtonComponent.new( action_message: t('components.captcha_submit_button.action_message'), type: :submit, big: true, wide: true, + **button_options, ).with_content(content) %> <% if recaptcha_script_src.present? %> <%= content_tag(:script, '', src: recaptcha_script_src, async: true) %> diff --git a/app/components/captcha_submit_button_component.rb b/app/components/captcha_submit_button_component.rb index 01a5d48b3a8..35be9c192f5 100644 --- a/app/components/captcha_submit_button_component.rb +++ b/app/components/captcha_submit_button_component.rb @@ -1,14 +1,15 @@ # frozen_string_literal: true class CaptchaSubmitButtonComponent < BaseComponent - attr_reader :form, :action, :tag_options + attr_reader :form, :action, :button_options, :tag_options alias_method :f, :form # @param [String] action https://developers.google.com/recaptcha/docs/v3#actions - def initialize(form:, action:, **tag_options) + def initialize(form:, action:, button_options: {}, **tag_options) @form = form @action = action + @button_options = button_options @tag_options = tag_options end diff --git a/app/controllers/idv/by_mail/request_letter_controller.rb b/app/controllers/idv/by_mail/request_letter_controller.rb index 4ed84020b46..189682018a1 100644 --- a/app/controllers/idv/by_mail/request_letter_controller.rb +++ b/app/controllers/idv/by_mail/request_letter_controller.rb @@ -70,8 +70,11 @@ def update_tracking resend: resend_requested?, first_letter_requested_at: first_letter_requested_at, hours_since_first_letter: - gpo_mail_service.hours_since_first_letter(first_letter_requested_at), - phone_step_attempts: gpo_mail_service.phone_step_attempts, + hours_since_first_letter(first_letter_requested_at), + phone_step_attempts: RateLimiter.new( + user: current_user, + rate_limit_type: :proof_address, + ).attempts, **ab_test_analytics_buckets, ) create_user_event(:gpo_mail_sent, current_user) @@ -87,6 +90,11 @@ def first_letter_requested_at current_user.gpo_verification_pending_profile&.gpo_verification_pending_at end + def hours_since_first_letter(first_letter_requested_at) + first_letter_requested_at ? + (Time.zone.now - first_letter_requested_at).to_i.seconds.in_hours.to_i : 0 + end + def confirm_mail_not_rate_limited redirect_to idv_enter_password_url if gpo_mail_service.rate_limited? end @@ -97,8 +105,11 @@ def resend_letter resend: true, first_letter_requested_at: first_letter_requested_at, hours_since_first_letter: - gpo_mail_service.hours_since_first_letter(first_letter_requested_at), - phone_step_attempts: gpo_mail_service.phone_step_attempts, + hours_since_first_letter(first_letter_requested_at), + phone_step_attempts: RateLimiter.new( + user: current_user, + rate_limit_type: :proof_address, + ).attempts, **ab_test_analytics_buckets, ) confirmation_maker = confirmation_maker_perform diff --git a/app/controllers/idv/enter_password_controller.rb b/app/controllers/idv/enter_password_controller.rb index b2c2963aa97..6b22e0fedcd 100644 --- a/app/controllers/idv/enter_password_controller.rb +++ b/app/controllers/idv/enter_password_controller.rb @@ -119,10 +119,6 @@ def confirm_current_password redirect_to idv_enter_password_url end - def gpo_mail_service - @gpo_mail_service ||= Idv::GpoMail.new(current_user) - end - def init_profile idv_session.create_profile_from_applicant_with_password( password, @@ -133,10 +129,12 @@ def init_profile analytics.idv_gpo_address_letter_enqueued( enqueued_at: Time.zone.now, resend: false, - phone_step_attempts: gpo_mail_service.phone_step_attempts, + phone_step_attempts: RateLimiter.new( + user: current_user, + rate_limit_type: :proof_address, + ).attempts, first_letter_requested_at: first_letter_requested_at, - hours_since_first_letter: - gpo_mail_service.hours_since_first_letter(first_letter_requested_at), + hours_since_first_letter: hours_since_first_letter(first_letter_requested_at), **ab_test_analytics_buckets, ) end @@ -155,6 +153,11 @@ def first_letter_requested_at idv_session.profile.gpo_verification_pending_at end + def hours_since_first_letter(first_letter_requested_at) + first_letter_requested_at ? + (Time.zone.now - first_letter_requested_at).to_i.seconds.in_hours.to_i : 0 + end + def valid_password? current_user.valid_password?(password) end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 315af26fffa..135ef08f9cd 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -33,6 +33,7 @@ def create session[:sign_in_flow] = :sign_in return process_locked_out_session if session_bad_password_count_max_exceeded? return process_locked_out_user if current_user && user_locked_out?(current_user) + return process_failed_captcha if !valid_captcha_result? rate_limit_password_failure = true self.resource = warden.authenticate!(auth_options) @@ -77,6 +78,36 @@ def process_locked_out_session redirect_to root_url end + def valid_captcha_result? + return @valid_captcha_result if defined?(@valid_captcha_result) + @valid_captcha_result = SignInRecaptchaForm.new(**recaptcha_form_args).submit( + email: auth_params[:email], + recaptcha_token: params.require(:user)[:recaptcha_token], + device_cookie: cookies[:device], + ).success? + end + + def process_failed_captcha + flash[:error] = t('errors.messages.invalid_recaptcha_token') + warden.logout(:user) + warden.lock! + redirect_to root_url + end + + def recaptcha_form_args + args = { analytics: } + if IdentityConfig.store.recaptcha_mock_validator + args.merge( + form_class: RecaptchaMockForm, + score: params.require(:user)[:recaptcha_mock_score].to_f, + ) + elsif FeatureManagement.recaptcha_enterprise? + args.merge(form_class: RecaptchaEnterpriseForm) + else + args + end + end + def redirect_to_signin controller_info = 'users/sessions#create' analytics.invalid_authenticity_token(controller: controller_info) @@ -125,22 +156,18 @@ def handle_valid_authentication def track_authentication_attempt(email) user = User.find_with_email(email) || AnonymousUser.new - success = user_signed_in_and_not_locked_out?(user) + success = current_user.present? && !user_locked_out?(user) && valid_captcha_result? analytics.email_and_password_auth( success: success, user_id: user.uuid, user_locked_out: user_locked_out?(user), + valid_captcha_result: valid_captcha_result?, bad_password_count: session[:bad_password_count].to_i, sp_request_url_present: sp_session[:request_url].present?, remember_device: remember_device_cookie.present?, ) end - def user_signed_in_and_not_locked_out?(user) - return false unless current_user - !user_locked_out?(user) - end - def user_locked_out?(user) user.locked_out? end diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index b87f260439a..6a80cfda56b 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -59,7 +59,7 @@ def confirm user_session: user_session, device_name: DeviceName.from_user_agent(request.user_agent), ) - result = form.submit(request.protocol, confirm_params) + result = form.submit(confirm_params) @platform_authenticator = form.platform_authenticator? @presenter = WebauthnSetupPresenter.new( current_user: current_user, @@ -161,7 +161,7 @@ def confirm_params :name, :platform_authenticator, :transports, - ) + ).merge(protocol: request.protocol) end end end diff --git a/app/forms/sign_in_recaptcha_form.rb b/app/forms/sign_in_recaptcha_form.rb new file mode 100644 index 00000000000..206e742ea70 --- /dev/null +++ b/app/forms/sign_in_recaptcha_form.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class SignInRecaptchaForm + include ActiveModel::Model + + RECAPTCHA_ACTION = 'sign_in' + + attr_reader :form_class, :form_args, :email, :recaptcha_token, :device_cookie + + validate :validate_recaptcha_result + + def initialize(form_class: RecaptchaForm, **form_args) + @form_class = form_class + @form_args = form_args + end + + def submit(email:, recaptcha_token:, device_cookie:) + @email = email + @recaptcha_token = recaptcha_token + @device_cookie = device_cookie + + success = valid? + FormResponse.new(success:, errors:, serialize_error_details_only: true) + end + + private + + def validate_recaptcha_result + recaptcha_response, _assessment_id = recaptcha_form.submit(recaptcha_token) + errors.merge!(recaptcha_form) if !recaptcha_response.success? + end + + def device + User.find_with_confirmed_email(email)&.devices&.find_by(cookie_uuid: device_cookie) + end + + def score_threshold + if IdentityConfig.store.sign_in_recaptcha_score_threshold.zero? || device.present? + 0.0 + else + IdentityConfig.store.sign_in_recaptcha_score_threshold + end + end + + def recaptcha_form + @recaptcha_form ||= form_class.new( + score_threshold:, + recaptcha_action: RECAPTCHA_ACTION, + **form_args, + ) + end +end diff --git a/app/forms/webauthn_setup_form.rb b/app/forms/webauthn_setup_form.rb index bd3e701a3c6..7148c980d33 100644 --- a/app/forms/webauthn_setup_form.rb +++ b/app/forms/webauthn_setup_form.rb @@ -10,6 +10,7 @@ class WebauthnSetupForm :name, presence: { message: proc { |object| object.send(:generic_error_message) } } validate :name_is_unique + validate :validate_attestation_response attr_reader :attestation_response @@ -22,12 +23,13 @@ def initialize(user:, user_session:, device_name:) @name = nil @platform_authenticator = false @authenticator_data_flags = nil + @protocol = nil @device_name = device_name end - def submit(protocol, params) + def submit(params) consume_parameters(params) - success = valid? && valid_attestation_response?(protocol) + success = valid? if success create_webauthn_configuration event = PushNotification::RecoveryInformationChangedEvent.new(user: user) @@ -59,7 +61,7 @@ def generic_error_message private - attr_reader :success, :transports, :invalid_transports + attr_reader :success, :transports, :invalid_transports, :protocol attr_accessor :user, :challenge, :attestation_object, :client_data_json, :name, :platform_authenticator, :authenticator_data_flags, :device_name @@ -74,6 +76,7 @@ def consume_parameters(params) @transports, @invalid_transports = params[:transports]&.split(',')&.partition do |transport| WebauthnConfiguration::VALID_TRANSPORTS.include?(transport) end + @protocol = params[:protocol] end def name_is_unique @@ -94,32 +97,22 @@ def name_is_unique end end + def validate_attestation_response + return if valid_attestation_response?(protocol) + errors.add(:attestation_object, :invalid, message: general_error_message) + end + def valid_attestation_response?(protocol) + original_origin = "#{protocol}#{self.class.domain_name}" @attestation_response = ::WebAuthn::AuthenticatorAttestationResponse.new( attestation_object: Base64.decode64(@attestation_object), client_data_json: Base64.decode64(@client_data_json), ) - safe_response("#{protocol}#{self.class.domain_name}") - end - - def safe_response(original_origin) - response = @attestation_response.valid?(@challenge.pack('c*'), original_origin) - add_attestation_error unless response - response - rescue StandardError - add_attestation_error - false - end - def add_attestation_error - if @platform_authenticator - errors.add :name, I18n.t('errors.webauthn_platform_setup.general_error'), - type: :attestation_error - else - errors.add :name, I18n.t( - 'errors.webauthn_setup.general_error_html', - link_html: I18n.t('errors.webauthn_setup.additional_methods_link'), - ), type: :attestation_error + begin + attestation_response.valid?(@challenge.pack('c*'), original_origin) + rescue StandardError + false end end @@ -151,6 +144,17 @@ def create_webauthn_configuration ) end + def general_error_message + if platform_authenticator + I18n.t('errors.webauthn_platform_setup.general_error') + else + I18n.t( + 'errors.webauthn_setup.general_error_html', + link_html: I18n.t('errors.webauthn_setup.additional_methods_link'), + ) + end + end + def mfa_user @mfa_user ||= MfaContext.new(user) end diff --git a/app/javascript/packages/build-sass/CHANGELOG.md b/app/javascript/packages/build-sass/CHANGELOG.md index 157a3446022..9d8716baec8 100644 --- a/app/javascript/packages/build-sass/CHANGELOG.md +++ b/app/javascript/packages/build-sass/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +- Fix typecheck error due to updated arguments in `fileURLToPath` from `node:url` + ## 3.1.0 ### New Features diff --git a/app/javascript/packages/build-sass/cli.js b/app/javascript/packages/build-sass/cli.js index 79e29d4fd0b..017cb3cdbdd 100755 --- a/app/javascript/packages/build-sass/cli.js +++ b/app/javascript/packages/build-sass/cli.js @@ -73,7 +73,7 @@ function build(files) { files.map(async (file) => { const { loadedUrls } = await buildFile(file, options); if (isWatching) { - const loadedPaths = loadedUrls.map(fileURLToPath); + const loadedPaths = loadedUrls.map((url) => fileURLToPath(url)); watchOnce(loadedPaths, () => build([file])); } }), diff --git a/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx b/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx index 78b560e8d92..c6d94ba397c 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx +++ b/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx @@ -1,38 +1,33 @@ -import { useContext, useEffect, useRef } from 'react'; +import { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { getAssetPath } from '@18f/identity-assets'; import { useI18n } from '@18f/identity-react-i18n'; import AcuantContext from '../context/acuant'; -import { - defineObservableProperty, - stopObservingProperty, -} from '../higher-order/observable-property'; +import { useObservableProperty } from '../hooks/use-observable-property'; function AcuantCaptureCanvas() { const { isReady, acuantCaptureMode, setAcuantCaptureMode } = useContext(AcuantContext); const { t } = useI18n(); const cameraRef = useRef(/** @type {HTMLDivElement?} */ (null)); + const [canvas, setCanvas] = useState(/** @type {HTMLElement? } */ (null)); useEffect(() => { - function onAcuantCameraCreated() { - const canvas = document.getElementById('acuant-ui-canvas'); - // Acuant SDK assigns a callback property to the canvas when it switches to its "Tap to - // Capture" mode (Acuant SDK v11.4.4, L158). Infer capture type by presence of the property. - defineObservableProperty(canvas, 'callback', (callback) => { - setAcuantCaptureMode(callback ? 'TAP' : 'AUTO'); - }); - } - + const onAcuantCameraCreated = () => setCanvas(document.getElementById('acuant-ui-canvas')); cameraRef.current?.addEventListener('acuantcameracreated', onAcuantCameraCreated); - return () => { - const canvas = document.getElementById('acuant-ui-canvas'); - if (canvas) { - stopObservingProperty(canvas, 'callback'); - } - + return () => cameraRef.current?.removeEventListener('acuantcameracreated', onAcuantCameraCreated); - }; - }, []); + }, [cameraRef.current]); + + const onCallback = useCallback( + (callback) => { + setAcuantCaptureMode(callback ? 'TAP' : 'AUTO'); + }, + [setAcuantCaptureMode], + ); + + // Acuant SDK assigns a callback property to the canvas when it switches to its "Tap to + // Capture" mode (Acuant SDK v11.4.4, L158). Infer capture type by presence of the property. + useObservableProperty(canvas, 'callback', onCallback); const clickCanvas = () => document.getElementById('acuant-ui-canvas')?.click(); diff --git a/app/javascript/packages/document-capture/higher-order/observable-property.tsx b/app/javascript/packages/document-capture/higher-order/observable-property.tsx deleted file mode 100644 index c6ed08b349f..00000000000 --- a/app/javascript/packages/document-capture/higher-order/observable-property.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Defines a property on the given object, calling the change callback when that property is set to - * a new value. - * - * @param object Object on which to define property. - * @param property Property name to observe. - * @param onChangeCallback Callback to trigger on change. - */ -export function defineObservableProperty( - object: any, - property: string, - onChangeCallback: (nextValue: any) => void, -) { - let currentValue: any; - - Object.defineProperty(object, property, { - get() { - return currentValue; - }, - set(nextValue) { - currentValue = nextValue; - onChangeCallback(nextValue); - }, - configurable: true, - }); -} - -/** - * Removes an observable property by removing the defined getter/setter methods - * and replaces the value with the most recent value. - * - * @param object Object on which to remove defined property. - * @param property Property name to remove observer for - */ -export function stopObservingProperty(object: any, property: string) { - const currentValue = object[property]; - - Object.defineProperty(object, property, { value: currentValue, writable: true }); -} diff --git a/app/javascript/packages/document-capture/hooks/use-observable-property.tsx b/app/javascript/packages/document-capture/hooks/use-observable-property.tsx new file mode 100644 index 00000000000..9ef6c45d971 --- /dev/null +++ b/app/javascript/packages/document-capture/hooks/use-observable-property.tsx @@ -0,0 +1,43 @@ +import { useEffect } from 'react'; + +/** + * Defines a property on the given object as an effect, + * It will call the change callback when that property is set to + * a new value. + * + * No-ops if object is not present. + * + * @param object Object on which to define property. + * @param property Property name to observe. + * @param onChangeCallback Callback to trigger on change. + */ +export function useObservableProperty( + object: any, + property: string, + onChangeCallback: (nextValue: any) => void, +) { + useEffect(() => { + if (!object) { + return; + } + + let currentValue: any; + + Object.defineProperty(object, property, { + get() { + return currentValue; + }, + set(nextValue) { + currentValue = nextValue; + onChangeCallback(nextValue); + }, + configurable: true, + }); + + return () => { + const value = object[property]; + + Object.defineProperty(object, property, { value, writable: true }); + }; + }, [object, property, onChangeCallback]); +} diff --git a/app/jobs/reports/drop_off_report.rb b/app/jobs/reports/drop_off_report.rb index 5e93e1140ad..66f18622a37 100644 --- a/app/jobs/reports/drop_off_report.rb +++ b/app/jobs/reports/drop_off_report.rb @@ -12,7 +12,7 @@ def perform(report_date) self.report_date = report_date subject = "Drop Off Report - #{report_date.to_date}" - JSON.parse(configs).each do |config| + configs.each do |config| reports = [report_maker(config['issuers']).as_emailable_reports] config['emails'].each do |email| ReportMailer.tables_report( diff --git a/app/jobs/risc_delivery_job.rb b/app/jobs/risc_delivery_job.rb index 9bdbcb7cd53..c48afb8c243 100644 --- a/app/jobs/risc_delivery_job.rb +++ b/app/jobs/risc_delivery_job.rb @@ -44,7 +44,7 @@ def perform( 'Content-Type' => 'application/secevent+jwt', ) do |req| req.options.context = { - service_name: inline? ? 'risc_http_push_direct' : 'risc_http_push_async', + service_name: 'risc_http_push_async', } end end @@ -58,7 +58,7 @@ def perform( user:, ) rescue *NETWORK_ERRORS => err - raise err if self.executions < 2 && !inline? + raise err if self.executions < 2 track_event( error: err.message, @@ -68,7 +68,7 @@ def perform( user:, ) rescue RedisRateLimiter::LimitError => err - raise err if self.executions < 10 && !inline? + raise err if self.executions < 10 track_event( error: err.message, @@ -102,10 +102,6 @@ def faraday end end - def inline? - queue_adapter.is_a?(ActiveJob::QueueAdapters::InlineAdapter) - end - def track_event(event_type:, issuer:, success:, user:, error: nil, status: nil) analytics(user).risc_security_event_pushed( client_id: issuer, @@ -113,7 +109,6 @@ def track_event(event_type:, issuer:, success:, user:, error: nil, status: nil) event_type:, status:, success:, - transport: inline? ? 'direct' : 'async', ) end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index aa1aa55db71..ce7d54fcd8e 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -400,6 +400,7 @@ def edit_password_visit # @param [Boolean] success # @param [String] user_id # @param [Boolean] user_locked_out if the user is currently locked out of their second factor + # @param [Boolean] valid_captcha_result Whether user passed the reCAPTCHA check # @param [String] bad_password_count represents number of prior login failures # @param [Boolean] sp_request_url_present if was an SP request URL in the session # @param [Boolean] remember_device if the remember device cookie was present @@ -408,6 +409,7 @@ def email_and_password_auth( success:, user_id:, user_locked_out:, + valid_captcha_result:, bad_password_count:, sp_request_url_present:, remember_device:, @@ -415,12 +417,13 @@ def email_and_password_auth( ) track_event( 'Email and Password Authentication', - success: success, - user_id: user_id, - user_locked_out: user_locked_out, - bad_password_count: bad_password_count, - sp_request_url_present: sp_request_url_present, - remember_device: remember_device, + success:, + user_id:, + user_locked_out:, + valid_captcha_result:, + bad_password_count:, + sp_request_url_present:, + remember_device:, **extra, ) end @@ -4919,14 +4922,12 @@ def return_to_sp_failure_to_proof(redirect_url:, flow: nil, step: nil, location: # @param [String] client_id # @param [String] event_type # @param [Boolean] success - # @param ['async'|'direct'] transport # @param [Integer] status # @param [String] error def risc_security_event_pushed( client_id:, event_type:, success:, - transport:, status: nil, error: nil, **extra @@ -4938,7 +4939,6 @@ def risc_security_event_pushed( event_type:, status:, success:, - transport:, **extra, ) end diff --git a/app/services/idv/gpo_mail.rb b/app/services/idv/gpo_mail.rb index 1d14ca87fa8..4408ce8ebf4 100644 --- a/app/services/idv/gpo_mail.rb +++ b/app/services/idv/gpo_mail.rb @@ -19,17 +19,6 @@ def profile_too_old? current_user.pending_profile.created_at < min_creation_date end - # Next two methods are analytics helpers used from RequestLetterController and - # EnterPasswordController - def phone_step_attempts - RateLimiter.new(user: @current_user, rate_limit_type: :proof_address).attempts - end - - def hours_since_first_letter(first_letter_requested_at) - first_letter_requested_at ? - (Time.zone.now - first_letter_requested_at).to_i.seconds.in_hours.to_i : 0 - end - private def window_limit_enabled? diff --git a/app/services/push_notification/http_push.rb b/app/services/push_notification/http_push.rb index 57a53dae816..f89d3b700bd 100644 --- a/app/services/push_notification/http_push.rb +++ b/app/services/push_notification/http_push.rb @@ -40,18 +40,12 @@ def url_options def deliver_one(service_provider) deliver_local(service_provider) if IdentityConfig.store.risc_notifications_local_enabled - job_arguments = { + RiscDeliveryJob.perform_later( push_notification_url: service_provider.push_notification_url, jwt: jwt(service_provider), event_type: event.event_type, issuer: service_provider.issuer, - } - - if IdentityConfig.store.risc_notifications_active_job_enabled - RiscDeliveryJob.perform_later(**job_arguments) - else - RiscDeliveryJob.perform_now(**job_arguments) - end + ) end def deliver_local(service_provider) diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb index 23d491c2340..a0188aa4db5 100644 --- a/app/views/accounts/show.html.erb +++ b/app/views/accounts/show.html.erb @@ -25,7 +25,7 @@

<%= t('i18n.language') %>

-
+
<%= @presenter.user.email_language_preference_description %>
diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 6a062383042..0013f1d7ed5 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -42,7 +42,16 @@ }, ) %> <%= hidden_field_tag :platform_authenticator_available, id: 'platform_authenticator_available' %> - <%= f.submit t('links.sign_in'), full_width: true, wide: false %> + + <% if FeatureManagement.sign_in_recaptcha_enabled? || IdentityConfig.store.recaptcha_mock_validator %> + <%= render CaptchaSubmitButtonComponent.new( + form: f, + action: SignInRecaptchaForm::RECAPTCHA_ACTION, + button_options: { full_width: true }, + ).with_content(t('links.sign_in')) %> + <% else %> + <%= f.submit t('links.sign_in'), full_width: true, wide: false %> + <% end %> <% end %> <% if desktop_device? %>
diff --git a/app/views/idv/by_mail/request_letter/index.html.erb b/app/views/idv/by_mail/request_letter/index.html.erb index 4f3cc18f59a..99a2f06e42b 100644 --- a/app/views/idv/by_mail/request_letter/index.html.erb +++ b/app/views/idv/by_mail/request_letter/index.html.erb @@ -26,7 +26,11 @@ ) %>

<% else %> - <%= render AlertComponent.new(message: t('idv.messages.gpo.info_alert'), class: 'margin-bottom-4') %> + <%= render AlertComponent.new( + type: :info, + message: t('idv.messages.gpo.info_alert'), + class: 'margin-bottom-4', + ) %> <%= render PageHeadingComponent.new.with_content(@presenter.title) %>

<%= t('idv.messages.gpo.timeframe_html') %> diff --git a/app/views/idv/in_person/ready_to_verify/show.html.erb b/app/views/idv/in_person/ready_to_verify/show.html.erb index a8d1b4b7dce..cd53bdde31c 100644 --- a/app/views/idv/in_person/ready_to_verify/show.html.erb +++ b/app/views/idv/in_person/ready_to_verify/show.html.erb @@ -28,7 +28,7 @@ ) %>

-<%= render AlertComponent.new(class: 'margin-y-4', text_tag: :div) do %> +<%= render AlertComponent.new(type: :info, class: 'margin-y-4', text_tag: :div) do %>

<%= t('in_person_proofing.body.barcode.deadline', deadline: @presenter.formatted_due_date) %>

<%= t('in_person_proofing.body.barcode.deadline_restart') %>

<% end %> diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index 432f55d996b..2ec6e6e4247 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -3,9 +3,11 @@ - - - + + + + + <% if content_for?(:meta_refresh) %> @@ -47,7 +49,6 @@ color: '#e21c3d', type: nil, ) %> - <%# Prelude script for error tracking (see `track-errors`) %> <%= javascript_tag(nonce: true) do %> diff --git a/app/views/shared/_sp_alert.html.erb b/app/views/shared/_sp_alert.html.erb index ac2ff7601ef..676bbc617cd 100644 --- a/app/views/shared/_sp_alert.html.erb +++ b/app/views/shared/_sp_alert.html.erb @@ -1,6 +1,6 @@ <% alert = decorated_sp_session.sp_alert(section) %> <% if alert %> - <%= render AlertComponent.new(text_tag: 'div', class: 'margin-bottom-4') do %> + <%= render AlertComponent.new(type: :info, text_tag: 'div', class: 'margin-bottom-4') do %> <%= raw sanitize(alert, tags: %w[a b strong em br p ol ul li], attributes: %w[href target]) %> <% end %> <% end %> diff --git a/app/views/user_mailer/reset_password_instructions.html.erb b/app/views/user_mailer/reset_password_instructions.html.erb index 06dbf2abe8f..8a4616aa5e6 100644 --- a/app/views/user_mailer/reset_password_instructions.html.erb +++ b/app/views/user_mailer/reset_password_instructions.html.erb @@ -2,7 +2,7 @@ -
+ <%= image_tag('email/letter-warning.png', width: 140, height: 140, alt: '') %> diff --git a/app/views/user_mailer/shared/_in_person_ready_to_verify.html.erb b/app/views/user_mailer/shared/_in_person_ready_to_verify.html.erb index 8c095e20d82..cd8ce7bc386 100644 --- a/app/views/user_mailer/shared/_in_person_ready_to_verify.html.erb +++ b/app/views/user_mailer/shared/_in_person_ready_to_verify.html.erb @@ -1,7 +1,7 @@ <% if @presenter.outage_message_enabled? %> -
+ <%= image_tag('email/warning.png', width: 16, height: 16, alt: '') %> @@ -28,7 +28,7 @@ <%# Alert box %> -
+ <%= image_tag('email/info.png', width: 16, height: 16, alt: '') %> @@ -51,9 +51,9 @@ - -
+ <%= image_tag( - asset_url('idv/real-id.svg'), + asset_url('email/real-id.png'), width: 110, height: 80, alt: t('in_person_proofing.process.eipp_bring_id.image_alt_text'), @@ -61,7 +61,7 @@ class: 'margin-bottom-3', ) %> +

<%= t('in_person_proofing.process.eipp_bring_id.info') %>

@@ -70,7 +70,7 @@
-
+
<%# Option 2: Bring a standard State ID... %>

<%= t('in_person_proofing.process.eipp_what_to_bring.heading') %>

@@ -80,16 +80,16 @@ - -
+ <%= image_tag( - asset_url('idv/state-id-and-passport.svg'), - width: 110.46, - height: 129.11, + asset_url('email/state-id-and-passport.png'), + width: 110, + height: 129, alt: t('in_person_proofing.process.eipp_state_id_passport.image_alt_text'), role: 'img', ) %> +

<%= t('in_person_proofing.process.eipp_state_id_passport.heading') %>

@@ -98,22 +98,22 @@
-
+
<%# B. State ID + Military ID %> - -
+ <%= image_tag( - asset_url('idv/state-id-and-military-id.svg'), - width: 110.46, + asset_url('email/state-id-and-military-id.png'), + width: 110, height: 93, alt: t('in_person_proofing.process.eipp_state_id_military_id.image_alt_text'), role: 'img', ) %> +

<%= t('in_person_proofing.process.eipp_state_id_military_id.heading') %>

@@ -122,22 +122,22 @@
-
+
<%# C. State ID + two supporting documents %> - -
+ <%= image_tag( - asset_url('idv/state-id-and-fair-evidence-documents.svg'), - width: 110.46, + asset_url('email/state-id-and-fair-evidence-documents.png'), + width: 110, height: 107, alt: t('in_person_proofing.process.eipp_state_id_supporting_docs.image_alt_text'), role: 'img', ) %> +

<%= t('in_person_proofing.process.eipp_state_id_supporting_docs.heading') %>

diff --git a/config/application.yml.default b/config/application.yml.default index 490eac60801..24e0c0ecfb4 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -276,7 +276,6 @@ reset_password_email_max_attempts: 20 reset_password_email_window_in_minutes: 60 reset_password_on_auth_fraud_event: true risc_notifications_local_enabled: false -risc_notifications_active_job_enabled: false risc_notifications_rate_limit_interval: 60 risc_notifications_rate_limit_max_requests: 1_000 risc_notifications_rate_limit_overrides: '{"https://example.com/push":{"interval":120,"max_requests":10000}}' @@ -298,6 +297,7 @@ session_timeout_in_minutes: 15 session_timeout_warning_seconds: 150 session_total_duration_timeout_in_minutes: 720 ses_configuration_set_name: '' +sign_in_recaptcha_score_threshold: 0.0 sp_handoff_bounce_max_seconds: 2 show_unsupported_passkey_platform_authentication_setup: false show_user_attribute_deprecation_warnings: false @@ -411,6 +411,7 @@ development: secret_key_base: development_secret_key_base session_encryption_key: 27bad3c25711099429c1afdfd1890910f3b59f5a4faec1c85e945cb8b02b02f261ba501d99cfbb4fab394e0102de6fecf8ffe260f322f610db3e96b2a775c120 show_unsupported_passkey_platform_authentication_setup: true + sign_in_recaptcha_score_threshold: 0.3 skip_encryption_allowed_list: '["urn:gov:gsa:SAML:2.0.profiles:sp:sso:localhost"]' state_tracking_enabled: true telephony_adapter: test diff --git a/config/application.yml.default.docker b/config/application.yml.default.docker index 5692d6a86a7..4aa054a39ba 100644 --- a/config/application.yml.default.docker +++ b/config/application.yml.default.docker @@ -26,6 +26,7 @@ production: piv_cac_verify_token_secret: "a6ed2fb16320ae85a7a8e48f4b0eeb6afca5f1ac64af2a05a0c486df1c20b693987832a11f0910729f199b3ce5c7609fe6d580bed428d035ea8460990e38a382" piv_cac_verify_token_url: ['env', 'PIV_CAC_VERIFY_TOKEN_URL'] secret_key_base: development_secret_key_base + sign_in_recaptcha_score_threshold: 0.3 domain_name: ['env', 'DOMAIN_NAME'] use_kms: false email_from: no-reply@identitysandbox.gov diff --git a/docs/frontend.md b/docs/frontend.md index 0b2285cd49a..2aef106c123 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -125,8 +125,11 @@ yarn add -W webpack As much as possible, try to use the same version of a dependency when it is used across multiple workspace packages. Otherwise, it can inflate the size of the compiled bundles and have a negative -performance impact on users. Similarly, consider using a tool like [`yarn-deduplicate`](https://github.com/scinos/yarn-deduplicate) -to deduplicate resolved package versions within the Yarn lockfile. +performance impact on users. + +We use [`yarn-deduplicate`](https://github.com/scinos/yarn-deduplicate) +to deduplicate resolved package versions within the Yarn lockfile, and enforce it with +the `make lint_yarn_lock` check. ### Localization diff --git a/lib/data_requests/local/fetch_cloudwatch_logs.rb b/lib/data_requests/local/fetch_cloudwatch_logs.rb index f9474818dfc..090d666e784 100644 --- a/lib/data_requests/local/fetch_cloudwatch_logs.rb +++ b/lib/data_requests/local/fetch_cloudwatch_logs.rb @@ -81,6 +81,7 @@ def query_string <<~QUERY fields @timestamp, @message | filter properties.user_id = '#{uuid}' and name != 'IRS Attempt API: Event metadata' + | limit #{Reporting::CloudwatchClient::MAX_RESULTS_LIMIT} QUERY end diff --git a/lib/feature_management.rb b/lib/feature_management.rb index 45a8a465c9d..c5469bb8dc5 100644 --- a/lib/feature_management.rb +++ b/lib/feature_management.rb @@ -106,10 +106,18 @@ def self.log_to_stdout? end def self.phone_recaptcha_enabled? - return false if IdentityConfig.store.recaptcha_site_key.blank? || - !IdentityConfig.store.phone_recaptcha_score_threshold.positive? + IdentityConfig.store.phone_recaptcha_score_threshold.positive? && recaptcha_enabled? + end + + def self.sign_in_recaptcha_enabled? + IdentityConfig.store.sign_in_recaptcha_score_threshold.positive? && recaptcha_enabled? + end - recaptcha_enterprise? || IdentityConfig.store.recaptcha_secret_key.present? + def self.recaptcha_enabled? + IdentityConfig.store.recaptcha_site_key.present? && ( + recaptcha_enterprise? || + IdentityConfig.store.recaptcha_secret_key.present? + ) end def self.recaptcha_enterprise? diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 98314846d7b..773018ff91a 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -13,6 +13,7 @@ def self.store Identity::Hostdata.config end + # identity-hostdata transforms these configs to the described type # rubocop:disable Metrics/BlockLength BUILDER = proc do |config| # ______________________________________ @@ -334,7 +335,6 @@ def self.store config.add(:reset_password_email_max_attempts, type: :integer) config.add(:reset_password_email_window_in_minutes, type: :integer) config.add(:reset_password_on_auth_fraud_event, type: :boolean) - config.add(:risc_notifications_active_job_enabled, type: :boolean) config.add(:risc_notifications_local_enabled, type: :boolean) config.add(:risc_notifications_rate_limit_interval, type: :integer) config.add(:risc_notifications_rate_limit_max_requests, type: :integer) @@ -367,6 +367,7 @@ def self.store config.add(:show_user_attribute_deprecation_warnings, type: :boolean) config.add(:short_term_phone_otp_max_attempts, type: :integer) config.add(:short_term_phone_otp_max_attempt_window_in_seconds, type: :integer) + config.add(:sign_in_recaptcha_score_threshold, type: :float) config.add(:skip_encryption_allowed_list, type: :json) config.add(:sp_handoff_bounce_max_seconds, type: :integer) config.add(:sp_issuer_user_counts_report_configs, type: :json) diff --git a/lib/reporting/cloudwatch_client.rb b/lib/reporting/cloudwatch_client.rb index ba67c0f1bf1..1829e0e3f77 100644 --- a/lib/reporting/cloudwatch_client.rb +++ b/lib/reporting/cloudwatch_client.rb @@ -4,12 +4,14 @@ require 'ruby-progressbar' require 'identity/hostdata' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/string/filters' module Reporting class CloudwatchClient DEFAULT_NUM_THREADS = 5 DEFAULT_WAIT_DURATION = 3 MAX_RESULTS_LIMIT = 10_000 + MAX_RESULTS_LIMIT_MATCHER = /\|\s+limit\s+#{MAX_RESULTS_LIMIT}/i attr_reader :num_threads, :wait_duration, :slice_interval, :logger, :log_group_name @@ -55,6 +57,8 @@ def initialize( # @yieldparam [Hash] row a row of the query result # @return [nil] def fetch(query:, from: nil, to: nil, time_slices: nil) + validate_query!(query) + results = Concurrent::Array.new if !block_given? errors = Concurrent::Array.new each_result_queue = Queue.new if block_given? @@ -290,6 +294,16 @@ def wait_for_query_result(query_id) end # rubocop:enable Rails/TimeZone + # @raise [ArgumentError] if the query is missing a limit + def validate_query!(query) + if ensure_complete_logs? && !query.match?(MAX_RESULTS_LIMIT_MATCHER) + raise ArgumentError, <<~STR.squish + ensure_complete_logs is true but query is missing '| limit #{MAX_RESULTS_LIMIT}', + script is unable to detect incomplete results + STR + end + end + # @yield [ProgressBar] def with_progress_bar @progress_bar_mutex&.synchronize do diff --git a/package.json b/package.json index 643fd7f2833..279d76117b7 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,8 @@ "svgo": "^3.2.0", "swr": "^2.0.0", "typescript": "^5.2.2", - "webpack-dev-server": "^5.0.4" + "webpack-dev-server": "^5.0.4", + "yarn-deduplicate": "^6.0.2" }, "resolutions": { "minimist": "1.2.6", diff --git a/spec/components/alert_component_spec.rb b/spec/components/alert_component_spec.rb index f52b2666ecb..f31ec38f0d5 100644 --- a/spec/components/alert_component_spec.rb +++ b/spec/components/alert_component_spec.rb @@ -19,10 +19,10 @@ expect(rendered).to have_content('locals') end - it 'defaults to type "info"' do + it 'renders without modifier classes by default' do rendered = render_inline AlertComponent.new(message: 'FYI') - expect(rendered).to have_selector('.usa-alert.usa-alert--info') + expect(rendered).to have_selector('.usa-alert:not([class*=usa-alert--])') end it 'accepts alert type param' do diff --git a/spec/components/captcha_submit_button_component_spec.rb b/spec/components/captcha_submit_button_component_spec.rb index 7e18e155f7b..ec8a2a055a0 100644 --- a/spec/components/captcha_submit_button_component_spec.rb +++ b/spec/components/captcha_submit_button_component_spec.rb @@ -71,6 +71,14 @@ end end + context 'with button options' do + let(:options) { super().merge(button_options: { full_width: true }) } + + it 'renders spinner button with additional options' do + expect(rendered).to have_css('lg-spinner-button .usa-button--full-width') + end + end + describe 'mock score field' do let(:recaptcha_mock_validator) { nil } diff --git a/spec/components/previews/alert_component_preview.rb b/spec/components/previews/alert_component_preview.rb index 73f932b740f..3d60ce6a258 100644 --- a/spec/components/previews/alert_component_preview.rb +++ b/spec/components/previews/alert_component_preview.rb @@ -24,18 +24,14 @@ def emergency render(AlertComponent.new(message: 'An emergency message', type: :emergency)) end - def other - render(AlertComponent.new(message: 'An other message', type: :other)) - end - def with_custom_text_tag render(AlertComponent.new(type: :success, message: 'A custom message', text_tag: 'div')) end # @!endgroup # @param message text - # @param type select [info, success, warning, error, emergency, other] - def workbench(message: 'An important message', type: :info) - render(AlertComponent.new(message:, type:)) + # @param type select [~, info, success, warning, error, emergency] + def workbench(message: 'An important message', type: nil) + render(AlertComponent.new(message:, type: type&.to_sym)) end end diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index 6a9f029888a..e50c63953fe 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -53,6 +53,7 @@ success: true, user_id: user.uuid, user_locked_out: false, + valid_captcha_result: true, bad_password_count: 0, sp_request_url_present: false, remember_device: false, @@ -123,6 +124,7 @@ success: false, user_id: user.uuid, user_locked_out: false, + valid_captcha_result: true, bad_password_count: 1, sp_request_url_present: false, remember_device: false, @@ -142,6 +144,7 @@ success: false, user_id: 'anonymous-uuid', user_locked_out: false, + valid_captcha_result: true, bad_password_count: 1, sp_request_url_present: false, remember_device: false, @@ -173,6 +176,7 @@ success: false, user_id: user.uuid, user_locked_out: true, + valid_captcha_result: true, bad_password_count: 0, sp_request_url_present: false, remember_device: false, @@ -184,6 +188,28 @@ post :create, params: { user: { email: user.email.upcase, password: user.password } } end + it 'tracks unsuccessful authentication for failed reCAPTCHA' do + user = create(:user, :fully_registered) + + allow(FeatureManagement).to receive(:sign_in_recaptcha_enabled?).and_return(true) + allow(IdentityConfig.store).to receive(:recaptcha_mock_validator).and_return(true) + allow(IdentityConfig.store).to receive(:sign_in_recaptcha_score_threshold).and_return(0.2) + stub_analytics + + post :create, params: { user: { email: user.email, password: user.password, score: 0.1 } } + + expect(@analytics).to have_logged_event( + 'Email and Password Authentication', + success: false, + user_id: user.uuid, + user_locked_out: false, + valid_captcha_result: false, + bad_password_count: 0, + remember_device: false, + sp_request_url_present: false, + ) + end + it 'tracks count of multiple unsuccessful authentication attempts' do user = create( :user, @@ -196,6 +222,7 @@ success: false, user_id: user.uuid, user_locked_out: false, + valid_captcha_result: true, bad_password_count: 2, sp_request_url_present: false, remember_device: false, @@ -214,6 +241,7 @@ success: false, user_id: 'anonymous-uuid', user_locked_out: false, + valid_captcha_result: true, bad_password_count: 1, sp_request_url_present: true, remember_device: false, @@ -384,6 +412,7 @@ success: true, user_id: user.uuid, user_locked_out: false, + valid_captcha_result: true, bad_password_count: 0, sp_request_url_present: false, remember_device: false, @@ -510,6 +539,7 @@ success: true, user_id: user.uuid, user_locked_out: false, + valid_captcha_result: true, bad_password_count: 0, sp_request_url_present: false, remember_device: true, @@ -535,6 +565,7 @@ success: true, user_id: user.uuid, user_locked_out: false, + valid_captcha_result: true, bad_password_count: 0, sp_request_url_present: false, remember_device: true, diff --git a/spec/controllers/users/webauthn_setup_controller_spec.rb b/spec/controllers/users/webauthn_setup_controller_spec.rb index 434ad532ff5..96a5f9b9612 100644 --- a/spec/controllers/users/webauthn_setup_controller_spec.rb +++ b/spec/controllers/users/webauthn_setup_controller_spec.rb @@ -348,8 +348,10 @@ 'Multi-Factor Authentication Setup', { enabled_mfa_methods_count: 0, - errors: { name: [I18n.t('errors.webauthn_platform_setup.general_error')] }, - error_details: { name: { attestation_error: true } }, + errors: { + attestation_object: [I18n.t('errors.webauthn_platform_setup.general_error')], + }, + error_details: { attestation_object: { invalid: true } }, in_account_creation_flow: false, mfa_method_counts: {}, multi_factor_auth_method: 'webauthn_platform', diff --git a/spec/forms/sign_in_recaptcha_form_spec.rb b/spec/forms/sign_in_recaptcha_form_spec.rb new file mode 100644 index 00000000000..65c7e0328f4 --- /dev/null +++ b/spec/forms/sign_in_recaptcha_form_spec.rb @@ -0,0 +1,87 @@ +require 'rails_helper' + +RSpec.describe SignInRecaptchaForm do + let(:user) { create(:user, :with_authenticated_device) } + let(:score_threshold_config) { 0.2 } + let(:analytics) { FakeAnalytics.new } + let(:email) { user.email } + let(:recaptcha_token) { 'token' } + let(:device_cookie) { Random.hex } + let(:score) { 1.0 } + subject(:form) do + described_class.new(form_class: RecaptchaMockForm, analytics:, score:) + end + before do + allow(IdentityConfig.store).to receive(:sign_in_recaptcha_score_threshold). + and_return(score_threshold_config) + end + + it 'passes instance variables to form' do + recaptcha_form = instance_double( + RecaptchaMockForm, + submit: FormResponse.new(success: true), + ) + expect(RecaptchaMockForm).to receive(:new). + with( + score_threshold: score_threshold_config, + score:, + analytics:, + recaptcha_action: described_class::RECAPTCHA_ACTION, + ). + and_return(recaptcha_form) + + form.submit(email:, recaptcha_token:, device_cookie:) + end + + context 'with custom recaptcha form class' do + subject(:form) do + described_class.new( + analytics:, + form_class: RecaptchaForm, + ) + end + + it 'validates using form instance of the given class' do + recaptcha_form = instance_double( + RecaptchaForm, + submit: FormResponse.new(success: true), + ) + expect(RecaptchaForm).to receive(:new).and_return(recaptcha_form) + expect(recaptcha_form).to receive(:submit) + + form.submit(email:, recaptcha_token:, device_cookie:) + end + end + + describe '#submit' do + let(:recaptcha_form_success) { false } + subject(:response) { form.submit(email:, recaptcha_token:, device_cookie:) } + + context 'recaptcha form validates as unsuccessful' do + let(:score) { 0.0 } + + context 'existing device for user' do + let(:device_cookie) { user.devices.first.cookie_uuid } + + it 'is successful' do + expect(response.to_h).to eq(success: true) + end + end + + context 'new device for user' do + it 'is unsuccessful with errors from recaptcha validation' do + expect(response.to_h).to eq( + success: false, + error_details: { recaptcha_token: { invalid: true } }, + ) + end + end + end + + context 'recaptcha form validates as successful' do + it 'is successful' do + expect(response.to_h).to eq(success: true) + end + end + end +end diff --git a/spec/forms/webauthn_setup_form_spec.rb b/spec/forms/webauthn_setup_form_spec.rb index 284b5811446..21aa0aa9c29 100644 --- a/spec/forms/webauthn_setup_form_spec.rb +++ b/spec/forms/webauthn_setup_form_spec.rb @@ -15,6 +15,7 @@ platform_authenticator: false, transports: 'usb', authenticator_data_value: '153', + protocol:, } end let(:subject) { WebauthnSetupForm.new(user:, user_session:, device_name:) } @@ -41,7 +42,7 @@ pii_like_keypaths: [[:mfa_method_counts, :phone]], } - expect(subject.submit(protocol, params).to_h).to eq( + expect(subject.submit(params).to_h).to eq( success: true, errors: {}, **extra_attributes, @@ -57,7 +58,7 @@ expect(PushNotification::HttpPush).to receive(:deliver). with(PushNotification::RecoveryInformationChangedEvent.new(user: user)) - subject.submit(protocol, params) + subject.submit(params) end context 'with platform authenticator' do @@ -66,7 +67,7 @@ end it 'creates a platform authenticator' do - result = subject.submit(protocol, params) + result = subject.submit(params) expect(result.extra[:multi_factor_auth_method]).to eq 'webauthn_platform' user.reload @@ -81,7 +82,7 @@ let(:params) { super().merge(authenticator_data_value: '65') } it 'includes data flags with bs set as false ' do - result = subject.submit(protocol, params) + result = subject.submit(params) expect(result.to_h[:authenticator_data_flags]).to eq( up: true, @@ -98,7 +99,7 @@ let(:params) { super().merge(authenticator_data_value: 'bad_error') } it 'should not include authenticator data flag' do - result = subject.submit(protocol, params) + result = subject.submit(params) expect(result.to_h[:authenticator_data_flags]).to be_nil end @@ -108,7 +109,7 @@ let(:params) { super().merge(authenticator_data_value: nil) } it 'should not include authenticator data flag' do - result = subject.submit(protocol, params) + result = subject.submit(params) expect(result.to_h[:authenticator_data_flags]).to be_nil end @@ -119,7 +120,7 @@ let(:params) { super().merge(transports: 'wrong') } it 'creates a webauthn configuration without transports' do - subject.submit(protocol, params) + subject.submit(params) user.reload @@ -127,7 +128,7 @@ end it 'includes unknown transports in extra analytics' do - result = subject.submit(protocol, params) + result = subject.submit(params) expect(result.to_h).to eq( success: true, @@ -169,13 +170,17 @@ pii_like_keypaths: [[:mfa_method_counts, :phone]], } - expect(subject.submit(protocol, params).to_h).to eq( + expect(subject.submit(params).to_h).to eq( success: false, - errors: { name: [I18n.t( - 'errors.webauthn_setup.general_error_html', - link_html: I18n.t('errors.webauthn_setup.additional_methods_link'), - )] }, - error_details: { name: { attestation_error: true } }, + errors: { + attestation_object: [ + I18n.t( + 'errors.webauthn_setup.general_error_html', + link_html: I18n.t('errors.webauthn_setup.additional_methods_link'), + ), + ], + }, + error_details: { attestation_object: { invalid: true } }, **extra_attributes, ) end @@ -185,7 +190,7 @@ let(:params) { super().except(:transports) } it 'creates a webauthn configuration without transports' do - subject.submit(protocol, params) + subject.submit(params) user.reload @@ -214,13 +219,17 @@ pii_like_keypaths: [[:mfa_method_counts, :phone]], } - expect(subject.submit(protocol, params).to_h).to eq( + expect(subject.submit(params).to_h).to eq( success: false, - errors: { name: [I18n.t( - 'errors.webauthn_setup.general_error_html', - link_html: I18n.t('errors.webauthn_setup.additional_methods_link'), - )] }, - error_details: { name: { attestation_error: true } }, + errors: { + attestation_object: [ + I18n.t( + 'errors.webauthn_setup.general_error_html', + link_html: I18n.t('errors.webauthn_setup.additional_methods_link'), + ), + ], + }, + error_details: { attestation_object: { invalid: true } }, **extra_attributes, ) end @@ -234,7 +243,7 @@ user end it 'checks for unique device on a webauthn device' do - result = subject.submit(protocol, params) + result = subject.submit(params) expect(result.extra[:multi_factor_auth_method]).to eq 'webauthn' expect(result.errors[:name]).to eq( [I18n.t( @@ -263,7 +272,7 @@ end it 'adds a new platform device with the same existing name and appends a (1)' do - result = subject.submit(protocol, params) + result = subject.submit(params) expect(result.extra[:multi_factor_auth_method]).to eq 'webauthn_platform' expect(user.webauthn_configurations.platform_authenticators.count).to eq(2) expect( @@ -289,7 +298,7 @@ end it 'adds a second new platform device with the same existing name and appends a (2)' do - result = subject.submit(protocol, params) + result = subject.submit(params) expect(result.success?).to eq(true) expect(user.webauthn_configurations.platform_authenticators.count).to eq(3) diff --git a/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx b/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx index 3eaa8b4f8e9..d50fa33d75d 100644 --- a/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx +++ b/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx @@ -1,5 +1,6 @@ import sinon from 'sinon'; import userEvent from '@testing-library/user-event'; +import { act } from '@testing-library/react'; import { AcuantContextProvider, DeviceContext } from '@18f/identity-document-capture'; import AcuantCaptureCanvas from '@18f/identity-document-capture/components/acuant-capture-canvas'; import { render, useAcuant } from '../../../support/document-capture'; @@ -16,9 +17,10 @@ describe('document-capture/components/acuant-capture-canvas', () => { , ); - initialize(); - window.AcuantCameraUI.start(); - + act(() => { + initialize(); + window.AcuantCameraUI.start(); + }); const button = getByRole('button', { name: 'doc_auth.buttons.take_picture' }); expect(button.disabled).to.be.true(); diff --git a/spec/javascript/packages/document-capture/higher-order/observable-property-spec.tsx b/spec/javascript/packages/document-capture/hooks/use-observable-property-spec.tsx similarity index 56% rename from spec/javascript/packages/document-capture/higher-order/observable-property-spec.tsx rename to spec/javascript/packages/document-capture/hooks/use-observable-property-spec.tsx index 0e0f66a56e6..c07f5e19fda 100644 --- a/spec/javascript/packages/document-capture/higher-order/observable-property-spec.tsx +++ b/spec/javascript/packages/document-capture/hooks/use-observable-property-spec.tsx @@ -1,14 +1,14 @@ import sinon from 'sinon'; -import { - defineObservableProperty, - stopObservingProperty, -} from '@18f/identity-document-capture/higher-order/observable-property'; +import { useObservableProperty } from '@18f/identity-document-capture/hooks/use-observable-property'; +import { renderHook } from '@testing-library/react-hooks'; -describe('document-capture/higher-order/observable-property', () => { - describe('defineObservableProperty', () => { +describe('document-capture/hooks/use-observable-property', () => { + describe('useObservableProperty', () => { it('behaves like an object', () => { const object = {} as { key?: string }; - defineObservableProperty(object, 'key', () => {}); + + renderHook(() => useObservableProperty(object, 'key', () => {})); + object.key = 'value'; expect(object.key).to.equal('value'); @@ -17,22 +17,21 @@ describe('document-capture/higher-order/observable-property', () => { it('calls the callback on changes, with the changed value', () => { const callback = sinon.spy(); const object = {} as { key?: string }; - defineObservableProperty(object, 'key', callback); + + renderHook(() => useObservableProperty(object, 'key', callback)); object.key = 'value'; expect(callback).to.have.been.calledOnceWithExactly('value'); }); - }); - describe('stopObservingProperty', () => { - it('removes the defined property and set the last value as a plain value', () => { + it('returns a cleanup function that removes the observer', () => { const object = {} as { key?: string }; const callback = sinon.spy(); - defineObservableProperty(object, 'key', callback); + const { unmount } = renderHook(() => useObservableProperty(object, 'key', callback)); object.key = 'value'; - stopObservingProperty(object, 'key'); + unmount(); expect(object.key).to.equal('value'); object.key = 'second_value'; diff --git a/spec/jobs/reports/drop_off_report_spec.rb b/spec/jobs/reports/drop_off_report_spec.rb index 9d9b03c8cab..c961b8fcebc 100644 --- a/spec/jobs/reports/drop_off_report_spec.rb +++ b/spec/jobs/reports/drop_off_report_spec.rb @@ -2,9 +2,10 @@ RSpec.describe Reports::DropOffReport do let(:report_date) { Date.new(2023, 12, 12).in_time_zone('UTC') } + # This is in S3 as a string that gets parsed via identity_config.rb let(:report_config) do - '[{"emails":["ursula@example.com"], - "issuers":"urn:gov:gsa:openidconnect.profiles:sp:sso:agency_name:app_name"}]' + JSON.parse '[{"emails":["ursula@example.com"], + "issuers":["urn:gov:gsa:openidconnect.profiles:sp:sso:agency_name:app_name"]}]' end before do diff --git a/spec/jobs/risc_delivery_job_spec.rb b/spec/jobs/risc_delivery_job_spec.rb index 75b938183d8..ba832a7352d 100644 --- a/spec/jobs/risc_delivery_job_spec.rb +++ b/spec/jobs/risc_delivery_job_spec.rb @@ -23,7 +23,6 @@ event_type: event_type, status: nil, success: false, - transport: 'direct', } end @@ -39,6 +38,8 @@ before do allow(job).to receive(:analytics).and_return(job_analytics) + allow(job).to receive(:queue_adapter). + and_return(ActiveJob::QueueAdapters::GoodJobAdapter.new) end it 'POSTs the jwt to the given URL' do @@ -68,43 +69,24 @@ stub_request(:post, push_notification_url).to_raise(Faraday::SSLError) end - context 'when performed inline' do - it 'logs an event' do - expect { perform }.to_not raise_error - - expect(job_analytics).to have_logged_event( - :risc_security_event_pushed, - risc_event_payload.merge(error: 'Exception from WebMock'), - ) - end + it 'raises and retries via ActiveJob' do + expect { perform }.to raise_error(Faraday::SSLError) end - context 'when performed in a worker' do + context 'it has already failed twice' do before do - allow(job).to receive(:queue_adapter). - and_return(ActiveJob::QueueAdapters::GoodJobAdapter.new) + allow(job).to receive(:executions).and_return 2 end - it 'raises and retries via ActiveJob' do - expect { perform }.to raise_error(Faraday::SSLError) - end + it 'logs an event' do + expect { perform }.to_not raise_error - context 'it has already failed twice' do - before do - allow(job).to receive(:executions).and_return 2 - end - - it 'logs an event' do - expect { perform }.to_not raise_error - - expect(job_analytics).to have_logged_event( - :risc_security_event_pushed, - risc_event_payload.merge( - error: 'Exception from WebMock', - transport: 'async', - ), - ) - end + expect(job_analytics).to have_logged_event( + :risc_security_event_pushed, + risc_event_payload.merge( + error: 'Exception from WebMock', + ), + ) end end end @@ -116,42 +98,24 @@ expect(job.faraday).to receive(:post).and_raise(Errno::ECONNREFUSED) end - context 'when performed inline' do - it 'logs an event' do - expect { perform }.to_not raise_error - expect(job_analytics).to have_logged_event( - :risc_security_event_pushed, - risc_event_payload.merge(error: 'Connection refused'), - ) - end + it 'raises and retries via ActiveJob' do + expect { perform }.to raise_error(Errno::ECONNREFUSED) end - context 'when performed in a worker' do + context 'it has already failed twice' do before do - allow(job).to receive(:queue_adapter). - and_return(ActiveJob::QueueAdapters::GoodJobAdapter.new) + allow(job).to receive(:executions).and_return 2 end - it 'raises and retries via ActiveJob' do - expect { perform }.to raise_error(Errno::ECONNREFUSED) - end + it 'logs an event' do + expect { perform }.to_not raise_error - context 'it has already failed twice' do - before do - allow(job).to receive(:executions).and_return 2 - end - - it 'logs an event' do - expect { perform }.to_not raise_error - - expect(job_analytics).to have_logged_event( - :risc_security_event_pushed, - risc_event_payload.merge( - error: 'Connection refused', - transport: 'async', - ), - ) - end + expect(job_analytics).to have_logged_event( + :risc_security_event_pushed, + risc_event_payload.merge( + error: 'Connection refused', + ), + ) end end end @@ -161,55 +125,33 @@ stub_request(:post, push_notification_url).to_return(status: 403) end - context 'when performed inline' do - it 'logs an event' do - expect { perform }.to_not raise_error - expect(job_analytics).to have_logged_event( - :risc_security_event_pushed, - risc_event_payload.merge( - error: 'http_push_error', - status: 403, - ), - ) - end + it 'logs an event' do + expect { perform }.to_not raise_error + expect(job_analytics).to have_logged_event( + :risc_security_event_pushed, + risc_event_payload.merge( + error: 'http_push_error', + status: 403, + ), + ) end - context 'when performed in a worker' do + context 'it has already failed twice' do before do - allow(job).to receive(:queue_adapter). - and_return(ActiveJob::QueueAdapters::GoodJobAdapter.new) + allow(job).to receive(:executions).and_return 2 end it 'logs an event' do expect { perform }.to_not raise_error + expect(job_analytics).to have_logged_event( :risc_security_event_pushed, risc_event_payload.merge( error: 'http_push_error', status: 403, - transport: 'async', ), ) end - - context 'it has already failed twice' do - before do - allow(job).to receive(:executions).and_return 2 - end - - it 'logs an event' do - expect { perform }.to_not raise_error - - expect(job_analytics).to have_logged_event( - :risc_security_event_pushed, - risc_event_payload.merge( - error: 'http_push_error', - status: 403, - transport: 'async', - ), - ) - end - end end end @@ -218,9 +160,18 @@ stub_request(:post, push_notification_url).to_timeout end - context 'when performed inline' do + it 'raises and retries via ActiveJob' do + expect { perform }.to raise_error(Faraday::ConnectionFailed) + end + + context 'it has already failed twice' do + before do + allow(job).to receive(:executions).and_return 2 + end + it 'logs an event' do expect { perform }.to_not raise_error + expect(job_analytics).to have_logged_event( :risc_security_event_pushed, risc_event_payload.merge( @@ -229,35 +180,6 @@ ) end end - - context 'when performed in a worker' do - before do - allow(job).to receive(:queue_adapter). - and_return(ActiveJob::QueueAdapters::GoodJobAdapter.new) - end - - it 'raises and retries via ActiveJob' do - expect { perform }.to raise_error(Faraday::ConnectionFailed) - end - - context 'it has already failed twice' do - before do - allow(job).to receive(:executions).and_return 2 - end - - it 'logs an event' do - expect { perform }.to_not raise_error - - expect(job_analytics).to have_logged_event( - :risc_security_event_pushed, - risc_event_payload.merge( - error: 'execution expired', - transport: 'async', - ), - ) - end - end - end end context 'rate limiting' do @@ -267,9 +189,18 @@ end end - context 'when performed inline' do - it 'logs an event on limit hit' do + it 'raises on rate limit errors (and retries via ActiveJob)' do + expect { perform }.to raise_error(RedisRateLimiter::LimitError) + end + + context 'it has already failed ten times' do + before do + allow(job).to receive(:executions).and_return 10 + end + + it 'logs an event' do expect { perform }.to_not raise_error + expect(job_analytics).to have_logged_event( :risc_security_event_pushed, risc_event_payload.merge( @@ -279,35 +210,6 @@ end end - context 'when performed in a worker' do - before do - allow(job).to receive(:queue_adapter). - and_return(ActiveJob::QueueAdapters::GoodJobAdapter.new) - end - - it 'raises on rate limit errors (and retries via ActiveJob)' do - expect { perform }.to raise_error(RedisRateLimiter::LimitError) - end - - context 'it has already failed ten times' do - before do - allow(job).to receive(:executions).and_return 10 - end - - it 'logs an event' do - expect { perform }.to_not raise_error - - expect(job_analytics).to have_logged_event( - :risc_security_event_pushed, - risc_event_payload.merge( - error: 'rate limit for push-notification-https://push.example.gov has maxed out', - transport: 'async', - ), - ) - end - end - end - context 'when the rate limit is overridden' do before do allow(IdentityConfig.store).to receive(:risc_notifications_rate_limit_overrides). @@ -324,7 +226,6 @@ risc_event_payload.merge( success: true, status: 200, - transport: 'direct', ), ) end diff --git a/spec/lib/feature_management_spec.rb b/spec/lib/feature_management_spec.rb index dea0fb9fc10..a1f35529f9c 100644 --- a/spec/lib/feature_management_spec.rb +++ b/spec/lib/feature_management_spec.rb @@ -376,57 +376,102 @@ end end - describe '.phone_recaptcha_enabled?' do + describe '.recaptcha_enabled?' do let(:recaptcha_site_key) { '' } let(:recaptcha_secret_key) { '' } let(:recaptcha_enterprise_api_key) { '' } let(:recaptcha_enterprise_project_id) { '' } - let(:phone_recaptcha_score_threshold) { 0.0 } - subject(:phone_recaptcha_enabled) { FeatureManagement.phone_recaptcha_enabled? } + subject(:recaptcha_enabled) { FeatureManagement.recaptcha_enabled? } before do allow(IdentityConfig.store).to receive(:recaptcha_site_key). and_return(recaptcha_site_key) allow(IdentityConfig.store).to receive(:recaptcha_secret_key). and_return(recaptcha_secret_key) - allow(IdentityConfig.store).to receive(:phone_recaptcha_score_threshold). - and_return(phone_recaptcha_score_threshold) allow(IdentityConfig.store).to receive(:recaptcha_enterprise_api_key). and_return(recaptcha_enterprise_api_key) allow(IdentityConfig.store).to receive(:recaptcha_enterprise_project_id). and_return(recaptcha_enterprise_project_id) end - it { expect(phone_recaptcha_enabled).to eq(false) } + it { is_expected.to eq(false) } context 'with configured recaptcha site key' do let(:recaptcha_site_key) { 'key' } - it { expect(phone_recaptcha_enabled).to eq(false) } + it { is_expected.to eq(false) } - context 'with configured default success rate threshold greater than 0' do - let(:phone_recaptcha_score_threshold) { 1.0 } + context 'with configured recaptcha secret key' do + let(:recaptcha_secret_key) { 'key' } - it { expect(phone_recaptcha_enabled).to eq(false) } + it { is_expected.to eq(true) } + end + + context 'with configured recaptcha enterprise api key' do + let(:recaptcha_enterprise_api_key) { 'key' } + + it { is_expected.to eq(false) } - context 'with configured recaptcha secret key' do - let(:recaptcha_secret_key) { 'key' } + context 'with configured recaptcha enterprise project id' do + let(:recaptcha_enterprise_project_id) { 'project-id' } - it { expect(phone_recaptcha_enabled).to eq(true) } + it { is_expected.to eq(true) } end + end + end + end - context 'with configured recaptcha enterprise api key' do - let(:recaptcha_enterprise_api_key) { 'key' } + describe '.phone_recaptcha_enabled?' do + let(:recaptcha_enabled) { false } + let(:phone_recaptcha_score_threshold) { 0.0 } - it { expect(phone_recaptcha_enabled).to eq(false) } + subject(:phone_recaptcha_enabled) { FeatureManagement.phone_recaptcha_enabled? } - context 'with configured recaptcha enterprise project id' do - let(:recaptcha_enterprise_project_id) { 'project-id' } + before do + allow(FeatureManagement).to receive(:recaptcha_enabled?).and_return(recaptcha_enabled) + allow(IdentityConfig.store).to receive(:phone_recaptcha_score_threshold). + and_return(phone_recaptcha_score_threshold) + end - it { expect(phone_recaptcha_enabled).to eq(true) } - end - end + it { is_expected.to eq(false) } + + context 'with configured default success rate threshold greater than 0' do + let(:phone_recaptcha_score_threshold) { 1.0 } + + it { is_expected.to eq(false) } + + context 'with recaptcha enabled' do + let(:recaptcha_enabled) { true } + + it { is_expected.to eq(true) } + end + end + end + + describe '.sign_in_recaptcha_enabled?' do + let(:recaptcha_enabled) { false } + let(:sign_in_recaptcha_score_threshold) { 0.0 } + + subject(:sign_in_recaptcha_enabled) { FeatureManagement.sign_in_recaptcha_enabled? } + + before do + allow(FeatureManagement).to receive(:recaptcha_enabled?).and_return(recaptcha_enabled) + allow(IdentityConfig.store).to receive(:sign_in_recaptcha_score_threshold). + and_return(sign_in_recaptcha_score_threshold) + end + + it { is_expected.to eq(false) } + + context 'with configured default success rate threshold greater than 0' do + let(:sign_in_recaptcha_score_threshold) { 1.0 } + + it { is_expected.to eq(false) } + + context 'with recaptcha enabled' do + let(:recaptcha_enabled) { true } + + it { is_expected.to eq(true) } end end end diff --git a/spec/lib/reporting/cloudwatch_client_spec.rb b/spec/lib/reporting/cloudwatch_client_spec.rb index 6a6f1c48e49..66986b08b2e 100644 --- a/spec/lib/reporting/cloudwatch_client_spec.rb +++ b/spec/lib/reporting/cloudwatch_client_spec.rb @@ -221,6 +221,14 @@ def stub_single_page expect(results.size).to eq(999) end + + context 'query is missing a limit' do + let(:query) { 'fields @message | stats count(*) by bin(1d)' } + + it 'raises' do + expect { fetch }.to raise_error(ArgumentError, /query is missing '| limit 10000'/) + end + end end context 'query is before Cloudwatch Insights Availability and AWS errors' do diff --git a/spec/services/push_notification/http_push_spec.rb b/spec/services/push_notification/http_push_spec.rb index 6e3e08b2c8b..6a854c044b4 100644 --- a/spec/services/push_notification/http_push_spec.rb +++ b/spec/services/push_notification/http_push_spec.rb @@ -22,87 +22,55 @@ ) end let(:now) { Time.zone.now } - let(:risc_notifications_active_job_enabled) { false } let(:push_notifications_enabled) { true } - let(:job_analytics) { FakeAnalytics.new } - let(:risc_event_payload) do - { - client_id: sp_with_push_url.issuer, - error: nil, - event_type: event.event_type, - status: nil, - success: false, - transport: 'direct', - } - end subject(:http_push) { PushNotification::HttpPush.new(event, now: now) } before do - allow(IdentityConfig.store).to receive(:risc_notifications_active_job_enabled). - and_return(risc_notifications_active_job_enabled) + ActiveJob::Base.queue_adapter = :test allow(Identity::Hostdata).to receive(:env).and_return('dev') allow(IdentityConfig.store).to receive(:push_notifications_enabled). and_return(push_notifications_enabled) - allow(Analytics).to receive(:new).and_return(job_analytics) end describe '#deliver' do subject(:deliver) { http_push.deliver } - context 'when push_notifications_enabled is disabled' do - let(:push_notifications_enabled) { false } - - it 'does not deliver any notifications' do - expect(http_push).to_not receive(:deliver_one) - - deliver - end + it 'enqueues a background job to deliver a notification' do + expect { deliver }.to have_enqueued_job(RiscDeliveryJob).once end - context 'when risc_notifications_active_job_enabled is enabled' do - let(:risc_notifications_active_job_enabled) { true } + it 'enqueues a background job with the correct arguments' do + expect { deliver }.to have_enqueued_job(RiscDeliveryJob).with { |args| + expect(args[:push_notification_url]).to eq sp_with_push_url.push_notification_url + expect(args[:event_type]).to eq event.event_type + expect(args[:issuer]).to eq sp_with_push_url.issuer + + jwt_payload, headers = JWT.decode( + args[:jwt], + AppArtifacts.store.oidc_public_key, + true, + algorithm: 'RS256', + kid: JWT::JWK.new(AppArtifacts.store.oidc_private_key).kid, + ) - it 'delivers a notification via background job' do - expect(RiscDeliveryJob).to receive(:perform_later) + expect(headers['typ']).to eq('secevent+jwt') + expect(headers['kid']).to eq(JWT::JWK.new(AppArtifacts.store.oidc_private_key).kid) - deliver - end + expect(jwt_payload['iss']).to eq(root_url) + expect(jwt_payload['iat']).to eq(now.to_i) + expect(jwt_payload['exp']).to eq((now + 12.hours).to_i) + expect(jwt_payload['aud']).to eq(sp_with_push_url.push_notification_url) + expect(jwt_payload['events']).to eq(event.event_type => event.payload.as_json) + } end - it 'makes an HTTP post to service providers with a push_notification_url' do - stub_request(:post, sp_with_push_url.push_notification_url). - with do |request| - expect(request.headers['Content-Type']).to eq('application/secevent+jwt') - expect(request.headers['Accept']).to eq('application/json') - - payload, headers = JWT.decode( - request.body, - AppArtifacts.store.oidc_public_key, - true, - algorithm: 'RS256', - kid: JWT::JWK.new(AppArtifacts.store.oidc_private_key).kid, - ) + context 'when push_notifications_enabled is false' do + let(:push_notifications_enabled) { false } - expect(headers['typ']).to eq('secevent+jwt') - expect(headers['kid']).to eq(JWT::JWK.new(AppArtifacts.store.oidc_private_key).kid) - - expect(payload['iss']).to eq(root_url) - expect(payload['iat']).to eq(now.to_i) - expect(payload['exp']).to eq((now + 12.hours).to_i) - expect(payload['aud']).to eq(sp_with_push_url.push_notification_url) - expect(payload['events']).to eq(event.event_type => event.payload.as_json) - end - - deliver - - expect(job_analytics).to have_logged_event( - :risc_security_event_pushed, - risc_event_payload.merge( - status: 200, - success: true, - ), - ) + it 'does not enqueue a RISC notification' do + expect { deliver }.not_to have_enqueued_job(RiscDeliveryJob) + end end context 'with an event that sends agency-specific iss_sub' do @@ -111,97 +79,35 @@ let(:agency_uuid) { AgencyIdentityLinker.new(sp_with_push_url_identity).link_identity.uuid } it 'sends the agency-specific uuid' do - stub_request(:post, sp_with_push_url.push_notification_url). - with do |request| - payload, _headers = JWT.decode( - request.body, - AppArtifacts.store.oidc_public_key, - true, - algorithm: 'RS256', - ) - - expect(payload['events'][event.event_type]['subject']['sub']).to eq(agency_uuid) - end - - deliver - end - end - - context 'with a timeout when posting to one url' do - let(:third_sp) { create(:service_provider, active: true, push_notification_url: 'http://sp.url/push') } - - before do - IdentityLinker.new(user, third_sp).link_identity - - stub_request(:post, sp_with_push_url.push_notification_url).to_timeout - stub_request(:post, third_sp.push_notification_url).to_return(status: 200) - end - - it 'still posts to the others' do - deliver - - expect(WebMock).to have_requested(:post, third_sp.push_notification_url) - end - - it 'logs both events' do - deliver - - expect(job_analytics).to have_logged_event( - :risc_security_event_pushed, - risc_event_payload.merge( - error: 'execution expired', - ), - ) - expect(job_analytics).to have_logged_event( - :risc_security_event_pushed, - risc_event_payload.merge( - client_id: third_sp.issuer, - status: 200, - success: true, - ), - ) - end - end - - context 'with a non-200 response from a push notification url' do - before do - stub_request(:post, sp_with_push_url.push_notification_url). - to_return(status: 500) - end - - it 'logs an event' do - deliver - - expect(job_analytics).to have_logged_event( - :risc_security_event_pushed, - risc_event_payload.merge( - error: 'http_push_error', - status: 500, - ), - ) + expect { deliver }.to have_enqueued_job(RiscDeliveryJob).with { |args| + jwt_payload, _headers = JWT.decode( + args[:jwt], + AppArtifacts.store.oidc_public_key, + true, + algorithm: 'RS256', + kid: JWT::JWK.new(AppArtifacts.store.oidc_private_key).kid, + ) + expect(jwt_payload['events'][event.event_type]['subject']['sub']).to eq(agency_uuid) + } end end context 'when a service provider is no longer active' do before { sp_with_push_url.update!(active: false) } - it 'does not notify that SP' do - deliver - - expect(WebMock).not_to have_requested(:get, sp_with_push_url.push_notification_url) + it 'does not enqueue a RISC notification' do + expect { deliver }.not_to have_enqueued_job(RiscDeliveryJob) end end - context 'when a user has revoked access to an SP' do + context 'when a user has revoked access to a service provider' do before do identity = user.identities.find_by(service_provider: sp_with_push_url.issuer) RevokeServiceProviderConsent.new(identity).call end - it 'does not notify that SP' do - deliver - - expect(WebMock).not_to have_requested(:get, sp_with_push_url.push_notification_url) + it 'does not enqueue a RISC notification' do + expect { deliver }.not_to have_enqueued_job(RiscDeliveryJob) end end end diff --git a/spec/views/accounts/show.html.erb_spec.rb b/spec/views/accounts/show.html.erb_spec.rb index 1923dfb8e7e..0734dc6c0f5 100644 --- a/spec/views/accounts/show.html.erb_spec.rb +++ b/spec/views/accounts/show.html.erb_spec.rb @@ -182,4 +182,34 @@ ) end end + + describe 'email language' do + context 'without explicit user language preference' do + let(:user) { create(:user, :fully_registered, email_language: nil) } + + before do + I18n.locale = :es + end + + it 'renders email language with language of parts as English' do + # Ensure that non-English content in English page is annotated with language. + # See: https://www.w3.org/WAI/WCAG21/Understanding/language-of-parts + render + + expect(rendered).to have_css('[lang=en]', text: t('account.email_language.name.en')) + end + end + + context 'with user language preference' do + let(:user) { create(:user, :fully_registered, email_language: :es) } + + it 'renders email language with language of parts as that language' do + # Ensure that non-English content in English page is annotated with language. + # See: https://www.w3.org/WAI/WCAG21/Understanding/language-of-parts + render + + expect(rendered).to have_css('[lang=es]', text: t('account.email_language.name.es')) + end + end + end end diff --git a/spec/views/devise/sessions/new.html.erb_spec.rb b/spec/views/devise/sessions/new.html.erb_spec.rb index 28a896f2d89..6bfa22d06a5 100644 --- a/spec/views/devise/sessions/new.html.erb_spec.rb +++ b/spec/views/devise/sessions/new.html.erb_spec.rb @@ -138,7 +138,10 @@ it 'does not have an sp alert for service providers without alert messages' do render - expect(rendered).to_not have_selector('.usa-alert') + expect(rendered).to_not have_selector( + '.usa-alert', + text: 'custom sign in help text for Awesome Application!', + ) end end end @@ -204,4 +207,45 @@ end end end + + describe 'submit button' do + let(:sign_in_recaptcha_enabled) { false } + let(:recaptcha_mock_validator) { false } + + subject(:rendered) { render } + + before do + allow(FeatureManagement).to receive(:sign_in_recaptcha_enabled?). + and_return(sign_in_recaptcha_enabled) + allow(IdentityConfig.store).to receive(:recaptcha_mock_validator). + and_return(recaptcha_mock_validator) + end + + context 'recaptcha at sign in is disabled' do + let(:sign_in_recaptcha_enabled) { false } + + it 'renders default sign-in submit button' do + expect(rendered).to have_button(t('links.sign_in')) + expect(rendered).not_to have_css('lg-captcha-submit-button') + end + + context 'recaptcha mock validator is enabled' do + let(:recaptcha_mock_validator) { true } + + it 'renders captcha sign-in submit button' do + expect(rendered).to have_button(t('links.sign_in')) + expect(rendered).to have_css('lg-captcha-submit-button') + end + end + end + + context 'recaptcha at sign in is enabled' do + let(:sign_in_recaptcha_enabled) { true } + + it 'renders captcha sign-in submit button' do + expect(rendered).to have_button(t('links.sign_in')) + expect(rendered).to have_css('lg-captcha-submit-button') + end + end + end end diff --git a/spec/views/layouts/base.html.erb_spec.rb b/spec/views/layouts/base.html.erb_spec.rb new file mode 100644 index 00000000000..147eff67b07 --- /dev/null +++ b/spec/views/layouts/base.html.erb_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' + +RSpec.describe 'layouts/base.html.erb' do + before do + view.title = 'Example' + end + + it 'includes expected OpenGraph metadata' do + render + + expect(rendered).to have_css('meta[name="og:site_name"][content~=""]', visible: false) + end +end diff --git a/yarn.lock b/yarn.lock index 2c06bca13a3..24ae052d682 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1218,10 +1218,30 @@ "@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.4" - resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" - integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== + 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" @@ -1477,9 +1497,9 @@ integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": - version "4.17.41" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz#5077defa630c2e8d28aa9ffc2c01c157c305bef6" - integrity sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA== + 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" "*" @@ -1547,9 +1567,9 @@ "@types/sizzle" "*" "@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.13" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" - integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ== + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/json5@^0.0.29": version "0.0.29" @@ -1581,9 +1601,9 @@ "@types/node" "*" "@types/node@*", "@types/node@^20.11.16", "@types/node@^20.2.5": - version "20.11.19" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.19.tgz#b466de054e9cb5b3831bee38938de64ac7f81195" - integrity sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ== + version "20.14.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.7.tgz#342cada27f97509eb8eb2dbc003edf21ce8ab5a8" + integrity sha512-uTr2m2IbJJucF3KUxgnGOZvYbN0QgkGyWxG6973HCpMYFy2KfcgYuIwkJQMQkt1VbBMlvWRbpshFTLxnxCZjKQ== dependencies: undici-types "~5.26.4" @@ -1593,9 +1613,9 @@ integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== "@types/qs@*": - version "6.9.11" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.11.tgz#208d8a30bc507bd82e03ada29e4732ea46a6bbda" - integrity sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ== + 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" @@ -1656,9 +1676,9 @@ "@types/express" "*" "@types/serve-static@*", "@types/serve-static@^1.15.5": - version "1.15.6" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.6.tgz#9cacd9b0b0fc5183ff0d5b27c1b1cad398113673" - integrity sha512-xkChxykiNb1X2YBevPIhQhNU9m9M7h9e2gDsmePAP2kNqhOvpKOrZWOCzq2ERQqfNFzlzHG2bdM0u3z5x+gQgg== + 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" "*" @@ -2031,6 +2051,11 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +"@yarnpkg/lockfile@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -2096,14 +2121,14 @@ ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: uri-js "^4.2.2" ajv@^8.0.0, ajv@^8.0.1, ajv@^8.9.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + version "8.16.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.16.0.tgz#22e2a92b94f005f7e0f9c9d39652ef0b8f6f0cb4" + integrity sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw== dependencies: - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.2.2" + uri-js "^4.4.1" ansi-colors@4.1.1: version "4.1.1" @@ -2249,7 +2274,7 @@ astral-regex@^2.0.0: asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== available-typed-arrays@^1.0.5: version "1.0.5" @@ -2391,7 +2416,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2, braces@^3.0.3, braces@~3.0.2: +braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -2447,13 +2472,16 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== +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" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" callsites@^3.0.0: version "3.1.0" @@ -2879,7 +2907,14 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: +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" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +debug@4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2958,6 +2993,15 @@ default-gateway@^6.0.3: 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" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-lazy-prop@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" @@ -2974,7 +3018,7 @@ define-properties@^1.1.3, define-properties@^1.1.4: delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== depd@2.0.0: version "2.0.0" @@ -3191,6 +3235,18 @@ es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.5, es-abstract@^1.20 unbox-primitive "^1.0.2" which-typed-array "^1.1.9" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-module-lexer@^1.2.1: version "1.5.0" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.0.tgz#4878fee3789ad99e065f975fdd3c645529ff0236" @@ -3741,9 +3797,9 @@ for-each@^0.3.3: is-callable "^1.1.3" foreground-child@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" - integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + version "3.2.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" + integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA== dependencies: cross-spawn "^7.0.0" signal-exit "^4.0.1" @@ -3787,7 +3843,7 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -function-bind@^1.1.1, function-bind@^1.1.2: +function-bind@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== @@ -3822,14 +3878,16 @@ get-func-name@^2.0.0, get-func-name@^2.0.2: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" - integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== +get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: - function-bind "^1.1.1" - has "^1.0.3" + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" get-stream@^6.0.0, get-stream@^6.0.1: version "6.0.1" @@ -3875,15 +3933,16 @@ glob@8.1.0: once "^1.3.0" glob@^10.3.7: - version "10.3.10" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" - integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== + version "10.4.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.2.tgz#bed6b95dade5c1f80b4434daced233aee76160e5" + integrity sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w== dependencies: foreground-child "^3.1.0" - jackspeak "^2.3.5" - minimatch "^9.0.1" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry "^1.10.1" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" glob@^7.1.3, glob@^7.2.0: version "7.2.3" @@ -3991,12 +4050,12 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== dependencies: - get-intrinsic "^1.1.1" + es-define-property "^1.0.0" has-proto@^1.0.1: version "1.0.1" @@ -4016,11 +4075,9 @@ has-tostringtag@^1.0.0: has-symbols "^1.0.2" has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" + version "1.0.4" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" + integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== hasown@^2.0.0: version "2.0.0" @@ -4144,6 +4201,11 @@ 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" @@ -4237,9 +4299,9 @@ ipaddr.js@1.9.1: integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== ipaddr.js@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.1.0.tgz#2119bc447ff8c257753b196fc5f1ce08a4cdf39f" - integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== + 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" @@ -4478,17 +4540,17 @@ isarray@~1.0.0: isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= -jackspeak@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" - integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== +jackspeak@^3.1.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.0.tgz#a75763ff36ad778ede6a156d8ee8b124de445b4a" + integrity sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw== dependencies: "@isaacs/cliui" "^8.0.2" optionalDependencies: @@ -4639,9 +4701,9 @@ language-tags@^1.0.5: language-subtag-registry "~0.3.2" launch-editor@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.1.tgz#f259c9ef95cbc9425620bbbd14b468fcdb4ffe3c" - integrity sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw== + 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" @@ -4812,6 +4874,11 @@ loupe@^2.3.6: dependencies: get-func-name "^2.0.0" +lru-cache@^10.2.0: + version "10.2.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" + integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -4819,18 +4886,6 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -"lru-cache@^9.1.1 || ^10.0.0": - version "10.2.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" - integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== - lz-string@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" @@ -4877,10 +4932,13 @@ media-typer@0.3.0: integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== memfs@^4.6.0: - version "4.8.1" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.8.1.tgz#1e02c15c4397212a9a1b037fa4324c6f7dd45b47" - integrity sha512-7q/AdPzf2WpwPlPL4v1kE2KsJsHl7EF4+hAeVzlyanr2+YnR21NVn9mDqo+7DEaKDRsQy8nvxPlKH4WqMtiO0w== + 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: @@ -4908,15 +4966,7 @@ methods@~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.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== - dependencies: - braces "^3.0.2" - picomatch "^2.3.1" - -micromatch@^4.0.4: +micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.7" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== @@ -4977,10 +5027,10 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.1: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== +minimatch@^9.0.4: + version "9.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== dependencies: brace-expansion "^2.0.1" @@ -4989,10 +5039,10 @@ minimist@1.2.6, minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": - version "7.0.4" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" - integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== mocha@^10.0.0: version "10.4.0" @@ -5149,19 +5199,19 @@ nth-check@^2.0.1: boolbase "^1.0.0" nwsapi@^2.2.4: - version "2.2.5" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.5.tgz#a52744c61b3889dd44b0a158687add39b8d935e2" - integrity sha512-6xpotnECFy/og7tKSBVmUNft7J3jyXAka4XvG6AUhFWRz+Q/Ljus7znJAA3bxColfQLdS+XsjoodtJfCgeTEFQ== + version "2.2.10" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.10.tgz#0b77a68e21a0b483db70b11fad055906e867cda8" + integrity sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ== object-assign@4.1.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -object-inspect@^1.12.3, object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.12.3, object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== object-keys@^1.1.1: version "1.1.1" @@ -5337,6 +5387,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -5396,12 +5451,12 @@ path-parse@^1.0.6, path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.10.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" - integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== dependencies: - lru-cache "^9.1.1 || ^10.0.0" + lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-to-regexp@0.1.7: @@ -5432,9 +5487,9 @@ pathval@^1.1.1: integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" @@ -5568,14 +5623,14 @@ proxy-addr@~2.0.7: ipaddr.js "1.9.1" psl@^1.1.33: - version "1.8.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" - integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + 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: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + 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" @@ -5795,7 +5850,7 @@ require-from-string@^2.0.2: requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== resolve-cwd@^3.0.0: version "3.0.0" @@ -5854,9 +5909,9 @@ rimraf@^3.0.2: glob "^7.1.3" rimraf@^5.0.5: - version "5.0.5" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.5.tgz#9be65d2d6e683447d2e9013da2bf451139a61ccf" - integrity sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A== + version "5.0.7" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.7.tgz#27bddf202e7d89cb2e0381656380d1734a854a74" + integrity sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg== dependencies: glob "^10.3.7" @@ -6086,12 +6141,10 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.7, semver@^7.5.4: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" +semver@^7.3.7, semver@^7.5.0, semver@^7.5.4: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== send@0.18.0: version "0.18.0" @@ -6149,6 +6202,18 @@ serve-static@1.15.0: 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" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + 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" @@ -6184,13 +6249,14 @@ shell-quote@^1.8.1: integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" @@ -6660,6 +6726,11 @@ 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" @@ -6688,9 +6759,9 @@ toidentifier@1.0.1: integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== tough-cookie@^4.1.2: - version "4.1.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" - integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== + version "4.1.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== dependencies: psl "^1.1.33" punycode "^2.1.1" @@ -6704,6 +6775,11 @@ tr46@^4.1.1: dependencies: punycode "^2.3.0" +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" @@ -6730,9 +6806,9 @@ tslib@^1.8.1: integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tslib@^2.0.0, tslib@^2.1.0, tslib@^2.5.0, tslib@^2.6.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== tsutils@^3.21.0: version "3.21.0" @@ -6851,7 +6927,7 @@ update-browserslist-db@^1.0.13: escalade "^3.1.1" picocolors "^1.0.0" -uri-js@^4.2.2: +uri-js@^4.2.2, uri-js@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== @@ -7185,9 +7261,9 @@ write-file-atomic@^5.0.1: signal-exit "^4.0.1" ws@^8.13.0, ws@^8.16.0: - version "8.16.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" - integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== xml-name-validator@^4.0.0: version "4.0.0" @@ -7209,11 +7285,6 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - yaml@^2.3.4: version "2.3.4" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" @@ -7270,6 +7341,16 @@ yargs@^17.7.2: y18n "^5.0.5" yargs-parser "^21.1.1" +yarn-deduplicate@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-6.0.2.tgz#63498d2d4c3a8567e992a994ce0ab51aa5681f2e" + integrity sha512-Efx4XEj82BgbRJe5gvQbZmEO7pU5DgHgxohYZp98/+GwPqdU90RXtzvHirb7hGlde0sQqk5G3J3Woyjai8hVqA== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + commander "^10.0.1" + semver "^7.5.0" + tslib "^2.5.0" + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"