From 910e3090e5ead5ab856e2a0507701100a9e18761 Mon Sep 17 00:00:00 2001 From: Christoph Marzell Date: Sat, 10 Oct 2026 09:02:35 +0200 Subject: [PATCH] add data export --- README.md | 4 + RELEASE_2026.10.02.1.md | 85 + app/controllers/api/v1/base_controller.rb | 5 +- .../api/v1/practice_transfers_controller.rb | 31 + app/services/practice_transfer_export.rb | 48 + .../professional_transfer_envelope.rb | 57 + config/application.rb | 2 +- config/database.yml | 2 +- config/environments/production.rb | 2 +- config/initializers/build_version.rb | 4 +- .../initializers/filter_parameter_logging.rb | 2 + config/routes.rb | 1 + ...80000_add_practice_transfer_id_to_users.rb | 6 + docker-compose-dev.yml | 2 +- docker-compose.yml | 1 + frontend/angular.json | 74 +- frontend/package-lock.json | 1839 ++++++++++++++--- frontend/package.json | 36 +- frontend/src/app/app.component.ts | 2 +- .../app/core/secure-transfer-origin.spec.ts | 11 + .../src/app/core/secure-transfer-origin.ts | 4 + .../practice-transfer-export.component.html | 17 + .../practice-transfer-export.component.scss | 1 + ...practice-transfer-export.component.spec.ts | 30 + .../practice-transfer-export.component.ts | 29 + frontend/src/app/pages/reports.component.ts | 4 +- frontend/tsconfig.spec.json | 13 + test/practice_transfer_test.rb | 41 + 28 files changed, 2031 insertions(+), 322 deletions(-) create mode 100644 RELEASE_2026.10.02.1.md create mode 100644 app/controllers/api/v1/practice_transfers_controller.rb create mode 100644 app/services/practice_transfer_export.rb create mode 100644 app/services/professional_transfer_envelope.rb create mode 100644 db/migrate/20261002080000_add_practice_transfer_id_to_users.rb create mode 100644 frontend/src/app/core/secure-transfer-origin.spec.ts create mode 100644 frontend/src/app/core/secure-transfer-origin.ts create mode 100644 frontend/src/app/pages/practice-transfer-export.component.html create mode 100644 frontend/src/app/pages/practice-transfer-export.component.scss create mode 100644 frontend/src/app/pages/practice-transfer-export.component.spec.ts create mode 100644 frontend/src/app/pages/practice-transfer-export.component.ts create mode 100644 frontend/tsconfig.spec.json create mode 100644 test/practice_transfer_test.rb diff --git a/README.md b/README.md index d7fca7a..b95cf5f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +## Build 2026.10.02.1 – Praxisexport + +Im Monatsbericht Fahrten und Ausbildungsaktivitäten passwortverschlüsselt an Praxisverwaltung 0.118.0 übertragen. [Bedienung, Installation und Prüfstatus](RELEASE_2026.10.02.1.md). + # Ausbildungsnachweis Die bisherige Rails-Webanwendung wurde in zwei klar getrennte Anwendungen umgebaut: diff --git a/RELEASE_2026.10.02.1.md b/RELEASE_2026.10.02.1.md new file mode 100644 index 0000000..6a37f11 --- /dev/null +++ b/RELEASE_2026.10.02.1.md @@ -0,0 +1,85 @@ +# Praktikumssoftware 2026.10.02.1 – Geschützter Praxisexport + +Stand: 02.10.2026. Ausgangspunkt Praxisverwaltung 0.117.0 und Praktikumssoftware Build 2026.08.20.1. Die Änderungen aus 0.117.0 bleiben enthalten. Passender neuer Praktikums-Build: 2026.10.02.1 (Frontend-Paket 1.1.0). + +## Bedienung + +1. Beide Anwendungen aktualisieren und ihre Datenbankmigrationen ausführen. +2. In der Praktikumssoftware anmelden und **Monatsbericht** öffnen. Zeitraum und ein eigenes Exportpasswort mit mindestens zwölf Zeichen angeben. **Verschlüsselten Praxisexport herunterladen** wählen. +3. In der Praxissoftware im gewünschten eigenen Konto anmelden. Über **Schnell finden → Praktikumsdaten importieren** oder **Fahrten & Fortbildung → Daten & Notfallplan** den Import öffnen. +4. `.pcmtransfer`-Datei und Exportpasswort eingeben. **Vorschau prüfen**, neue Einträge auswählen und die fachliche Prüfung bestätigen. **Auswahl verbindlich importieren** legt nur diese Datensätze an. +5. Fahrten erscheinen im Fahrtenbuch, Lernaktivitäten unter Bildung & Nachweise. Kategorien, Ausbildungsphase, Kilometeransatz und fehlende Strecken/Fahrzeugdaten prüfen. Nachweisdateien können anschließend am Bildungseintrag ergänzt werden. + +Dies ist ein geschützter, bewusst ausgelöster Dateiaustausch. Es handelt sich nicht um eine laufende Synchronisation oder um ein vollständiges Backup. + +## Daten und fachliche Grenzen + +- Datum, Ausbildungsbereich, Tätigkeitsart und Originalminuten werden übernommen. Stunden werden auf zwei Dezimalstellen umgerechnet; Originalminuten bleiben in den Notizen. +- Kilometer und der im Quellsystem konfigurierte Jahressatz werden übernommen. Fehlt dort ein Satz, verwendet der Export den bestehenden Quellsystem-Ersatzwert 0,42 EUR/km und markiert ihn intern als Ersatzwert. **Jeder importierte Kilometeransatz ist selbst zu prüfen**; die Software ermittelt hier keine steuerliche Zulässigkeit. +- Kosten bleiben Herkunftsnotizen. Es entstehen weder Ausgabenbuchungen noch Zahlungszuordnungen. +- Ausbildungsaktivitäten werden zunächst als `training` erfasst. Vergangene Aktivitäten sind `completed`, zukünftige `planned`. Keine ECTS-Umrechnung, keine automatische Mediationsstunden-Zuordnung oder behördliche Anerkennung. +- Unbekannte Tätigkeitsarten werden als Sonstiges gespeichert. Quelle und Originalart bleiben nachvollziehbar. +- Beschreibungen werden nur bei ausdrücklicher Auswahl im Export mitgenommen. Standardmäßig sind sie ausgeschlossen. Namen von Klienten, Kontopasswörter, E-Mail-Adressen und API-Tokens sind keine Exportfelder. +- Das vorhandene Quellmodell besitzt keine angehängten Nachweisdateien. Diese werden deshalb nicht übertragen und können im Ziel ergänzt werden. +- Laufende Zeiterfassungen werden ausgelassen und als Anzahl angezeigt. Einträge ohne Minuten und ohne Kilometer erzeugen keinen Ziel-Datensatz. Maximal 500 Quelleinträge beziehungsweise 1.000 Zielzeilen pro Export, maximal 4 MiB Datei. +- Ein Quelleintrag mit Kilometern und Zeit wird zu zwei unabhängig auswählbaren Zielzeilen: Fahrt und Lernaktivität. + +## Schutzmaßnahmen + +AES-256-GCM mit zufälligem Salt und IV; Schlüsselableitung PBKDF2-HMAC-SHA256 mit 600.000 Iterationen. Das Passwort steht nicht in der Datei. Veränderung und falsches Passwort führen zur Ablehnung. Algorithmus und KDF-Aufwand sind fest vorgegeben. Das Format ist versioniert. + +Export und Import benötigen jeweils die Anmeldung im betreffenden Konto. Die Quelle exportiert nur eigene Einträge. Das Ziel ignoriert keine fremden Benutzerangaben, sondern lehnt nicht unterstützte Felder ab; sämtliche Zielzuordnungen stammen vom angemeldeten Benutzer. Nur klinisch berechtigte Praxisrollen dürfen importieren. Vorhandene Verschlüsselung der Zielmodelle bleibt erhalten. + +Eine signierte Vorschau ist 15 Minuten gültig und an Benutzer und Dateihash gebunden. Auswahl und Dubletten werden beim Speichern erneut unter Kontosperre geprüft. Datensätze, Importbelege und Audit-Ereignis werden transaktional gespeichert. Bereits importierte Datensätze werden übersprungen; Änderungen erzeugen einen Konflikt. Gelöschte Ziel-Datensätze werden nicht unbemerkt erneut angelegt. Importbelege enthalten nur gehashte Quellidentität/Fingerabdruck und Zielreferenzen, keine Beschreibung. Sie bleiben nach einer einzelnen Datensatzlöschung als Dublettenschutz erhalten, bis das Konto gelöscht wird. + +Passwörter/Vorschautoken sind aus Rails-Parameterlogs gefiltert. Antworten haben `Cache-Control: no-store`. Kein Passwort oder entschlüsselter Vorschauinhalt wird in Local Storage gespeichert. Passwortfelder werden nach erfolgreicher Übernahme geleert. Die Benutzeroberfläche verweigert Datenaustausch auf unverschlüsselten entfernten HTTP-Seiten; lokale Entwicklungsadressen sind ausgenommen. Die Produktions-API verlangt HTTPS zusätzlich serverseitig. + +In der Praktikums-API wurde außerdem behoben, dass ein fehlender Token-Digest bei einem Konto ohne Token passen konnte. Fehlende Tokens und andere Authentifizierungsschemata als Bearer werden jetzt abgelehnt. + +## Installation und HTTPS + +Vor dem Update **jede Datenbank separat sichern** und vorhandene Konfiguration/Schlüssel behalten. Die beiden Projektordner gehören in ihre jeweiligen bestehenden Installationen, nicht ineinander. Keine neue `.env` über eine bestehende Serverkonfiguration kopieren. + +Neue Migrationen: + +- Praxis: `20261002090000_create_professional_import_receipts.rb` +- Praktikum: `20261002080000_add_practice_transfer_id_to_users.rb` + +In jedem Projekt die neuen Images bauen, `db:prepare` vor dem Neustart ausführen und erst nach erfolgreichem Healthcheck wieder freigeben. Die Praxis verwendet weiterhin ihren vorhandenen Deploy-/Rollback-Prozess. Bei einer manuellen Wartungsinstallation sind die Kernschritte je Projekt: + +```sh +docker compose build +docker compose run --rm api bin/rails db:prepare +docker compose up -d +``` + +Diese Befehle setzen laufende Datenbank/Redis und korrekt gesetzte bestehende Umgebungsvariablen voraus. Vorherige Images und Datenbanksicherung für einen kontrollierten Rollback behalten. Bei Rückkehr zum alten Code die neuen Importtabellen/-spalten stehen lassen; **keine automatische Down-Migration**, da sie den Dublettenschutz entfernen würde. Vor einem erneuten Import nach einer Datenbankwiederherstellung beide Datenstände prüfen. + +Beide öffentlichen Anwendungen über HTTPS bereitstellen. API-Ports dürfen nur dem vertrauenswürdigen Reverse Proxy zugänglich sein. In der Praxis muss der bestehende sichere Cookie-/TLS-Proxy-Betrieb korrekt konfiguriert sein. In der Praktikumssoftware ist neu `ASSUME_SSL` an den API-Container durchgereicht: nur hinter einem tatsächlich TLS-erzwingenden, vertrauenswürdigen Proxy auf `true` setzen. Alternativ den ursprünglichen HTTPS-Status korrekt bis Rails durchreichen. `ASSUME_SSL=true` schaltet selbst kein TLS ein. Ein HTTP-Produktionsaufruf wird mit HTTP 426 und verständlichem Hinweis abgelehnt. + +Keine Produktionsgeheimnisse im Paket. Vorhandene private Credentials-Dateien bleiben Teil der jeweiligen Installation. Entwicklungs-Datenbankpasswörter werden ebenfalls über Umgebungsvariablen gesetzt. + +## Geprüft und offen + +- Praxis: Produktionsbuild erfolgreich; 143 Frontend-Tests in 37 Dateien erfolgreich. +- Praktikum: Produktionsbuild erfolgreich; 4 Frontend-Tests in 2 Dateien erfolgreich. +- Praxis: 51 HTML-Templates auf Übersetzungsabdeckung geprüft, keine fehlende englische Übersetzung im automatisierten Template-Check. UI-Konsistenzprüfung erfolgreich. +- `npm audit`: in beiden aktualisierten Frontends 0 gemeldete Schwachstellen. Praktikum: Angular-Runtime 22.2.1, Build/CLI 22.1.4, gezielter Piscina-Override 5.3.2. Keine erzwungene Major-Aktualisierung. Lockfiles enthalten die geprüften Versionen. +- Praxisbuild: fünf bestehende SCSS-Budgetwarnungen außerhalb der neuen Importkomponente; kein Buildfehler. +- Rails-Spezifikationen für Verschlüsselung, Kontotrennung, Auswahl, Dubletten, Konflikte, Löschschutz, Transaktion und Vorschau-Bindung sowie Quell-API-Authentifizierung ergänzt. **Nicht ausgeführt:** Ruby/Bundler und PostgreSQL sind hier nicht verfügbar. `bundle exec rspec` und `ruby test/practice_transfer_test.rb` konnten deshalb nicht starten. +- Kein Docker-Build, kein Produktivdeployment und kein browsergestützter Ende-zu-Ende-Test gegen laufende Backends ausgeführt. Migrationen und verschlüsselter Roundtrip sind vor Freigabe auf einer Testinstallation auszuführen. + +Backend-Prüfung auf einer geeigneten Testumgebung: + +```sh +# Praxis/backend, ausschließlich separate Testdatenbank: +RAILS_ENV=test bundle exec rails db:prepare +bundle exec rspec spec/services/professional_transfer_envelope_spec.rb spec/services/professional_transfer_import_spec.rb spec/requests/api/v1/professional_transfers_spec.rb +# Praktikumsprojekt, ausschließlich separate Testdatenbank: +RAILS_ENV=test bundle exec rails db:prepare +bundle exec ruby test/practice_transfer_test.rb +``` + +## English summary + +This release adds an authenticated, password-encrypted file transfer from the internship application to the practice application. Open Monthly report in the updated source app to export a date range, then use **Import internship records** in practice quick find. Review and select records before confirming. Repeated imports are skipped; changed sources are marked for manual review. Learning activities initially use the training phase. No automatic professional credit, certificate transfer, accounting entry or ongoing synchronization is performed. Both applications and migrations are required. Frontend builds/tests and dependency audits pass; Rails, migration and live end-to-end verification remain pending because the required runtimes are unavailable here. diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index f4717df..97c7117 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -11,9 +11,10 @@ module Api attr_reader :current_user def authenticate_api_user! - token = request.authorization.to_s.delete_prefix("Bearer ").strip + scheme, value = request.authorization.to_s.split(" ", 2) + token = value.to_s.strip if scheme&.casecmp("Bearer")&.zero? digest = Digest::SHA256.hexdigest(token) if token.present? - @current_user = User.find_by(api_token_digest: digest) + @current_user = User.find_by(api_token_digest: digest) if digest.present? render json: { error: "Nicht angemeldet" }, status: :unauthorized unless @current_user end diff --git a/app/controllers/api/v1/practice_transfers_controller.rb b/app/controllers/api/v1/practice_transfers_controller.rb new file mode 100644 index 0000000..5d61d3b --- /dev/null +++ b/app/controllers/api/v1/practice_transfers_controller.rb @@ -0,0 +1,31 @@ +module Api + module V1 + class PracticeTransfersController < BaseController + before_action { response.set_header("Cache-Control", "no-store") } + + before_action :require_secure_transfer + + def create + payload = PracticeTransferExport.new(user: current_user, from: params[:from], to: params[:to], + descriptions: ActiveModel::Type::Boolean.new.cast(params[:include_descriptions])).call + encrypted = ProfessionalTransferEnvelope.encrypt(payload, params[:transfer_password]) + response.set_header("Cache-Control", "no-store") + response.set_header("X-Transfer-Skipped-Running", payload[:skipped_running].to_s) + send_data encrypted, type: "application/octet-stream", disposition: "attachment", + filename: "praktikum-praxis-#{Date.current.iso8601}.pcmtransfer" + rescue PracticeTransferExport::Invalid => error + render json: { error: error.message }, status: :unprocessable_entity + rescue ProfessionalTransferEnvelope::Invalid => error + message = error.message == "password" ? "Das Transferpasswort muss mindestens 12 Zeichen lang sein (höchstens 256 Bytes)." : "Der Export ist zu groß. Bitte den Zeitraum verkleinern." + render json: { error: message }, status: :unprocessable_entity + end + private + + def require_secure_transfer + if Rails.env.production? && !request.ssl? + render json: { error: "Der Praxisexport ist nur über HTTPS erlaubt. Bitte die TLS-Proxy-Konfiguration prüfen." }, status: :upgrade_required + end + end + end + end +end diff --git a/app/services/practice_transfer_export.rb b/app/services/practice_transfer_export.rb new file mode 100644 index 0000000..e9f7a01 --- /dev/null +++ b/app/services/practice_transfer_export.rb @@ -0,0 +1,48 @@ +require "date" +require "bigdecimal" +require "securerandom" + +class PracticeTransferExport + class Invalid < StandardError; end + MAX_ENTRIES = 500 + + def initialize(user:, from:, to:, descriptions: false) + @user, @from, @to, @descriptions = user, Date.iso8601(from.to_s), Date.iso8601(to.to_s), descriptions + raise Invalid, "Der Zeitraum ist ungültig." if @from > @to || !(1900..2200).cover?(@from.year) || !(1900..2200).cover?(@to.year) + rescue Date::Error + raise Invalid, "Bitte Beginn und Ende des Zeitraums angeben." + end + + def call + entries = @user.entries.where(date: @from..@to).order(:id).limit(MAX_ENTRIES + 1).to_a + raise Invalid, "Höchstens 500 Einträge je Export. Bitte den Zeitraum verkleinern." if entries.length > MAX_ENTRIES + namespace = @user.with_lock do + @user.update!(practice_transfer_id: SecureRandom.uuid) if @user.practice_transfer_id.blank? + @user.practice_transfer_id + end + rates = MileageRate.where(year: (@from.year..@to.year)).pluck(:year, :rate_per_km).to_h + skipped = 0 + rows = entries.flat_map do |entry| + if entry.start_time.present? && entry.end_time.nil? + skipped += 1 + next [] + end + hours, minutes = entry.hours_and_minutes + total_minutes = hours * 60 + minutes + common = { source_id: entry.id.to_s, date: entry.date.iso8601, education: entry.praktikums_typ, + activity: entry.entry_art, duration_minutes: total_minutes, + description: @descriptions ? entry.beschreibung.to_s.first(4_000) : nil, + cost_cents: entry.kosten ? (entry.kosten.to_d * 100).round.to_i : nil } + result = [] + if entry.distance_km.to_i.positive? + rate = rates[entry.date.year] || BigDecimal("0.42") # Existing source application fallback, not a tax determination. + result << common.merge(type: "mileage", kilometers: entry.distance_km, rate_cents: (rate.to_d * 100).round.to_i, + rate_origin: rates.key?(entry.date.year) ? "configured" : "source_default") + end + result << common.merge(type: "development") if total_minutes.positive? + result + end + { format: "pcm-professional-transfer-v1", source_namespace: namespace, exported_at: Time.current.iso8601, + from: @from.iso8601, to: @to.iso8601, skipped_running: skipped, records: rows } + end +end diff --git a/app/services/professional_transfer_envelope.rb b/app/services/professional_transfer_envelope.rb new file mode 100644 index 0000000..6523f06 --- /dev/null +++ b/app/services/professional_transfer_envelope.rb @@ -0,0 +1,57 @@ +require "openssl" +require "base64" +require "json" + +# Portable authenticated encryption, identical in both applications. +# The file contains no password or key. Never accept client-selected algorithms/KDF costs. +class ProfessionalTransferEnvelope + FORMAT = "pcm-professional-transfer-encrypted-v1".freeze + AAD = "pcm-professional-transfer:v1".freeze + ITERATIONS = 600_000 + MAX_FILE_BYTES = 4 * 1024 * 1024 + class Invalid < StandardError; end + + def self.encrypt(payload, password) + check_password!(password) + salt = OpenSSL::Random.random_bytes(16) + iv = OpenSSL::Random.random_bytes(12) + cipher = OpenSSL::Cipher.new("aes-256-gcm").encrypt + cipher.key = key(password, salt) + cipher.iv = iv + cipher.auth_data = AAD + ciphertext = cipher.update(JSON.generate(payload)) + cipher.final + envelope = { format: FORMAT, salt: Base64.strict_encode64(salt), iv: Base64.strict_encode64(iv), + tag: Base64.strict_encode64(cipher.auth_tag), ciphertext: Base64.strict_encode64(ciphertext) } + output = JSON.generate(envelope) + raise Invalid, "too_large" if output.bytesize > MAX_FILE_BYTES + output + end + + def self.decrypt(file, password) + check_password!(password) + raise Invalid, "too_large" if file.bytesize > MAX_FILE_BYTES + envelope = JSON.parse(file, max_nesting: 5) + raise Invalid, "invalid_file" unless envelope.is_a?(Hash) && envelope.keys.sort == %w[ciphertext format iv salt tag] && envelope["format"] == FORMAT + salt, iv, tag, ciphertext = %w[salt iv tag ciphertext].map { |name| Base64.strict_decode64(envelope.fetch(name)) } + raise Invalid, "invalid_file" unless salt.bytesize == 16 && iv.bytesize == 12 && tag.bytesize == 16 && ciphertext.bytesize.positive? + cipher = OpenSSL::Cipher.new("aes-256-gcm").decrypt + cipher.key = key(password, salt) + cipher.iv = iv + cipher.auth_tag = tag + cipher.auth_data = AAD + JSON.parse(cipher.update(ciphertext) + cipher.final, max_nesting: 10) + rescue Invalid + raise + rescue JSON::ParserError, ArgumentError, TypeError, KeyError, OpenSSL::Cipher::CipherError + raise Invalid, "invalid_file" + end + + def self.check_password!(password) + raise Invalid, "password" unless password.is_a?(String) && password.length >= 12 && password.bytesize <= 256 + end + + def self.key(password, salt) + OpenSSL::PKCS5.pbkdf2_hmac(password, salt, ITERATIONS, 32, "sha256") + end + private_class_method :key, :check_password! +end diff --git a/config/application.rb b/config/application.rb index 337d8fe..e98c991 100644 --- a/config/application.rb +++ b/config/application.rb @@ -12,7 +12,7 @@ require "action_mailbox/engine" require "action_text/engine" require "action_view/railtie" require "action_cable/engine" -# require "rails/test_unit/railtie" +require "rails/test_unit/railtie" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. diff --git a/config/database.yml b/config/database.yml index a3be50d..b1744a5 100644 --- a/config/database.yml +++ b/config/database.yml @@ -18,7 +18,7 @@ default: &default # For details on connection pooling, see Rails configuration guide # https://guides.rubyonrails.org/configuring.html#database-pooling pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> - password: password + password: <%= ENV["POSTGRES_PASSWORD"] || ENV["PRAKTIKUM_DATABASE_PASSWORD"] %> username: praktikum host: <%= ENV['DB'] || 'localhost'%> port: <%= ENV['DB_PORT'] || 35432%> diff --git a/config/environments/production.rb b/config/environments/production.rb index d0196c5..8e9f1d4 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -45,7 +45,7 @@ Rails.application.configure do # Assume all access to the app is happening through a SSL-terminating reverse proxy. # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. - # config.assume_ssl = true + config.assume_ssl = ENV.fetch("ASSUME_SSL", "false") == "true" # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = false diff --git a/config/initializers/build_version.rb b/config/initializers/build_version.rb index a93c30d..fb61fa6 100644 --- a/config/initializers/build_version.rb +++ b/config/initializers/build_version.rb @@ -1,2 +1,2 @@ -Rails.application.config.x.build_version = "2026.08.20.1" -Rails.logger.info("Praktikum API build 2026.08.20.1 loaded") +Rails.application.config.x.build_version = "2026.10.02.1" +Rails.logger.info("Praktikum API build 2026.10.02.1 loaded") diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index c2d89e2..1705504 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -6,3 +6,5 @@ Rails.application.config.filter_parameters += [ :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn ] + +Rails.application.config.filter_parameters += [:transfer_password] diff --git a/config/routes.rb b/config/routes.rb index 927e75d..3b7651f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,6 +18,7 @@ Rails.application.routes.draw do end get "dashboard", to: "dashboard#show" get "calendar", to: "calendar#index" + post "practice_transfer", to: "practice_transfers#create" get "monthly_report", to: "reports#monthly" resources :mileage_rates, except: %i[new edit destroy] resource :settings, only: %i[show update] diff --git a/db/migrate/20261002080000_add_practice_transfer_id_to_users.rb b/db/migrate/20261002080000_add_practice_transfer_id_to_users.rb new file mode 100644 index 0000000..e7e8283 --- /dev/null +++ b/db/migrate/20261002080000_add_practice_transfer_id_to_users.rb @@ -0,0 +1,6 @@ +class AddPracticeTransferIdToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :practice_transfer_id, :uuid + add_index :users, :practice_transfer_id, unique: true + end +end diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index e07f06b..7b8db6c 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -5,7 +5,7 @@ services: image: postgres:17 restart: unless-stopped environment: - POSTGRES_PASSWORD: password + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set a local database password} POSTGRES_USER: praktikum volumes: - pgdata:/var/lib/postgresql/data diff --git a/docker-compose.yml b/docker-compose.yml index 408c52d..7364221 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,7 @@ services: environment: DATABASE_URL: postgres://praktikum:${DATABASE_PASSWORD:-password}@db:5432/praktikum REDIS_URL: redis://redis:6379/0 + ASSUME_SSL: ${ASSUME_SSL:-false} RAILS_ENV: production SECRET_KEY_BASE: ${SECRET_KEY_BASE:?SECRET_KEY_BASE fehlt. Bitte zuerst setup-env.ps1 ausfuehren oder eine .env anlegen.} FRONTEND_ORIGINS: ${FRONTEND_ORIGINS:-http://localhost:13131} diff --git a/frontend/angular.json b/frontend/angular.json index 334ec4a..bfb2efd 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -1,9 +1,71 @@ { - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", - "projects": { "praktikum": { "projectType": "application", "root": "", "sourceRoot": "src", "prefix": "app", - "architect": { "build": { "builder": "@angular/build:application", "options": { "browser": "src/main.ts", "tsConfig": "tsconfig.app.json", "assets": [{"glob":"**/*","input":"public"}], "styles": ["node_modules/@fontsource-variable/inter/index.css", "src/styles.scss"] }, - "configurations": { "production": { "outputHashing": "all", "budgets": [{"type":"initial","maximumWarning":"1MB","maximumError":"2MB"}] } }, "defaultConfiguration": "production" }, - "serve": { "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "praktikum:build:production" }, "development": { "buildTarget": "praktikum:build" } }, "defaultConfiguration": "development" } + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "praktikum": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "node_modules/@fontsource-variable/inter/index.css", + "src/styles.scss" + ] + }, + "configurations": { + "production": { + "outputHashing": "all", + "budgets": [ + { + "type": "initial", + "maximumWarning": "1MB", + "maximumError": "2MB" + } + ] + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "praktikum:build:production" + }, + "development": { + "buildTarget": "praktikum:build" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular/build:unit-test", + "options": { + "tsConfig": "tsconfig.spec.json" + } + } + } } - } }, "cli": { "analytics": false } + }, + "cli": { + "analytics": false + } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8629428..d92bc7a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,34 +1,44 @@ { "name": "praktikum-frontend", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "praktikum-frontend", - "version": "1.0.0", + "version": "1.1.0", "dependencies": { - "@angular/animations": "^22.0.0", - "@angular/cdk": "^22.0.0", - "@angular/common": "^22.0.0", - "@angular/compiler": "^22.0.0", - "@angular/core": "^22.0.0", - "@angular/forms": "^22.0.0", - "@angular/material": "^22.0.0", - "@angular/platform-browser": "^22.0.0", - "@angular/router": "^22.0.0", + "@angular/animations": "22.2.1", + "@angular/cdk": "22.2.1", + "@angular/common": "22.2.1", + "@angular/compiler": "22.2.1", + "@angular/core": "22.2.1", + "@angular/forms": "22.2.1", + "@angular/material": "22.2.1", + "@angular/platform-browser": "22.2.1", + "@angular/router": "22.2.1", "@fontsource-variable/inter": "^5.3.0", "material-icons": "^1.13.14", "rxjs": "~7.8.2", "tslib": "^2.8.1" }, "devDependencies": { - "@angular/build": "^22.0.0", - "@angular/cli": "^22.0.0", - "@angular/compiler-cli": "^22.0.0", - "typescript": "~6.0.0" + "@angular/build": "22.1.4", + "@angular/cli": "22.1.4", + "@angular/compiler-cli": "22.2.1", + "@types/node": "^24.10.1", + "jsdom": "^27.0.1", + "typescript": "~6.0.2", + "vitest": "^4.0.8" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -110,9 +120,10 @@ } }, "node_modules/@angular/animations": { - "version": "22.1.2", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-22.1.2.tgz", - "integrity": "sha512-SHvopFG8I92isKE6lgoWVVmmt6fSGKt76bfrBuJ9qvrKWYqWmWUh87Z5J3XRPUM8zdzuEkXjcTbn+GTGOGRiaA==", + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-22.2.1.tgz", + "integrity": "sha512-vTxZN6uL/m61lv7ZCvfmQ3VuFb4o/tniGLpc3SpkNHE6MLM8WN0IjfNBRLMYMovd1uUBSPwhcG4Llcmucr9dmw==", + "deprecated": "@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -121,7 +132,7 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/core": "22.1.2" + "@angular/core": "22.2.1" } }, "node_modules/@angular/build": { @@ -249,9 +260,9 @@ } }, "node_modules/@angular/cdk": { - "version": "22.1.2", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-22.1.2.tgz", - "integrity": "sha512-YwTzVnTgZIQDLq3lnnPLINIB24e0SiDn7XxBxHSrOzD/+Y0zLAid5XIiMALXWOqa7rfdhfVh1kEeM93RRLnllQ==", + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-22.2.1.tgz", + "integrity": "sha512-0vjlqYBbLif1EwHhKz2r4thTDvtRI2CKi2js9TzPRD0n4VlFUnAXaaW47s5TCCObIoYun6Bo1zZ0uPgwb+qEMw==", "license": "MIT", "dependencies": { "parse5": "^8.0.0", @@ -260,6 +271,7 @@ "peerDependencies": { "@angular/common": "^22.0.0 || ^23.0.0", "@angular/core": "^22.0.0 || ^23.0.0", + "@angular/forms": "^22.0.0 || ^23.0.0", "@angular/platform-browser": "^22.0.0 || ^23.0.0", "rxjs": "^6.5.3 || ^7.4.0" } @@ -313,9 +325,9 @@ } }, "node_modules/@angular/common": { - "version": "22.1.2", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-22.1.2.tgz", - "integrity": "sha512-8TzyYPBDvSItV8Vqq8k5u8a9fWaomC9bMEr8z1vVLRZDIPhNQ/UcCyuTYbC7GZkeqrrKGu5m8tjUrA09RCwuaQ==", + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-22.2.1.tgz", + "integrity": "sha512-OqBH3EY4i8e3PvnYDugZGipSs1HWs2ZrqoClndKtFDS9mQULSSuLoxjG9nF1J/c+yUqhuyZlHjkXWS0n32pEfw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -324,14 +336,14 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/core": "22.1.2", + "@angular/core": "22.2.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "22.1.2", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-22.1.2.tgz", - "integrity": "sha512-aQv0p5MeXuguCeftUUxK4H8Hbw1hC5Zyu+cFsGbsi025LZ9Ngw+BW+iUHDQZAcqHvWDw2wgGOdKh4e7IkcfRyQ==", + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-22.2.1.tgz", + "integrity": "sha512-KRSz0c/T5eEiFa+F1l9OppAebJj9pT8kU3ArBMlxR3v/vzhpCfuQsFVQ4xEFC7dEm3zZIdv9ASKAP6ocNC4D/Q==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -341,9 +353,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "22.1.2", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-22.1.2.tgz", - "integrity": "sha512-GWRtvQQz/LvKejF/Zpls+hGnnJkvaPPtd3TZnxfZQ1fYCs7b7gpi0YM9PP02TJz51vhlsgkPYJwgtJAKDJEHNQ==", + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-22.2.1.tgz", + "integrity": "sha512-Nh8sCBl0JF6x+H7/1hemY76JeTRC66EfMULT4B3ryF9srRSLYc6X6RfPRtYvrIyjgr6h3jZgrNHlV76wiJaOcA==", "dev": true, "license": "MIT", "dependencies": { @@ -364,7 +376,7 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/compiler": "22.1.2", + "@angular/compiler": "22.2.1", "typescript": ">=6.0 <6.1" }, "peerDependenciesMeta": { @@ -374,9 +386,9 @@ } }, "node_modules/@angular/core": { - "version": "22.1.2", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-22.1.2.tgz", - "integrity": "sha512-t7BrgfdeqbdSbFe3CsGX8ySMeKDlZNBPxFMCOanAAqd6W0H1Kh1WFILBs3DIg0NfjpybfSRJAzKcagOFQWNtiw==", + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-22.2.1.tgz", + "integrity": "sha512-a8WlYTRiaMPz1KvLE5xjeWrLyZUuQX8bh6Ak7zajUci9iWsTUQefschjSwP1WKlin3JhM3XjmsTEDyZvSLIfRg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -385,7 +397,7 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/compiler": "22.1.2", + "@angular/compiler": "22.2.1", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0 || ~0.16.0" }, @@ -399,9 +411,9 @@ } }, "node_modules/@angular/forms": { - "version": "22.1.2", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-22.1.2.tgz", - "integrity": "sha512-pKDYSNKje/jox74KzswBdX4sL7kJ8OFkJwLnqng8oc7a/NVl/Vf4/Xq4kyowRAdtPcZfLTUnZG6s0qbegfV5mg==", + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-22.2.1.tgz", + "integrity": "sha512-xHM+PhrvYDJ+edPxh6PWhkE99tXWbsMcxdXCWF6QcyByX+bsW2rVlNZ7dumOsUle9MYa8Yj0lZFACUWbwFw+4w==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -412,22 +424,22 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/common": "22.1.2", - "@angular/core": "22.1.2", - "@angular/platform-browser": "22.1.2", + "@angular/common": "22.2.1", + "@angular/core": "22.2.1", + "@angular/platform-browser": "22.2.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/material": { - "version": "22.1.2", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-22.1.2.tgz", - "integrity": "sha512-KX0bVUppfwjDfUepsjpG1CAQGUGHI0l5e0QYpFcskB+1wNRCocBGsIO1br5z6neYu0rzsbhtWocjCeH05gwi7w==", + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-22.2.1.tgz", + "integrity": "sha512-qw+/aJBV4lqqoJgv1VBWbMBVnE7431SX0Mew1j7PO+s1UcTushbG7CIAEo1hwmRq3kyo4Qqch8+uzFq4R631HQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/cdk": "22.1.2", + "@angular/cdk": "22.2.1", "@angular/common": "^22.0.0 || ^23.0.0", "@angular/core": "^22.0.0 || ^23.0.0", "@angular/forms": "^22.0.0 || ^23.0.0", @@ -436,9 +448,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "22.1.2", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.1.2.tgz", - "integrity": "sha512-MQO735ve/tk4gLsWbw8NyBD/DxPfbbmf1pM/vp6K7tZz2xyysMXilqF/k8PBdQBZVOofST8S13inZHQ35nHuRA==", + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.2.1.tgz", + "integrity": "sha512-KQ3oguemaSqyNP00yqEvPouY7NC4Lji+6S8yfjaWaUZjSth/GckpeP/Fo9HtMqd04y6MZ6lEb2hbQtTqFcSdEQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -447,9 +459,9 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/animations": "22.1.2", - "@angular/common": "22.1.2", - "@angular/core": "22.1.2" + "@angular/animations": "22.2.1", + "@angular/common": "22.2.1", + "@angular/core": "22.2.1" }, "peerDependenciesMeta": { "@angular/animations": { @@ -458,9 +470,9 @@ } }, "node_modules/@angular/router": { - "version": "22.1.2", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-22.1.2.tgz", - "integrity": "sha512-C2c6NJ9HZUfb1L07M554Zpgvi0TvpOBO6Og9L36v/jsv42m7tpOenvqQVPa8R3va7eqLEszCL7wLFcWeJ8naew==", + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-22.2.1.tgz", + "integrity": "sha512-HJfJ4yA1znKd9YFtbDOJfKmD12x0PhS7LkmYdWlJRu2WkQZRBVrdGKdmbvV5UvrqCRllVWKpzw1TIZJM69rlcw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -469,20 +481,55 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/common": "22.1.2", - "@angular/core": "22.1.2", - "@angular/platform-browser": "22.1.2", + "@angular/common": "22.2.1", + "@angular/core": "22.2.1", + "@angular/platform-browser": "22.2.1", "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.0.tgz", - "integrity": "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.6.tgz", + "integrity": "sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^8.0.0", + "@babel/helper-validator-identifier": "^8.0.6", "js-tokens": "^10.0.0" }, "engines": { @@ -490,9 +537,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.0.tgz", - "integrity": "sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.5.tgz", + "integrity": "sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw==", "dev": true, "license": "MIT", "engines": { @@ -539,16 +586,16 @@ "license": "MIT" }, "node_modules/@babel/generator": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz", - "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.6.tgz", + "integrity": "sha512-/pcanjdRsonCexCuxgGD8fKnBLLzk2i9uM4kmcmGyyev2R5YDs+rafMYZ89RvaGY1mW8T7C5au0TyYaEeHBGng==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^8.0.0", - "@babel/types": "^8.0.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", + "@babel/parser": "^8.0.6", + "@babel/types": "^8.0.6", + "@jridgewell/gen-mapping": "0.4.0-beta.0", + "@jridgewell/trace-mapping": "^0.3.31", "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" }, @@ -556,6 +603,17 @@ "node": "^22.18.0 || >=24.11.0" } }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.4.0-beta.0.tgz", + "integrity": "sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0-beta.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@babel/helper-annotate-as-pure": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-8.0.0.tgz", @@ -570,26 +628,26 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.0.tgz", - "integrity": "sha512-JwculLABZvyPvyLBpwU/E/IbH2uM3mnxNtIJpxnIfb24y1PrdVxK5Dqjle4DpgqpGRnwgC7G8IkzPdSXZrO1Ew==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.6.tgz", + "integrity": "sha512-HTrtZ4+iA0+kzdbCgvdSk1U4pEl3BvpGnlUbualCGNbrVhhQvhgBzurTUmqpWyvyHbWYWaIfdx2mOlL9LJjsew==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^8.0.0", + "@babel/compat-data": "^8.0.5", "@babel/helper-validator-option": "^8.0.0", "browserslist": "^4.24.0", - "lru-cache": "^11.0.0", - "semver": "^7.7.3" + "flru": "1.0.2", + "verkit": "^0.3.2" }, "engines": { "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/helper-globals": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.0.tgz", - "integrity": "sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.6.tgz", + "integrity": "sha512-OdnA/N52LhzFpeUuVz5YGuSWjc7TPHiFmVYZbPlSgExocO2GwbxVpSni9MVs5wwW+wZ4kPuwBEBN9pIPDYz1mA==", "dev": true, "license": "MIT", "engines": { @@ -644,9 +702,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz", - "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.6.tgz", + "integrity": "sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA==", "dev": true, "license": "MIT", "engines": { @@ -654,9 +712,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz", - "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.6.tgz", + "integrity": "sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==", "dev": true, "license": "MIT", "engines": { @@ -674,27 +732,27 @@ } }, "node_modules/@babel/helpers": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.0.tgz", - "integrity": "sha512-wfbi91pM3py96oIiJEz7qIpyXDytgr9zQC1HEWwlGNVRAEmItuU/0a41ZUKu1sJGyhhOIpc4t5vk4PYzt8wpsg==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.5.tgz", + "integrity": "sha512-fQtPOXjYOYv85PIdwotp2TJGVYOycX0PQq+l844fFAxOULtBy8BVF35GyeueX0r4KvDthqPH5xAI1clQPk/2uA==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^8.0.0", - "@babel/types": "^8.0.0" + "@babel/types": "^8.0.5" }, "engines": { "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/parser": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz", - "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.6.tgz", + "integrity": "sha512-LpGDIYJAzc3Y3PT9x+FxBrGYu2PlWxLmyN7SwPTzaX0LSwEmFJBsRnPBqmaXF2r1v+Zhz6lNiEJ+7yKtM5+Ugg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^8.0.4" + "@babel/types": "^8.0.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -719,18 +777,18 @@ } }, "node_modules/@babel/traverse": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.4.tgz", - "integrity": "sha512-bZnmqzGG8UZneG1lLxBoWIH0G6Gr1D846Yu4/3XnY6FhCndMR49u26nTY08u/dAxWmLWF9vGQOuC+84FfIUoeg==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.6.tgz", + "integrity": "sha512-ilYV+qLStGzSKLipJlETdvdtpK3rAuwfCmCoGKv15MU4EoeIuLkespSNoNLFGOPSciEHx9TLWtN5mv0fz5w5mQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^8.0.0", - "@babel/generator": "^8.0.0", - "@babel/helper-globals": "^8.0.0", - "@babel/parser": "^8.0.4", + "@babel/code-frame": "^8.0.6", + "@babel/generator": "^8.0.6", + "@babel/helper-globals": "^8.0.6", + "@babel/parser": "^8.0.6", "@babel/template": "^8.0.0", - "@babel/types": "^8.0.4", + "@babel/types": "^8.0.6", "obug": "^2.1.1" }, "engines": { @@ -738,19 +796,159 @@ } }, "node_modules/@babel/types": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz", - "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.6.tgz", + "integrity": "sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^8.0.0", - "@babel/helper-validator-identifier": "^8.0.4" + "@babel/helper-string-parser": "^8.0.6", + "@babel/helper-validator-identifier": "^8.0.6" }, "engines": { "node": "^22.18.0 || >=24.11.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz", + "integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.2.tgz", + "integrity": "sha512-tL6LRF844sHjAKO7jt2w+uHo8Tocqpb9+KPUvIQETpR4RU87IaUzDuOV0QyzuQ37jmzFgwPHLLtLu/PDuH5qgg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.5.tgz", + "integrity": "sha512-Qqbp5pjbMwoaErP2t5qne5QDVDr3G5243oNW8ev5zfyyoqbzrQKbYDpKP/cuo3xBkzwqZqpP/rChrJhBdyTX7g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.2", + "@csstools/css-calc": "^3.4.2" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.2.tgz", + "integrity": "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.15.tgz", + "integrity": "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz", + "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@emnapi/core": { "version": "1.11.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", @@ -1227,6 +1425,24 @@ "node": ">=18" } }, + "node_modules/@exodus/bytes": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz", + "integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@fontsource-variable/inter": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz", @@ -1245,9 +1461,9 @@ "optional": true }, "node_modules/@hono/node-server": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", - "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.3.tgz", + "integrity": "sha512-TA//nWMqPhbfdfneACk6t5a9eqbS9lABEPyKn0/xZTah3H3U2XaVg85rJFl0/Fyit0I552YDHgXGVSf3GwqbUw==", "dev": true, "license": "MIT", "engines": { @@ -1258,9 +1474,9 @@ } }, "node_modules/@inquirer/ansi": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", - "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.8.tgz", + "integrity": "sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==", "dev": true, "license": "MIT", "engines": { @@ -1268,16 +1484,43 @@ } }, "node_modules/@inquirer/checkbox": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.1.tgz", - "integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.5.tgz", + "integrity": "sha512-bRt8J8m+Fot9CXv+zNQGXUq2ET0MggR1fPz7v6edN6MFYmsbfGnMmkmWZJEegMKqrAC8ej/o1sqisHZXZJMAfQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/core": "^11.2.1", - "@inquirer/figures": "^2.0.7", - "@inquirer/type": "^4.0.7" + "@inquirer/ansi": "^2.0.8", + "@inquirer/core": "^12.0.3", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/checkbox/node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -1341,15 +1584,42 @@ } }, "node_modules/@inquirer/editor": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.2.tgz", - "integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.3.tgz", + "integrity": "sha512-YsKkS2q63IiLtaDK/9nqzdComN97SDQrmKiyNggN+ceP4ty+Z6VwyTz3FpjeUWeW1Efss2xHFKCC9sx7hnrsxg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/external-editor": "^3.0.3", - "@inquirer/type": "^4.0.7" + "@inquirer/core": "^12.0.3", + "@inquirer/external-editor": "^3.0.5", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor/node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -1364,14 +1634,41 @@ } }, "node_modules/@inquirer/expand": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.1.tgz", - "integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.5.tgz", + "integrity": "sha512-uHuXLmXW+TtIfT/9vSBotypAkqn1n34Ul+CLGPos/xANyO4Ff5xZzkYhbKR4NEcfVK4a9mHQOpwVZzluSHFRGw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand/node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -1386,9 +1683,9 @@ } }, "node_modules/@inquirer/external-editor": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.3.tgz", - "integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.5.tgz", + "integrity": "sha512-f3QQJRIX5ZEneBHNUIuPjmbdzHnmRFJA8r2dkcb8q+OM5Uv5KtnuAttQumnrjcBVBM3mcTX1CkmtAkU58VRZxg==", "dev": true, "license": "MIT", "dependencies": { @@ -1408,9 +1705,9 @@ } }, "node_modules/@inquirer/figures": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", - "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.9.tgz", + "integrity": "sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==", "dev": true, "license": "MIT", "engines": { @@ -1418,14 +1715,41 @@ } }, "node_modules/@inquirer/input": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.2.tgz", - "integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.6.tgz", + "integrity": "sha512-HtcJhB2QFVXbLuJ5S3syhNbTUVxYvwqV4VRBDkQceBloC9bmTViUoRFP5PbSaDZb3HzfPmpuU/gG4ybVBz4FHA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/input/node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -1440,14 +1764,41 @@ } }, "node_modules/@inquirer/number": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.1.tgz", - "integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.2.3.tgz", + "integrity": "sha512-6Yuwh1NGSbu1Lo4N1EWjXs1jKRntLg/ZCwhmeorEHde90v1XxAozdbd4Iu30eOQLW+6h1hp2O9ujNfLSbTPJnA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number/node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -1462,15 +1813,42 @@ } }, "node_modules/@inquirer/password": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.1.tgz", - "integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.2.2.tgz", + "integrity": "sha512-W9zYdyzogK+6110mqwaSJWCBu2yA5Q/OfnGSjjZB1bNpHlmUozXxTl0+QOZBNeVd6Qo81/qT75gW05gLAtITxw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" + "@inquirer/ansi": "^2.0.8", + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password/node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -1514,15 +1892,92 @@ } } }, - "node_modules/@inquirer/rawlist": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.1.tgz", - "integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==", + "node_modules/@inquirer/rawlist": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.5.tgz", + "integrity": "sha512-1oHky1ONfCOwNrnkQGDE1oaSij/3fI6HFMSf2H/WsGO2lEyDX9My82iggITSy9ddSZ8yk8j9v41OI0fVoSIoaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist/node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.3.3.tgz", + "integrity": "sha512-fyuIU1Nbpvwlikjg3gXwJFDI11+EFjqQ7P+iByfmivIKQ1vmaykNrD/vy5unHuUqUpsOsnvJ25//tPF7E/RBRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search/node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -1536,16 +1991,17 @@ } } }, - "node_modules/@inquirer/search": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.1.tgz", - "integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==", + "node_modules/@inquirer/select": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.5.tgz", + "integrity": "sha512-9kc15hr8r/kI+3DO/xLog5nOzTz1jqsHXa6JBFzmQKhkoJ8Slda1I1L/uD8ZSZ9tF1yp79wwXe7mclvX1rqR2Q==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/figures": "^2.0.7", - "@inquirer/type": "^4.0.7" + "@inquirer/ansi": "^2.0.8", + "@inquirer/core": "^12.0.3", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -1559,17 +2015,20 @@ } } }, - "node_modules/@inquirer/select": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.1.tgz", - "integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==", + "node_modules/@inquirer/select/node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/core": "^11.2.1", - "@inquirer/figures": "^2.0.7", - "@inquirer/type": "^4.0.7" + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -1584,9 +2043,9 @@ } }, "node_modules/@inquirer/type": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", - "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.1.1.tgz", + "integrity": "sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==", "dev": true, "license": "MIT", "engines": { @@ -1623,9 +2082,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, "license": "MIT" }, @@ -2204,9 +2663,9 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", - "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz", + "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==", "dev": true, "license": "MIT", "optional": true, @@ -3162,9 +3621,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", "dev": true, "license": "MIT", "optional": true, @@ -3172,6 +3631,31 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/gensync": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.5.tgz", @@ -3186,6 +3670,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz", + "integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz", @@ -3199,6 +3693,146 @@ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -3275,9 +3909,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", - "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, "license": "MIT", "engines": { @@ -3300,10 +3934,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/baseline-browser-mapping": { - "version": "2.11.14", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz", - "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==", + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3334,6 +3978,16 @@ "node": ">=18.0.0" } }, + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -3381,9 +4035,9 @@ "license": "ISC" }, "node_modules/browserslist": { - "version": "4.28.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", - "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", "dev": true, "funding": [ { @@ -3401,11 +4055,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.11.12", - "caniuse-lite": "^1.0.30001809", - "electron-to-chromium": "^1.5.402", - "node-releases": "^2.0.53", - "update-browserslist-db": "^1.3.0" + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -3463,9 +4117,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001809", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", - "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", "dev": true, "funding": [ { @@ -3483,6 +4137,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -3727,6 +4391,20 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css-what": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", @@ -3740,6 +4418,46 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3758,6 +4476,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3860,9 +4585,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.406", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.406.tgz", - "integrity": "sha512-hWH5ORBi3d0IipnMh7BN5GDTaAmrSSSWmznwt2zltdiRNEWoEQyTwF0FFSBxzHO7hLSRT6loQu3IQGV0wg/Tvg==", + "version": "1.5.444", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.444.tgz", + "integrity": "sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ==", "dev": true, "license": "ISC" }, @@ -3874,9 +4599,9 @@ "license": "MIT" }, "node_modules/empathic": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", - "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.1.0.tgz", + "integrity": "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==", "dev": true, "license": "MIT", "engines": { @@ -3939,6 +4664,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -4011,6 +4743,16 @@ "dev": true, "license": "MIT" }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -4051,6 +4793,16 @@ "node": ">=18.0.0" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -4096,9 +4848,9 @@ } }, "node_modules/express-rate-limit": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", - "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", "dev": true, "license": "MIT", "dependencies": { @@ -4140,9 +4892,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ { @@ -4206,6 +4958,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/flru": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz", + "integrity": "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -4272,9 +5034,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, "license": "MIT", "engines": { @@ -4370,9 +5132,9 @@ } }, "node_modules/hono": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.2.tgz", - "integrity": "sha512-JydRilDRkYBQMt9qR9U92mXxmbGqsqSn/IKOrh4e7/gEbn+0zSr8igTu0obwJoNGN4sez28DIql7FBHWydoJpA==", + "version": "4.13.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.12.tgz", + "integrity": "sha512-6E2QDAc9Ick9Sq77ZrGS/dk2WUYni91aufTw6LJKpV7w8kW5/GxVUc650FOADOmlwg3K+f7Pun6XlV+pYmW6gw==", "dev": true, "license": "MIT", "engines": { @@ -4392,6 +5154,19 @@ "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/htmlparser2": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", @@ -4446,6 +5221,30 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", @@ -4504,9 +5303,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", - "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", + "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", "dev": true, "license": "MIT", "engines": { @@ -4577,6 +5376,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -4604,22 +5410,86 @@ "dev": true, "license": "ISC" }, - "node_modules/jose": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.8.tgz", - "integrity": "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==", + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" + "engines": { + "node": ">= 14" } }, - "node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } }, "node_modules/jsesc": { "version": "3.1.0", @@ -5067,9 +5937,9 @@ } }, "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -5102,6 +5972,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/media-typer": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", @@ -5231,9 +6108,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, "funding": [ { @@ -5250,13 +6127,34 @@ } }, "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", "dev": true, "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/node-addon-api": { @@ -5284,9 +6182,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.53", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", - "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", "dev": true, "license": "MIT", "engines": { @@ -5346,9 +6244,9 @@ } }, "node_modules/obug": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", - "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -5422,9 +6320,9 @@ } }, "node_modules/ordered-binary": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", - "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.2.tgz", + "integrity": "sha512-BY+qgDArmYpvvmij141xbkB4Cw8RFLBQM5Zj4MODFb1SyXBRE/xO4Xj/QWbR7aUEBxbkjuNdsFVCL2TmbYrlNg==", "dev": true, "license": "MIT", "optional": true @@ -5495,9 +6393,9 @@ } }, "node_modules/parse5-html-rewriting-stream/node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5521,9 +6419,9 @@ } }, "node_modules/parse5/node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", "license": "BSD-2-Clause", "engines": { "node": ">=20.19.0" @@ -5563,6 +6461,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5584,9 +6489,9 @@ } }, "node_modules/piscina": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.2.0.tgz", - "integrity": "sha512-DszUCKeVN/5G5QKo6jAVHL8fmKnkJvQ0ACiVgY7YGCq3TUB2oznAOayvZPIAdEThvhczkXR+qm3IHsNXpFCYfA==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.3.2.tgz", + "integrity": "sha512-vv7l/mM7B9WgrhDaudIqv1x6v4LOBRYYmpWGdWushhpbCmVHp11sUB2v4hj1CAeTGXVxuZFz6xOU1RalAiPKfQ==", "dev": true, "license": "MIT", "engines": { @@ -5607,9 +6512,9 @@ } }, "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -5627,7 +6532,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.17", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5643,9 +6548,9 @@ "license": "MIT" }, "node_modules/postcss-safe-parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", - "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.1.0.tgz", + "integrity": "sha512-1WzZxRLaAFwEh6Do+zyGpjWV3nGJNxxhuh7Ubu/q1ICImMgZJnLwhoaEpRbG4pJppp2Y1ncL9ffA2f+LhrefQg==", "dev": true, "funding": [ { @@ -5680,9 +6585,9 @@ } }, "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5691,6 +6596,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/proxy-agent-negotiate": { @@ -5711,10 +6620,20 @@ } } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5911,6 +6830,19 @@ "@parcel/watcher": "^2.4.1" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -6077,6 +7009,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -6118,9 +7057,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -6148,6 +7087,13 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -6158,6 +7104,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, "node_modules/stdin-discarder": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", @@ -6172,9 +7125,9 @@ } }, "node_modules/string-width": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", - "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6204,6 +7157,30 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -6221,6 +7198,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.16", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.16.tgz", + "integrity": "sha512-QwBER5KMR86IIjpIiO7H/Z3IMJPsZ1A6RKPAqzTTgOyUQUSt9FdnKcqhTaJmkY6HVrgouZHZR0ncK5QxvmnQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.16" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.16", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.16.tgz", + "integrity": "sha512-MDolfaSJtlSK5Y0A1xl3277ekubZwobpBjugknDizI9O5Rm60a1m8k4ICK+MRsCDzPygT81mp3BBf5RKDlFRfA==", + "dev": true, + "license": "MIT" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -6231,6 +7238,32 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -6284,6 +7317,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6295,9 +7335,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", - "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "dev": true, "funding": [ { @@ -6345,6 +7385,19 @@ "node": ">= 0.8" } }, + "node_modules/verkit": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.3.2.tgz", + "integrity": "sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/vite": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", @@ -6747,6 +7800,119 @@ "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, + "node_modules/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/watchpack": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", @@ -6768,6 +7934,40 @@ "license": "MIT", "optional": true }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6784,16 +7984,32 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", - "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.2.tgz", + "integrity": "sha512-6OLRNZqRntVGBm26ghZ5eZhCuWWyOkRCSn8XsOIuq5stpCE/tjLbdozd93naILZig0lmD8ANfRwnhn0DLeXGlg==", "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.2.3", - "string-width": "^8.2.0", - "strip-ansi": "^7.1.2" + "string-width": "^8.2.0" }, "engines": { "node": ">=20" @@ -6809,6 +8025,45 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5097996..2019117 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "praktikum-frontend", - "version": "1.0.0", + "version": "1.1.0", "private": true, "scripts": { "start": "ng serve --proxy-config proxy.conf.json", @@ -8,24 +8,32 @@ "test": "ng test --watch=false" }, "dependencies": { - "@angular/animations": "^22.0.0", - "@angular/cdk": "^22.0.0", - "@angular/common": "^22.0.0", - "@angular/compiler": "^22.0.0", - "@angular/core": "^22.0.0", - "@angular/forms": "^22.0.0", - "@angular/material": "^22.0.0", - "@angular/platform-browser": "^22.0.0", - "@angular/router": "^22.0.0", + "@angular/animations": "22.2.1", + "@angular/cdk": "22.2.1", + "@angular/common": "22.2.1", + "@angular/compiler": "22.2.1", + "@angular/core": "22.2.1", + "@angular/forms": "22.2.1", + "@angular/material": "22.2.1", + "@angular/platform-browser": "22.2.1", + "@angular/router": "22.2.1", "@fontsource-variable/inter": "^5.3.0", "material-icons": "^1.13.14", "rxjs": "~7.8.2", "tslib": "^2.8.1" }, "devDependencies": { - "@angular/build": "^22.0.0", - "@angular/cli": "^22.0.0", - "@angular/compiler-cli": "^22.0.0", - "typescript": "~6.0.0" + "@angular/build": "22.1.4", + "@angular/cli": "22.1.4", + "@angular/compiler-cli": "22.2.1", + "typescript": "~6.0.2", + "vitest": "^4.0.8", + "jsdom": "^27.0.1", + "@types/node": "^24.10.1" + }, + "overrides": { + "@angular/build": { + "piscina": "5.3.2" + } } } diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index b3d0cae..2530a7b 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -77,7 +77,7 @@ import { ApiService } from "./core/api.service"; - Build 2026.08.18.1 + Build 2026.10.02.1 { + it('accepts HTTPS and loopback development, and rejects remote plaintext pages',()=>{ + expect(secureTransferOrigin({protocol:'https:',hostname:'practice.example'})).toBe(true); + expect(secureTransferOrigin({protocol:'http:',hostname:'localhost'})).toBe(true); + expect(secureTransferOrigin({protocol:'http:',hostname:'practice.example'})).toBe(false); + expect(secureTransferOrigin({protocol:'http:',hostname:'localhost.example'})).toBe(false); + expect(secureTransferOrigin({protocol:'file:',hostname:''})).toBe(false); + }); +}); diff --git a/frontend/src/app/core/secure-transfer-origin.ts b/frontend/src/app/core/secure-transfer-origin.ts new file mode 100644 index 0000000..1d67fc8 --- /dev/null +++ b/frontend/src/app/core/secure-transfer-origin.ts @@ -0,0 +1,4 @@ +/** Never submit an export password from an unencrypted remote page. */ +export function secureTransferOrigin(origin:Pick=globalThis.location):boolean { + return origin.protocol==='https:' || (origin.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(origin.hostname)); +} diff --git a/frontend/src/app/pages/practice-transfer-export.component.html b/frontend/src/app/pages/practice-transfer-export.component.html new file mode 100644 index 0000000..32aa1e3 --- /dev/null +++ b/frontend/src/app/pages/practice-transfer-export.component.html @@ -0,0 +1,17 @@ + +

Fahrten und Ausbildung an die Praxisverwaltung übergeben

Verschlüsselter Export nur aus deinem angemeldeten Konto. In der Praxisverwaltung unter „Schnell finden → Praktikumsdaten importieren“ die Datei öffnen, Vorschau prüfen und Einträge auswählen.

+

Für den Datenaustausch beide Anwendungen über HTTPS öffnen.

+ @if(!transportSafe){

Bitte die Anwendung über HTTPS öffnen. Der Export ist über eine unverschlüsselte Verbindung gesperrt.

} +
+ Zeitraum von + Zeitraum bis + Eigenes ExportpasswortMindestens 12 Zeichen. Kein Kontopasswort verwenden. + Exportpasswort wiederholen +
Beschreibungen bewusst mitnehmen

Standardmäßig werden nur Datum, Kategorie, Minuten, Kilometer und Kosten übertragen. Beschreibungen können persönliche Angaben enthalten. Nicht benötigte Angaben vor dem Export entfernen.

+

Maximal 500 Einträge je Export. Laufende Zeiterfassungen werden ausgelassen. Kosten werden als Notiz übernommen, ohne Buchung. Nachweise und fachliche Anrechnung anschließend in der Praxisverwaltung prüfen. Passwort und Datei getrennt aufbewahren.

+ +
+ @if(busy()){

Export wird verschlüsselt …

} + @if(error()){

{{error()}}

} + @if(success()){

Export erstellt. Ausgelassene laufende Zeiterfassungen: {{skipped()}}. Die Datei kann jetzt in der Praxisverwaltung eingelesen werden.

} +
diff --git a/frontend/src/app/pages/practice-transfer-export.component.scss b/frontend/src/app/pages/practice-transfer-export.component.scss new file mode 100644 index 0000000..2f2b61b --- /dev/null +++ b/frontend/src/app/pages/practice-transfer-export.component.scss @@ -0,0 +1 @@ +:host{display:block;margin:0 0 24px}.transfer-export{padding:24px;gap:20px}header{border-bottom:1px solid var(--app-border);padding-bottom:16px}h2,p{margin:0}p{line-height:1.6;margin-top:8px}form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start;gap:24px}.full{grid-column:1/-1}.full p{font-size:.9rem;color:var(--app-muted)}button{justify-self:start;grid-column:1/-1}@media(max-width:600px){form{grid-template-columns:1fr}.transfer-export{padding:16px}button{width:100%;height:auto;min-height:48px}} diff --git a/frontend/src/app/pages/practice-transfer-export.component.spec.ts b/frontend/src/app/pages/practice-transfer-export.component.spec.ts new file mode 100644 index 0000000..e0c24fd --- /dev/null +++ b/frontend/src/app/pages/practice-transfer-export.component.spec.ts @@ -0,0 +1,30 @@ +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; +import { PracticeTransferExportComponent } from './practice-transfer-export.component'; +describe('Protected practice export',()=>{ + let component:PracticeTransferExportComponent;let http:HttpTestingController; + beforeEach(()=>{ + TestBed.configureTestingModule({imports:[PracticeTransferExportComponent],providers:[provideHttpClient(),provideHttpClientTesting()]}); + component=TestBed.createComponent(PracticeTransferExportComponent).componentInstance;http=TestBed.inject(HttpTestingController); + component.form.patchValue({from:'2024-01-01',to:'2024-12-31',password:'separate-transfer-passphrase',confirmation:'separate-transfer-passphrase'}); + }); + afterEach(()=>{http.verify();vi.restoreAllMocks();}); + it('requires matching passwords and a valid date range before contacting the server',()=>{ + component.form.controls.confirmation.setValue('different');component.export();http.expectNone('/api/v1/practice_transfer');expect(component.error()).toContain('stimmen nicht'); + component.form.controls.confirmation.setValue('separate-transfer-passphrase');component.form.controls.to.setValue('2023-01-01');component.export();http.expectNone('/api/v1/practice_transfer'); + }); + it('excludes descriptions by default and clears passwords after an encrypted download',()=>{ + Object.defineProperty(URL,'createObjectURL',{configurable:true,value:vi.fn(()=> 'blob:test')});Object.defineProperty(URL,'revokeObjectURL',{configurable:true,value:vi.fn()}); + vi.spyOn(HTMLAnchorElement.prototype,'click').mockImplementation(()=>{}); + component.export();const request=http.expectOne('/api/v1/practice_transfer');expect(request.request.body.include_descriptions).toBe(false);expect(request.request.body.confirmation).toBeUndefined(); + request.flush(new Blob(['ciphertext']),{headers:{'X-Transfer-Skipped-Running':'2'}}); + expect(component.skipped()).toBe(2);expect(component.success()).toBe(true);expect(component.form.controls.password.value).toBe('');expect(component.form.controls.confirmation.value).toBe(''); + }); + it('retains the form after a rejected export and allows correction',async ()=>{ + component.export();http.expectOne('/api/v1/practice_transfer').flush(new Blob([JSON.stringify({error:'Nicht angemeldet'})],{type:'application/json'}),{status:401,statusText:'Unauthorized'}); + await vi.waitFor(()=>expect(component.busy()).toBe(false)); + expect(component.busy()).toBe(false);expect(component.success()).toBe(false);expect(component.form.controls.password.value).toBe('separate-transfer-passphrase'); + }); +}); diff --git a/frontend/src/app/pages/practice-transfer-export.component.ts b/frontend/src/app/pages/practice-transfer-export.component.ts new file mode 100644 index 0000000..cb66f76 --- /dev/null +++ b/frontend/src/app/pages/practice-transfer-export.component.ts @@ -0,0 +1,29 @@ +import { secureTransferOrigin } from '../core/secure-transfer-origin'; +import { Component, inject, signal } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { MATERIAL } from '../shared/material'; +@Component({selector:'app-practice-transfer-export',standalone:true,imports:[ReactiveFormsModule,...MATERIAL], + templateUrl:'./practice-transfer-export.component.html',styleUrl:'./practice-transfer-export.component.scss'}) +export class PracticeTransferExportComponent { + private readonly http=inject(HttpClient); + readonly transportSafe=secureTransferOrigin(); + readonly busy=signal(false); readonly error=signal(''); readonly success=signal(false); readonly skipped=signal(0); + readonly form=inject(FormBuilder).nonNullable.group({from:[`${new Date().getFullYear()}-01-01`,Validators.required],to:[new Date().toLocaleDateString('sv-SE'),Validators.required],password:['',[Validators.required,Validators.minLength(12),Validators.maxLength(256)]],confirmation:['',Validators.required],include_descriptions:false}); + export():void{ + if(this.busy()||!this.transportSafe)return;this.error.set('');this.success.set(false); + const value=this.form.getRawValue(); + if(this.form.invalid){this.form.markAllAsTouched();this.error.set('Bitte Zeitraum und Exportpasswort mit mindestens 12 Zeichen angeben.');return;} + if(value.password!==value.confirmation){this.error.set('Die Passwörter stimmen nicht überein.');return;} + if(value.from>value.to){this.error.set('Das Ende muss am oder nach dem Beginn liegen.');return;} + this.busy.set(true); + this.http.post('/api/v1/practice_transfer',{from:value.from,to:value.to,transfer_password:value.password,include_descriptions:value.include_descriptions},{observe:'response',responseType:'blob'}).subscribe({ + next:response=>{ + const file=response.body;if(file){const url=URL.createObjectURL(file);const link=document.createElement('a');link.href=url;link.download=`praktikum-praxis-${value.from}-${value.to}.pcmtransfer`;link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);} + this.skipped.set(Number(response.headers.get('X-Transfer-Skipped-Running'))||0);this.success.set(true);this.busy.set(false); + this.form.controls.password.reset();this.form.controls.confirmation.reset(); + }, + error:async response=>{let message='Export fehlgeschlagen. Bitte erneut versuchen.';try{const body=response.error instanceof Blob?JSON.parse(await response.error.text()):response.error;if(typeof body?.error==='string')message=body.error;}catch{}this.error.set(message);this.busy.set(false);} + }); + } +} diff --git a/frontend/src/app/pages/reports.component.ts b/frontend/src/app/pages/reports.component.ts index 0788dd8..501acdb 100644 --- a/frontend/src/app/pages/reports.component.ts +++ b/frontend/src/app/pages/reports.component.ts @@ -1,3 +1,4 @@ +import { PracticeTransferExportComponent } from './practice-transfer-export.component'; import { Component, OnInit, signal } from "@angular/core"; import { DatePipe } from "@angular/common"; import { MATERIAL } from "../shared/material"; @@ -12,7 +13,7 @@ interface Row { @Component({ standalone: true, - imports: [DatePipe, ...MATERIAL], + imports: [PracticeTransferExportComponent, DatePipe, ...MATERIAL], template: `
@@ -20,6 +21,7 @@ interface Row {

Geleistete Zeiten nach Monat und Kategorie

+ @if (loading()) { }
@for (group of groups(); track group.month) { diff --git a/frontend/tsconfig.spec.json b/frontend/tsconfig.spec.json new file mode 100644 index 0000000..e38c997 --- /dev/null +++ b/frontend/tsconfig.spec.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "node" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/test/practice_transfer_test.rb b/test/practice_transfer_test.rb new file mode 100644 index 0000000..3a5a812 --- /dev/null +++ b/test/practice_transfer_test.rb @@ -0,0 +1,41 @@ +ENV["RAILS_ENV"] = "test" +require_relative "../config/environment" +require "rails/test_help" + +class PracticeTransferTest < ActionDispatch::IntegrationTest + setup do + @user=User.create!(email:"export@example.test",password:"secure-test-password-123",password_confirmation:"secure-test-password-123",api_token_digest:Digest::SHA256.hexdigest("test-transfer-token")) + @other=User.create!(email:"other-export@example.test",password:"secure-test-password-123",password_confirmation:"secure-test-password-123") + @entry=@user.entries.create!(date:"2024-06-15",praktikums_typ:"propädeutikum",entry_art:"Supervision",hours:1,minutes:31,distance_km:20,beschreibung:"Confidential description",kosten:120) + @other.entries.create!(date:"2024-06-15",praktikums_typ:"propädeutikum",entry_art:"Supervision",hours:9,minutes:0,distance_km:999) + @password="separate-transfer-passphrase" + @params={from:"2024-01-01",to:"2024-12-31",transfer_password:@password} + end + test "missing and non bearer tokens never select a user without a token" do + post "/api/v1/practice_transfer",params:@params + assert_response :unauthorized + post "/api/v1/practice_transfer",params:@params,headers:{"Authorization"=>"Basic test-transfer-token"} + assert_response :unauthorized + end + test "exports only own records, excludes descriptions by default and retains source identities" do + post "/api/v1/practice_transfer",params:@params,headers:{"Authorization"=>"Bearer test-transfer-token"} + assert_response :success + assert_includes response.headers["Cache-Control"],"no-store" + payload=ProfessionalTransferEnvelope.decrypt(response.body,@password) + assert_equal 2,payload["records"].length + assert_equal [@entry.id.to_s],payload["records"].map { |r| r["source_id"] }.uniq + assert payload["records"].all? { |r| r["description"].nil? } + assert payload["records"].all? { |r| r["duration_minutes"]==91 } + namespace=payload["source_namespace"] + post "/api/v1/practice_transfer",params:@params.merge(include_descriptions:true),headers:{"Authorization"=>"Bearer test-transfer-token"} + second=ProfessionalTransferEnvelope.decrypt(response.body,@password) + assert_equal namespace,second["source_namespace"] + assert_equal "Confidential description",second["records"].first["description"] + end + test "envelope rejects tampering and incorrect passwords" do + content=ProfessionalTransferEnvelope.encrypt({"value"=>42},@password) + assert_raises(ProfessionalTransferEnvelope::Invalid) { ProfessionalTransferEnvelope.decrypt(content,"wrong-transfer-password") } + envelope=JSON.parse(content);envelope["tag"]=Base64.strict_encode64("0"*16) + assert_raises(ProfessionalTransferEnvelope::Invalid) { ProfessionalTransferEnvelope.decrypt(JSON.generate(envelope),@password) } + end +end