From b305ecb7a4dd0758e258240f0da67314304df701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jovan=20Kruni=C4=87?= Date: Fri, 2 Oct 2026 17:41:11 +0200 Subject: [PATCH] WIP --- backend/backend/config/f-u/backendrc.js | 26 +- backend/backend/package.json | 2 +- frontend/app/capacitor.config.ts | 2 +- frontend/app/ios/App/Podfile | 2 +- frontend/app/package.json | 7 +- frontend/app/src/app/app.component.ts | 10 +- frontend/app/src/app/app.module.ts | 8 +- .../assessments/assessments.provider.ts | 6 +- .../page/auth-callback-page.component.ts | 56 +- .../app/modules/auth/auth-guard.service.ts | 8 +- .../modules/auth/auth-helper.service.spec.ts | 337 +++-- .../app/modules/auth/auth-helper.service.ts | 159 ++- .../app/src/app/modules/auth/auth-paths.ts | 12 +- .../app/modules/auth/auth-routing.module.ts | 4 +- .../app/src/app/modules/auth/auth.module.ts | 14 +- .../app/modules/auth/auth.provider.methods.ts | 103 +- .../app/modules/auth/auth.provider.spec.ts | 239 ++++ .../app/src/app/modules/auth/auth.provider.ts | 1122 ++++++++++++++++ .../app/src/app/modules/auth/auth.service.ts | 526 -------- ...capacitor-authorization-request-handler.ts | 270 ++++ .../app/modules/auth/capacitor-requestor.ts | 106 +- .../modules/auth/default-auth.service.spec.ts | 104 -- .../app/modules/auth/default-auth.service.ts | 102 -- .../{http.factory.ts => requestor.factory.ts} | 8 +- .../modules/auth/factories/storage.factory.ts | 8 +- .../src/app/modules/auth/ng-http.service.ts | 77 -- .../src/app/modules/auth/user-info.model.ts | 21 - .../app/modules/profile/id-cards.provider.ts | 7 +- .../src/app/modules/profile/id-cards.spec.ts | 1 - .../page/profile-page-section.component.ts | 12 +- .../profile/page/profile-page.component.ts | 24 +- .../storage/capacitor-secure-storage.ts | 61 +- .../environments/environment.production.ts | 4 +- frontend/app/src/environments/environment.ts | 3 +- pnpm-lock.yaml | 1133 ++++++++--------- 35 files changed, 2771 insertions(+), 1813 deletions(-) create mode 100644 frontend/app/src/app/modules/auth/auth.provider.spec.ts create mode 100644 frontend/app/src/app/modules/auth/auth.provider.ts delete mode 100644 frontend/app/src/app/modules/auth/auth.service.ts create mode 100644 frontend/app/src/app/modules/auth/capacitor-authorization-request-handler.ts delete mode 100644 frontend/app/src/app/modules/auth/default-auth.service.spec.ts delete mode 100644 frontend/app/src/app/modules/auth/default-auth.service.ts rename frontend/app/src/app/modules/auth/factories/{http.factory.ts => requestor.factory.ts} (76%) delete mode 100644 frontend/app/src/app/modules/auth/ng-http.service.ts delete mode 100644 frontend/app/src/app/modules/auth/user-info.model.ts diff --git a/backend/backend/config/f-u/backendrc.js b/backend/backend/config/f-u/backendrc.js index 8138cdc2..299129f9 100644 --- a/backend/backend/config/f-u/backendrc.js +++ b/backend/backend/config/f-u/backendrc.js @@ -13,23 +13,23 @@ const config = { default: { client: { clientId: '1cac3f99-33fa-4234-8438-979f07e0cdab', - scopes: '', - url: 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0', + scopes: 'openid profile email offline_access', + url: 'https://idp.ub.uni-frankfurt.de/idp/profile/oidc', }, endpoints: { - authorization: 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0/authorize', - endSession: 'https://cas.rz.uni-frankfurt.de/cas/logout', + authorization: 'https://idp.ub.uni-frankfurt.de/idp/profile/oidc/authorize', + endSession: 'https://idp.ub.uni-frankfurt.de/idp/profile/oidc/end-session', mapping: { - id: '$.id', - email: '$.attributes.mailPrimaryAddress', - familyName: '$.attributes.sn', - givenName: '$.attributes.givenName', - name: '$.attributes.givenName', - role: '$.attributes.eduPersonPrimaryAffiliation', - studentId: '$.attributes.employeeNumber', + id: '$.preferred_username', + email: '$.mailPrimaryAddress', + familyName: '$.family_name', + givenName: '$.given_name', + name: '$.name', + role: '$.eduPersonPrimaryAffiliation', + studentId: '$.employeeNumber', }, - token: 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0/accessToken', - userinfo: 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0/profile', + token: 'https://idp.ub.uni-frankfurt.de/idp/profile/oidc/token', + userinfo: 'https://idp.ub.uni-frankfurt.de/idp/profile/oidc/userinfo', }, }, }, diff --git a/backend/backend/package.json b/backend/backend/package.json index 47ae6ac7..4c7bccce 100644 --- a/backend/backend/package.json +++ b/backend/backend/package.json @@ -36,7 +36,7 @@ "format:fix": "prettier --write . --ignore-path ../../.gitignore", "lint": "tsc --noEmit && eslint --ext .ts src/", "lint:fix": "eslint --fix --ext .ts src/", - "start": "cross-env NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true node app.js", + "start": "cross-env NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true NODE_APP_INSTANCE=\"f-u\" node app.js", "start:debug": "cross-env STAPPS_LOG_LEVEL=31 NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true node app.js", "test": "pnpm run test:unit", "test:integration": "sh integration-test.sh", diff --git a/frontend/app/capacitor.config.ts b/frontend/app/capacitor.config.ts index f3bee977..cf4b22b3 100644 --- a/frontend/app/capacitor.config.ts +++ b/frontend/app/capacitor.config.ts @@ -1,7 +1,7 @@ import {CapacitorConfig} from '@capacitor/cli'; const config: CapacitorConfig = { - appId: 'de.anyschool.app', + appId: 'de.unifrankfurt.app', appName: 'StApps', webDir: 'www/browser', cordova: { diff --git a/frontend/app/ios/App/Podfile b/frontend/app/ios/App/Podfile index fd9d9d84..fe5ca4cd 100644 --- a/frontend/app/ios/App/Podfile +++ b/frontend/app/ios/App/Podfile @@ -29,7 +29,7 @@ def capacitor_pods pod 'CapacitorShare', :path => '../../../../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.2.0/node_modules/@capacitor/share' pod 'CapacitorSplashScreen', :path => '../../../../node_modules/.pnpm/@capacitor+splash-screen@8.0.1_@capacitor+core@8.2.0/node_modules/@capacitor/splash-screen' pod 'TransistorsoftCapacitorBackgroundFetch', :path => '../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@8.0.0_@capacitor+core@8.2.0/node_modules/@transistorsoft/capacitor-background-fetch' - pod 'CapacitorSecureStoragePlugin', :path => '../../../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.12.0_@capacitor+core@8.2.0/node_modules/capacitor-secure-storage-plugin' + pod 'CapacitorSecureStoragePlugin', :path => '../../../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.13.0_@capacitor+core@8.2.0/node_modules/capacitor-secure-storage-plugin' pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins' end diff --git a/frontend/app/package.json b/frontend/app/package.json index 5c677aa1..c7985a80 100644 --- a/frontend/app/package.json +++ b/frontend/app/package.json @@ -41,7 +41,7 @@ "resources:ios": "capacitor-assets generate --ios --iconBackgroundColor $(grep -oE \"^@include ion-color\\(primary, #[a-fA-F0-9]{3,6}\" src/theme/colors.scss | grep -oE \"#[a-fA-F0-9]{3,6}\") --splashBackgroundColor $(grep -oE \"^@include ion-color\\(primary, #[a-fA-F0-9]{3,6}\" src/theme/colors.scss | grep -oE \"#[a-fA-F0-9]{3,6}\")", "run:android": "ionic capacitor run android --livereload --external", "start": "ionic serve", - "start:external": "ionic serve --external", + "start:external": "ionic serve --external --ssl=true", "start:prod": "ionic serve --prod", "start:virtual-host": "ionic serve --public-host=mobile.app.uni-frankfurt.de --ssl=true --open=false", "test": "ng test --code-coverage", @@ -81,7 +81,7 @@ "@maplibre/ngx-maplibre-gl": "22.1.0", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "@openid/appauth": "1.3.2", + "@openid/appauth": "1.4.0", "@openstapps/api": "workspace:*", "@openstapps/collection-utils": "workspace:*", "@openstapps/core": "workspace:*", @@ -93,8 +93,7 @@ "deepmerge": "4.3.1", "form-data": "4.0.6", "geojson": "0.5.0", - "ionic-appauth": "2.1.0", - "ionicons": "8.1.0", + "ionicons": "8.0.13", "jsonpath-plus": "10.3.0", "maplibre-gl": "6.4.1", "material-symbols": "0.17.1", diff --git a/frontend/app/src/app/app.component.ts b/frontend/app/src/app/app.component.ts index a5ecf0bb..827a7626 100644 --- a/frontend/app/src/app/app.component.ts +++ b/frontend/app/src/app/app.component.ts @@ -18,13 +18,14 @@ import {App, URLOpenListenerEvent} from '@capacitor/app'; import {Platform, ToastController} from '@ionic/angular/standalone'; import {SettingsProvider} from './modules/settings/settings.provider'; import {InAppReviewProvider} from './modules/settings/in-app-review/in-app-review.provider'; -import {AuthHelperService} from './modules/auth/auth-helper.service'; +import {AuthProvider} from "./modules/auth/auth.provider"; import {environment} from '../environments/environment'; import {Capacitor} from '@capacitor/core'; import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service'; import {Keyboard, KeyboardResize} from '@capacitor/keyboard'; import {AppVersionService} from './modules/about/app-version.service'; import {SplashScreen} from '@capacitor/splash-screen'; +import {AuthHelperService} from "./modules/auth/auth-helper.service"; /** * TODO @@ -45,6 +46,8 @@ export class AppComponent implements AfterContentInit { private readonly zone = inject(NgZone); + private readonly authProvider = inject(AuthProvider); + private readonly authHelper = inject(AuthHelperService); private readonly toastController = inject(ToastController); @@ -129,9 +132,8 @@ export class AppComponent implements AfterContentInit { } private async authNotificationsInit() { - this.authHelper - .getProvider() - .events$.subscribe(action => this.showMessage(this.authHelper.getAuthMessage('default', action))); + this.authProvider + .events$.subscribe(action => this.showMessage(this.authHelper.getAuthMessage(action))); } private async showMessage(message?: string) { diff --git a/frontend/app/src/app/app.module.ts b/frontend/app/src/app/app.module.ts index 894e72f4..9f974b7c 100644 --- a/frontend/app/src/app/app.module.ts +++ b/frontend/app/src/app/app.module.ts @@ -57,7 +57,6 @@ import {AssessmentsModule} from './modules/assessments/assessments.module'; import {ServiceHandlerInterceptor} from './_helpers/service-handler.interceptor'; import {RoutingStackService} from './util/routing-stack.service'; import {SCLanguageCode, SCSettingValue} from '@openstapps/core'; -import {DefaultAuthService} from './modules/auth/default-auth.service'; import {NavigationModule} from './modules/menu/navigation/navigation.module'; import {browserFactory, SimpleBrowser} from './util/browser.factory'; import {getDateFnsLocale} from './translation/dfns-locale'; @@ -67,6 +66,7 @@ import {Capacitor} from '@capacitor/core'; import {SplashScreen} from '@capacitor/splash-screen'; import * as maplibregl from 'maplibre-gl'; import {Protocol} from 'pmtiles'; +import {AuthProvider} from "./modules/auth/auth.provider"; registerLocaleData(localeDe); @@ -82,7 +82,7 @@ export function initializerFactory( configProvider: ConfigProvider, translateService: TranslateService, _routingStackService: RoutingStackService, - defaultAuthService: DefaultAuthService, + authProvider: AuthProvider, dateFnsConfigurationService: DateFnsConfigurationService, ) { return async () => { @@ -111,7 +111,7 @@ export function initializerFactory( setDefaultOptions({locale: dateFnsLocale}); dateFnsConfigurationService.setLocale(dateFnsLocale); - await defaultAuthService.init(); + await authProvider.init(); } catch (error) { logger.warn(error); } @@ -201,7 +201,7 @@ export function createTranslateLoader(http: HttpClient) { ConfigProvider, TranslateService, RoutingStackService, - DefaultAuthService, + AuthProvider, DateFnsConfigurationService, ], useFactory: initializerFactory, diff --git a/frontend/app/src/app/modules/assessments/assessments.provider.ts b/frontend/app/src/app/modules/assessments/assessments.provider.ts index 6c419e75..e6528267 100644 --- a/frontend/app/src/app/modules/assessments/assessments.provider.ts +++ b/frontend/app/src/app/modules/assessments/assessments.provider.ts @@ -15,9 +15,9 @@ import {Injectable, inject} from '@angular/core'; import {ConfigProvider} from '../config/config.provider'; import {SCAssessment, SCUuid} from '@openstapps/core'; -import {DefaultAuthService} from '../auth/default-auth.service'; import {HttpClient} from '@angular/common/http'; import {uniqBy, keyBy} from '@openstapps/collection-utils'; +import {AuthProvider} from "../auth/auth.provider"; /** * @@ -53,7 +53,7 @@ export function toAssessmentMap(data: SCAssessment[]): Record(`${url}/${this.assessmentPath}`, { headers: { - Authorization: `Bearer ${accessToken ?? (await this.defaultAuth.getValidToken()).accessToken}`, + Authorization: `Bearer ${accessToken ?? (await this.authProvider.getValidToken()).accessToken}`, }, }) .toPromise() diff --git a/frontend/app/src/app/modules/auth/auth-callback/page/auth-callback-page.component.ts b/frontend/app/src/app/modules/auth/auth-callback/page/auth-callback-page.component.ts index d3a76cd7..d3d7a18b 100644 --- a/frontend/app/src/app/modules/auth/auth-callback/page/auth-callback-page.component.ts +++ b/frontend/app/src/app/modules/auth/auth-callback/page/auth-callback-page.component.ts @@ -13,13 +13,16 @@ * this program. If not, see . */ import {Component, inject} from '@angular/core'; -import {NavController} from '@ionic/angular/standalone'; import {Router} from '@angular/router'; -import {AuthActions, IAuthAction} from 'ionic-appauth'; -import {AuthHelperService} from '../../auth-helper.service'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; -import {Observable} from 'rxjs'; -import {DefaultAuthService} from '../../default-auth.service'; +import {NavController} from '@ionic/angular/standalone'; + +import { + AuthActions, + AuthProvider, + IAuthAction, +} from '../../auth.provider'; +import {AuthHelperService} from '../../auth-helper.service'; @Component({ templateUrl: 'auth-callback-page.component.html', @@ -33,23 +36,42 @@ export class AuthCallbackPageComponent { private authHelper = inject(AuthHelperService); - private auth = inject(DefaultAuthService); + private authProvider = inject(AuthProvider); constructor() { - const events: Observable = this.auth.events$; + this.authProvider.events$ + .pipe(takeUntilDestroyed()) + .subscribe(action => { + void this.postCallback(action); + }); - events.pipe(takeUntilDestroyed()).subscribe((action: IAuthAction) => this.postCallback(action)); - this.auth.authorizationCallback(window.location.origin + this.router.url); + this.authProvider.authorizationCallback( + window.location.href, + ); } - async postCallback(action: IAuthAction) { - if (action.action === AuthActions.SignInSuccess) { - const originPath = await this.authHelper.getOriginPath(); - this.navCtrl.navigateRoot(originPath ?? 'profile'); - this.authHelper.deleteOriginPath(); - } - if (action.action === AuthActions.SignInFailed) { - this.navCtrl.navigateRoot('profile'); + private async postCallback( + action: IAuthAction, + ): Promise { + switch (action.action) { + case AuthActions.SignInSuccess: { + const originPath = + await this.authHelper.getOriginPath(); + + await this.authHelper.deleteOriginPath(); + + await this.navCtrl.navigateRoot( + originPath ?? 'profile', + ); + + break; + } + + case AuthActions.SignInFailed: { + await this.navCtrl.navigateRoot('profile'); + + break; + } } } } diff --git a/frontend/app/src/app/modules/auth/auth-guard.service.ts b/frontend/app/src/app/modules/auth/auth-guard.service.ts index a700f532..ead61617 100644 --- a/frontend/app/src/app/modules/auth/auth-guard.service.ts +++ b/frontend/app/src/app/modules/auth/auth-guard.service.ts @@ -16,13 +16,13 @@ import {Injectable, inject} from '@angular/core'; import {CanActivate, NavigationExtras, Router, RouterStateSnapshot} from '@angular/router'; import {ActivatedProtectedRouteSnapshot} from './protected.routes'; -import {AuthHelperService} from './auth-helper.service'; +import {AuthProvider} from "./auth.provider"; @Injectable({ providedIn: 'root', }) export class AuthGuardService implements CanActivate { - private authHelper = inject(AuthHelperService); + private authProvider = inject(AuthProvider); private router = inject(Router); @@ -32,7 +32,7 @@ export class AuthGuardService implements CanActivate { } try { - await this.authHelper.getProvider().getValidToken(); + await this.authProvider.getValidToken(); } catch { const originNavigation = this.router.currentNavigation(); let extras: NavigationExtras = {}; @@ -41,7 +41,7 @@ export class AuthGuardService implements CanActivate { extras = {queryParams: {origin_path: url}}; } this.router.navigate(['profile'], extras); - await this.authHelper.getProvider().signIn(); + await this.authProvider.signIn(); return false; } diff --git a/frontend/app/src/app/modules/auth/auth-helper.service.spec.ts b/frontend/app/src/app/modules/auth/auth-helper.service.spec.ts index 69a98d1a..0003c66e 100644 --- a/frontend/app/src/app/modules/auth/auth-helper.service.spec.ts +++ b/frontend/app/src/app/modules/auth/auth-helper.service.spec.ts @@ -1,118 +1,251 @@ -/* - * Copyright (C) 2023 StApps - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ -import {TestBed} from '@angular/core/testing'; +import {AuthActions, AuthProvider} from './auth.provider'; import {AuthHelperService} from './auth-helper.service'; -import {ConfigProvider} from '../config/config.provider'; -import {StorageProvider} from '../storage/storage.provider'; -import {DefaultAuthService} from './default-auth.service'; -import {Browser} from 'ionic-appauth'; -import {Requestor, StorageBackend} from '@openid/appauth'; -import {TranslateService} from '@ngx-translate/core'; -import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider'; -import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http'; -import {SimpleBrowser} from '../../util/browser.factory'; -import {LoggerTestingModule} from 'ngx-logger/testing'; + +const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path'; +const AUTH_MESSAGE_PREFIX = 'auth.messages.default'; +const END_SESSION_ENDPOINT = 'https://idp.example.org/logout'; + +const USER_MAPPING = { + id: '$.sub', + givenName: '$.given_name', + familyName: '$.family_name', + email: '$.attributes.mail', + role: '$.role', +}; + +interface TestAlertButton { + text: string; + role?: string; + cssClass?: string; + handler?: () => Promise | void; +} describe('AuthHelperService', () => { - let authHelperService: AuthHelperService; - const storageProviderSpy = jasmine.createSpyObj('StorageProvider', ['init', 'get', 'has', 'put', 'search']); - const translateServiceSpy = jasmine.createSpyObj('TranslateService', ['setDefaultLang', 'use']); - const defaultAuthServiceMock = jasmine.createSpyObj('DefaultAuthService', ['init', 'setupConfiguration']); - const authHelperServiceMock = jasmine.createSpyObj('AuthHelperService', ['constructor']); - const simpleBrowserMock = jasmine.createSpyObj('SimpleBrowser', ['open']); - const configProvider = jasmine.createSpyObj('ConfigProvider', { - getAnyValue: { - default: { - endpoints: { - mapping: { - id: '$.id', - email: '$.attributes.mailPrimaryAddress', - givenName: '$.attributes.givenName', - familyName: '$.attributes.sn', - name: '$.attributes.sn', - role: '$.attributes.eduPersonPrimaryAffiliation', - studentId: '$.attributes.employeeNumber', - }, - }, - }, - }, - }); + let service: AuthHelperService; + + let translateService: jasmine.SpyObj<{instant(key: string): string}>; + let configProvider: jasmine.SpyObj<{getAnyValue(key: string): unknown}>; + let storageProvider: jasmine.SpyObj<{ + get(key: string): Promise; + put(key: string, value: unknown): Promise; + delete(key: string): Promise; + }>; + let authProvider: jasmine.SpyObj>; + let browser: jasmine.SpyObj<{open(url: string): Promise}>; + let alertController: jasmine.SpyObj<{create(options: unknown): Promise}>; + let alert: jasmine.SpyObj<{present(): Promise}>; + + function createService(authConfig: unknown = {default: {endpoints: {mapping: USER_MAPPING}}}) { + configProvider.getAnyValue.and.returnValue(authConfig); + + return new AuthHelperService( + translateService as any, + configProvider as any, + storageProvider as any, + authProvider as any, + browser as any, + alertController as any, + ); + } + + /** Options passed to the most recent AlertController.create() call. */ + function lastAlertOptions(): {header: string; message: string; buttons: TestAlertButton[]} { + return alertController.create.calls.mostRecent().args[0] as any; + } + + function alertButton(role: string): TestAlertButton { + const button = lastAlertOptions().buttons.find(b => b.role === role); + if (!button) { + throw new Error(`No alert button with role "${role}"`); + } + return button; + } beforeEach(() => { - TestBed.configureTestingModule({ - imports: [LoggerTestingModule], - providers: [ - StAppsWebHttpClient, - { - provide: TranslateService, - useValue: translateServiceSpy, - }, - { - provide: StorageProvider, - useValue: storageProviderSpy, - }, - { - provider: DefaultAuthService, - useValue: defaultAuthServiceMock, - }, - { - provide: ConfigProvider, - useValue: configProvider, - }, - Browser, - StorageBackend, - Requestor, - { - provider: AuthHelperService, - useValue: authHelperServiceMock, - }, - { - provide: SimpleBrowser, - useValue: simpleBrowserMock, - }, - provideHttpClient(withInterceptorsFromDi()), - ], + translateService = jasmine.createSpyObj('TranslateService', ['instant']); + translateService.instant.and.callFake((key: string) => `translated:${key}`); + + configProvider = jasmine.createSpyObj('ConfigProvider', ['getAnyValue']); + + storageProvider = jasmine.createSpyObj('StorageProvider', ['get', 'put', 'delete']); + storageProvider.put.and.resolveTo(); + storageProvider.delete.and.resolveTo(); + + authProvider = jasmine.createSpyObj('AuthProvider', ['getEndSessionEndpoint']); + authProvider.getEndSessionEndpoint.and.resolveTo(END_SESSION_ENDPOINT); + + browser = jasmine.createSpyObj('SimpleBrowser', ['open']); + browser.open.and.resolveTo(); + + alert = jasmine.createSpyObj('HTMLIonAlertElement', ['present']); + alert.present.and.resolveTo(); + + alertController = jasmine.createSpyObj('AlertController', ['create']); + alertController.create.and.resolveTo(alert); + + service = createService(); + }); + + describe('userConfigurationMap', () => { + it('reads the user mapping from the default auth provider', () => { + expect(configProvider.getAnyValue).toHaveBeenCalledWith('auth'); + expect(service.userConfigurationMap).toEqual(USER_MAPPING as any); + }); + + it('falls back to an empty mapping when no default auth provider is configured', () => { + service = createService({}); + + expect(service.userConfigurationMap).toEqual({} as any); + }); + }); + + describe('getAuthMessage', () => { + it('returns the translated message for SignInSuccess', () => { + expect(service.getAuthMessage({action: AuthActions.SignInSuccess})).toBe( + `translated:${AUTH_MESSAGE_PREFIX}.logged_in_success`, + ); + }); + + it('returns the translated message for SignOutSuccess', () => { + expect(service.getAuthMessage({action: AuthActions.SignOutSuccess})).toBe( + `translated:${AUTH_MESSAGE_PREFIX}.logged_out_success`, + ); + }); + + it('returns undefined for all other actions without translating', () => { + const otherActions = Object.values(AuthActions).filter( + action => action !== AuthActions.SignInSuccess && action !== AuthActions.SignOutSuccess, + ); + + for (const action of otherActions) { + expect(service.getAuthMessage({action})).withContext(action).toBeUndefined(); + } + expect(translateService.instant).not.toHaveBeenCalled(); }); - authHelperService = TestBed.inject(AuthHelperService); }); describe('getUserFromUserInfo', () => { - it('should provide user configuration from userInfo', async () => { - const userConfiguration = authHelperService.getUserFromUserInfo({ - attributes: { - eduPersonPrimaryAffiliation: 'student', - employeeNumber: '123456', - givenName: 'Erika', - mailPrimaryAddress: 'emuster@anyschool.de', - oauthClientId: '123-abc-123', - sn: 'Musterfrau', - uid: 'emuster', - }, - id: 'emuster', - client_id: '123-abc-123', + it('maps user info fields via the configured JSONPath mapping', () => { + const user = service.getUserFromUserInfo({ + sub: 'user-123', + given_name: 'Erika', + family_name: 'Mustermann', + attributes: {mail: 'erika@example.org'}, + role: 'employee', }); - expect(userConfiguration).toEqual({ - id: 'emuster', - givenName: 'Erika', - familyName: 'Musterfrau', - name: 'Erika Musterfrau', - email: 'emuster@anyschool.de', + expect(user).toEqual( + jasmine.objectContaining({ + id: 'user-123', + givenName: 'Erika', + familyName: 'Mustermann', + email: 'erika@example.org', + role: 'employee', + }), + ); + }); + + it('builds the name from given name and family name', () => { + const user = service.getUserFromUserInfo({ + sub: 'user-123', + given_name: 'Erika', + family_name: 'Mustermann', + role: 'student', + }); + + expect(user.name).toBe('Erika Mustermann'); + }); + + it('keeps the default name when the family name is missing', () => { + const user = service.getUserFromUserInfo({ + sub: 'user-123', + given_name: 'Erika', + role: 'student', + }); + + expect(user.name).toBe(''); + }); + + it('returns the default user when no mapping is configured', () => { + service = createService({}); + + expect(service.getUserFromUserInfo({sub: 'user-123'})).toEqual({ + id: '', + name: '', role: 'student', - studentId: '123456', }); }); }); + + describe('origin path', () => { + it('stores the origin path', async () => { + await service.setOriginPath('/profile'); + + expect(storageProvider.put).toHaveBeenCalledOnceWith(AUTH_ORIGIN_PATH, '/profile'); + }); + + it('returns the stored origin path', async () => { + storageProvider.get.and.resolveTo('/profile'); + + expect(await service.getOriginPath()).toBe('/profile'); + expect(storageProvider.get).toHaveBeenCalledOnceWith(AUTH_ORIGIN_PATH); + }); + + it('returns undefined when reading the origin path fails', async () => { + storageProvider.get.and.rejectWith(new Error('Value not found')); + + expect(await service.getOriginPath()).toBeUndefined(); + }); + + it('deletes the origin path', async () => { + await service.deleteOriginPath(); + + expect(storageProvider.delete).toHaveBeenCalledOnceWith(AUTH_ORIGIN_PATH); + }); + }); + + describe('endBrowserSession', () => { + it('does nothing when the provider has no end-session endpoint', async () => { + authProvider.getEndSessionEndpoint.and.resolveTo(undefined); + + await service.endBrowserSession(); + + expect(alertController.create).not.toHaveBeenCalled(); + expect(browser.open).not.toHaveBeenCalled(); + }); + + it('presents a translated confirmation alert', async () => { + await service.endBrowserSession(); + + const options = lastAlertOptions(); + expect(options.header).toBe(`translated:${AUTH_MESSAGE_PREFIX}.log_out_alert.header`); + expect(options.message).toBe(`translated:${AUTH_MESSAGE_PREFIX}.log_out_alert.message`); + expect(options.buttons.map(b => b.role)).toEqual(['cancel', 'confirm']); + expect(alert.present).toHaveBeenCalledTimes(1); + }); + + it('does not open the end-session endpoint before confirmation', async () => { + await service.endBrowserSession(); + + expect(browser.open).not.toHaveBeenCalled(); + }); + + it('opens the end-session endpoint when the user confirms', async () => { + await service.endBrowserSession(); + + await alertButton('confirm').handler?.(); + + expect(browser.open).toHaveBeenCalledOnceWith(END_SESSION_ENDPOINT); + }); + + it('does not open the end-session endpoint when the user cancels', async () => { + await service.endBrowserSession(); + + const cancelButton = alertButton('cancel'); + await cancelButton.handler?.(); + + expect(cancelButton.handler).toBeUndefined(); + expect(browser.open).not.toHaveBeenCalled(); + }); + }); }); diff --git a/frontend/app/src/app/modules/auth/auth-helper.service.ts b/frontend/app/src/app/modules/auth/auth-helper.service.ts index 86dfdce6..546977ea 100644 --- a/frontend/app/src/app/modules/auth/auth-helper.service.ts +++ b/frontend/app/src/app/modules/auth/auth-helper.service.ts @@ -14,35 +14,38 @@ */ import {Injectable, inject} from '@angular/core'; -import {AuthActions, IAuthAction} from 'ionic-appauth'; +import {AlertController} from '@ionic/angular/standalone'; import {TranslateService} from '@ngx-translate/core'; -import {JSONPath} from 'jsonpath-plus'; import { SCAuthorizationProvider, - SCAuthorizationProviderType, SCUserConfiguration, SCUserConfigurationMap, } from '@openstapps/core'; +import {JSONPath} from 'jsonpath-plus'; + +import {SimpleBrowser} from '../../util/browser.factory'; import {ConfigProvider} from '../config/config.provider'; import {StorageProvider} from '../storage/storage.provider'; -import {DefaultAuthService} from './default-auth.service'; -import {SimpleBrowser} from '../../util/browser.factory'; -import {AlertController} from '@ionic/angular/standalone'; +import { + AuthActions, AuthProvider, + IAuthAction, +} from './auth.provider'; const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path'; +const AUTH_MESSAGE_PREFIX = 'auth.messages.default'; @Injectable({ providedIn: 'root', }) export class AuthHelperService { + private authProvider = inject(AuthProvider); + private translateService = inject(TranslateService); private configProvider = inject(ConfigProvider); private storageProvider = inject(StorageProvider); - private defaultAuth = inject(DefaultAuthService); - private browser = inject(SimpleBrowser); private alertController = inject(AlertController); @@ -58,93 +61,139 @@ export class AuthHelperService { ).default?.endpoints.mapping ?? {}; } - public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction) { - let message: string | undefined; + public getAuthMessage( + action: IAuthAction, + ): string | undefined { switch (action.action) { - case AuthActions.SignInSuccess: { - message = this.translateService.instant(`auth.messages.${provider}.logged_in_success`); - break; - } - case AuthActions.SignOutSuccess: { - message = this.translateService.instant(`auth.messages.${provider}.logged_out_success`); - break; - } + case AuthActions.SignInSuccess: + return this.translateService.instant( + `${AUTH_MESSAGE_PREFIX}.logged_in_success`, + ); + + case AuthActions.SignOutSuccess: + return this.translateService.instant( + `${AUTH_MESSAGE_PREFIX}.logged_out_success`, + ); + + default: + return undefined; } - return message; } - getUserFromUserInfo(userInfo: object) { + public getUserFromUserInfo( + userInfo: object, + ): SCUserConfiguration { const user: SCUserConfiguration = { id: '', name: '', role: 'student', }; + for (const key in this.userConfigurationMap) { - user[key as keyof SCUserConfiguration] = JSONPath({ - path: this.userConfigurationMap[key as keyof SCUserConfiguration] as string, + const userKey = + key as keyof SCUserConfiguration; + + user[userKey] = JSONPath({ + path: + this.userConfigurationMap[ + userKey + ] as string, json: userInfo, })[0]; } - if (user.givenName && user.givenName.length > 0 && user.familyName && user.familyName.length > 0) { - user.name = `${user.givenName} ${user.familyName}`; + + if ( + user.givenName && + user.familyName + ) { + user.name = + `${user.givenName} ${user.familyName}`; } return user; } - async deleteOriginPath() { - return this.storageProvider.delete(AUTH_ORIGIN_PATH); + public async deleteOriginPath(): + Promise { + await this.storageProvider.delete( + AUTH_ORIGIN_PATH, + ); } - async setOriginPath(path: string) { - return this.storageProvider.put(AUTH_ORIGIN_PATH, path); + public async setOriginPath( + path: string, + ): Promise { + await this.storageProvider.put( + AUTH_ORIGIN_PATH, + path, + ); } - async getOriginPath() { - let originPath: string; + public async getOriginPath(): + Promise { try { - originPath = await this.storageProvider.get(AUTH_ORIGIN_PATH); + return await this.storageProvider.get( + AUTH_ORIGIN_PATH, + ); } catch { + return undefined; + } + } + + public async endBrowserSession(): + Promise { + const endSessionEndpoint = + await this.authProvider + .getEndSessionEndpoint(); + + if (!endSessionEndpoint) { return; } - return originPath; - } - /** - * Provides appropriate auth service instance based on type (string) parameter - */ - getProvider(): DefaultAuthService { - return this.defaultAuth; - } + const alert = + await this.alertController.create({ + header: + this.translateService.instant( + `${AUTH_MESSAGE_PREFIX}.log_out_alert.header`, + ), - /** - * Ends browser session by opening endSessionEndpoint URL of the provider - * @param providerType Type of the provider (e.g. 'default' or 'paia') - */ - async endBrowserSession(providerType: SCAuthorizationProviderType) { - const endSessionEndpoint = (await this.getProvider().configuration).endSessionEndpoint; + message: + this.translateService.instant( + `${AUTH_MESSAGE_PREFIX}.log_out_alert.message`, + ), - if (endSessionEndpoint) { - const alert: HTMLIonAlertElement = await this.alertController.create({ - header: this.translateService.instant(`auth.messages.${providerType}.log_out_alert.header`), - message: this.translateService.instant(`auth.messages.${providerType}.log_out_alert.message`), buttons: [ { - text: this.translateService.instant('no'), + text: + this.translateService.instant( + 'no', + ), + + role: 'cancel', cssClass: 'default', }, { - text: this.translateService.instant('yes'), + text: + this.translateService.instant( + 'yes', + ), + role: 'confirm', cssClass: 'preferred', - handler: () => { - this.browser.open(new URL(endSessionEndpoint).href); + + handler: async () => { + /* + * Only now, after explicit confirmation, + * open the IdP end-session endpoint. + */ + await this.browser.open( + endSessionEndpoint, + ); }, }, ], }); - await alert.present(); - } + await alert.present(); } } diff --git a/frontend/app/src/app/modules/auth/auth-paths.ts b/frontend/app/src/app/modules/auth/auth-paths.ts index 3471c846..9cda3b4b 100644 --- a/frontend/app/src/app/modules/auth/auth-paths.ts +++ b/frontend/app/src/app/modules/auth/auth-paths.ts @@ -12,15 +12,5 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {SCAuthorizationProviderType} from '@openstapps/core'; -export const authPaths: { - [key in SCAuthorizationProviderType]: {redirect_path: string}; -} = { - default: { - redirect_path: 'auth/callback', - }, - paia: { - redirect_path: 'auth/paia/callback', - }, -}; +export const AUTH_REDIRECT_PATH = 'auth/callback'; diff --git a/frontend/app/src/app/modules/auth/auth-routing.module.ts b/frontend/app/src/app/modules/auth/auth-routing.module.ts index 3f52edb0..beb30d22 100644 --- a/frontend/app/src/app/modules/auth/auth-routing.module.ts +++ b/frontend/app/src/app/modules/auth/auth-routing.module.ts @@ -15,12 +15,12 @@ import {RouterModule, Routes} from '@angular/router'; import {NgModule} from '@angular/core'; -import {authPaths} from './auth-paths'; +import {AUTH_REDIRECT_PATH} from './auth-paths'; import {AuthCallbackPageComponent} from './auth-callback/page/auth-callback-page.component'; const authRoutes: Routes = [ { - path: authPaths.default.redirect_path, + path: AUTH_REDIRECT_PATH, component: AuthCallbackPageComponent, }, ]; diff --git a/frontend/app/src/app/modules/auth/auth.module.ts b/frontend/app/src/app/modules/auth/auth.module.ts index 0fd9b82a..319cd962 100644 --- a/frontend/app/src/app/modules/auth/auth.module.ts +++ b/frontend/app/src/app/modules/auth/auth.module.ts @@ -3,14 +3,12 @@ import {CommonModule} from '@angular/common'; import {Platform} from '@ionic/angular/standalone'; import {Requestor, StorageBackend} from '@openid/appauth'; import {storageFactory} from './factories'; -import {Browser} from 'ionic-appauth'; -import {CapacitorBrowser} from 'ionic-appauth/lib/capacitor'; -import {httpFactory} from './factories/http.factory'; +import {requestorFactory} from './factories/requestor.factory'; import {HttpClient} from '@angular/common/http'; import {AuthRoutingModule} from './auth-routing.module'; import {TranslateModule} from '@ngx-translate/core'; import {AuthCallbackPageComponent} from './auth-callback/page/auth-callback-page.component'; -import {DefaultAuthService} from './default-auth.service'; +import {AuthProvider} from "./auth.provider"; @NgModule({ declarations: [AuthCallbackPageComponent], @@ -23,14 +21,10 @@ import {DefaultAuthService} from './default-auth.service'; }, { provide: Requestor, - useFactory: httpFactory, + useFactory: requestorFactory, deps: [Platform, HttpClient], }, - { - provide: Browser, - useClass: CapacitorBrowser, - }, - DefaultAuthService, + AuthProvider, ], }) export class AuthModule {} diff --git a/frontend/app/src/app/modules/auth/auth.provider.methods.ts b/frontend/app/src/app/modules/auth/auth.provider.methods.ts index 5fc4dec0..ce051789 100644 --- a/frontend/app/src/app/modules/auth/auth.provider.methods.ts +++ b/frontend/app/src/app/modules/auth/auth.provider.methods.ts @@ -13,69 +13,90 @@ * this program. If not, see . */ -import {AuthorizationServiceConfigurationJson} from '@openid/appauth'; -import {IAuthConfig} from 'ionic-appauth'; -import {SCAuthorizationProvider, SCAuthorizationProviderType} from '@openstapps/core'; import {Capacitor} from '@capacitor/core'; -import {authPaths} from './auth-paths'; +import {SCAuthorizationProvider} from '@openstapps/core'; +import { + AuthorizationServiceConfiguration, +} from '@openid/appauth'; + import {environment} from '../../../environments/environment'; +import {AUTH_REDIRECT_PATH} from './auth-paths'; +import type {AuthConfig} from './auth.provider'; + +export interface OidcEndpoints { + userinfo?: string; + endSession?: string; +} /** - * Get configuration of an OAuth2 client + * Returns the configuration of the OIDC client. */ export function getClientConfig( - providerType: SCAuthorizationProviderType, authConfig: { - default?: SCAuthorizationProvider; - paia?: SCAuthorizationProvider; + default: SCAuthorizationProvider; }, -): IAuthConfig { - const providerConfig = authConfig[providerType] as SCAuthorizationProvider; +): AuthConfig { + const provider = + authConfig.default; + return { - end_session_redirect_url: '', + server_host: provider.client.url, + client_id: provider.client.clientId, + scopes: provider.client.scopes, + redirect_url: getRedirectUrl(), pkce: true, - scopes: providerConfig.client.scopes, - server_host: providerConfig.client.url, - client_id: providerConfig.client.clientId, - redirect_url: getRedirectUrl(authPaths[providerType].redirect_path), }; } /** - * Get configuration about endpoints of an OAuth2 server + * Returns the AppAuth OAuth/OIDC service configuration. */ -export function getEndpointsConfig( - providerType: SCAuthorizationProviderType, +export function getServiceConfiguration( authConfig: { - default?: SCAuthorizationProvider; - paia?: SCAuthorizationProvider; + default: SCAuthorizationProvider; }, -): AuthorizationServiceConfigurationJson { - const providerConfig = authConfig[providerType] as SCAuthorizationProvider; +): AuthorizationServiceConfiguration { + const endpoints = + authConfig.default.endpoints; + + return new AuthorizationServiceConfiguration({ + authorization_endpoint: endpoints.authorization, + token_endpoint: endpoints.token, + revocation_endpoint: endpoints.revoke || '', + }); +} + +/** + * Returns the additional OIDC endpoints that AppAuth-JS does not expose + * through AuthorizationServiceConfiguration. + */ +export function getOidcEndpoints( + authConfig: { + default: SCAuthorizationProvider; + }, +): OidcEndpoints { + const endpoints = authConfig.default.endpoints; + return { - authorization_endpoint: providerConfig.endpoints.authorization, - end_session_endpoint: providerConfig.endpoints.endSession, - revocation_endpoint: providerConfig.endpoints.revoke ?? '', - token_endpoint: providerConfig.endpoints.token, - userinfo_endpoint: providerConfig.endpoints.userinfo, + userinfo: + endpoints.userinfo, + + endSession: + endpoints.endSession, }; } /** - * Return a URL of the app, depending on the platform where it is running + * Returns the OIDC redirect URL depending on the current platform. */ -function getRedirectUrl(routePath: string): string { - let appHost: string; - let appSchema: string; - if (environment.production) { - appSchema = Capacitor.isNativePlatform() ? environment.custom_url_scheme : 'https'; - appHost = environment.app_host; - } else { - appSchema = Capacitor.isNativePlatform() - ? environment.custom_url_scheme - : window.location.protocol.split(':')[0]; - - appHost = Capacitor.isNativePlatform() ? environment.app_host : window.location.host; +function getRedirectUrl(): string { + if (Capacitor.isNativePlatform()) { + return `${environment.custom_url_scheme}://${environment.app_host}/${AUTH_REDIRECT_PATH}`; } - return `${appSchema}://${appHost}/${routePath}`; + + if (environment.production) { + return `https://${environment.app_host}/${AUTH_REDIRECT_PATH}`; + } + + return `${window.location.origin}/${AUTH_REDIRECT_PATH}`; } diff --git a/frontend/app/src/app/modules/auth/auth.provider.spec.ts b/frontend/app/src/app/modules/auth/auth.provider.spec.ts new file mode 100644 index 00000000..8db72393 --- /dev/null +++ b/frontend/app/src/app/modules/auth/auth.provider.spec.ts @@ -0,0 +1,239 @@ +import { + AppAuthError, + AuthorizationServiceConfiguration, + StorageBackend, + TokenRequestHandler, + TokenResponse, +} from '@openid/appauth'; +import {BehaviorSubject, firstValueFrom} from 'rxjs'; + +import {AuthProvider, IAuthAction, INVALID_GRANT} from './auth.provider'; + +const TOKEN_RESPONSE_KEY = 'token_response'; +const TOKEN_LIFETIME = 3600; +const NOW_MS = Date.parse('2026-01-01T12:00:00Z'); +const NOW = NOW_MS / 1000; + +/** + * Creates a token with a real lifetime instead of spying on isValid(). + * + * remainingSeconds > 0 → still valid for that long + * remainingSeconds < 0 → expired that many seconds ago + * refreshToken: null → token without refresh token + */ +function makeToken( + opts: {accessToken?: string; refreshToken?: string | null; remainingSeconds?: number} = {}, +): TokenResponse { + const remaining = opts.remainingSeconds ?? TOKEN_LIFETIME; + + return new TokenResponse({ + access_token: opts.accessToken ?? 'access-token', + refresh_token: opts.refreshToken === null ? undefined : (opts.refreshToken ?? 'refresh-token'), + token_type: 'bearer', + expires_in: String(TOKEN_LIFETIME), + issued_at: NOW - (TOKEN_LIFETIME - remaining), + }); +} + +/** Typed access to the private members the tests depend on. */ +interface AuthProviderInternals { + storage: StorageBackend; + tokenHandler: TokenRequestHandler; + tokenSubject: BehaviorSubject; + notify(action: IAuthAction): void; +} + +describe('AuthProvider', () => { + let provider: AuthProvider; + let internals: AuthProviderInternals; + + let storageGetItem: jasmine.Spy; + let storageSetItem: jasmine.Spy; + let storageRemoveItem: jasmine.Spy; + let tokenRequest: jasmine.Spy; + + /** Seeds storage with the given token and loads it through the public API. */ + async function givenLoadedToken(token: TokenResponse | null): Promise { + storageGetItem.and.resolveTo(token ? JSON.stringify(token.toJson()) : null); + await provider.loadTokenFromStorage(); + } + + const isAuthenticated = () => firstValueFrom(provider.isAuthenticated$); + const isLoggedIn = () => firstValueFrom(provider.isLoggedIn$); + + beforeEach(() => { + jasmine.clock().install(); + jasmine.clock().mockDate(new Date(NOW_MS)); + + const platform = {is: () => false} as any; + const configProvider = {} as any; + + provider = new AuthProvider(platform, configProvider); + internals = provider as unknown as AuthProviderInternals; + + (provider as any).authConfigValue = { + server_host: 'https://idp.example.org', + client_id: 'test-client', + redirect_url: 'https://app.example.org/callback', + scopes: 'openid profile', + pkce: true, + }; + + (provider as any).localConfiguration = new AuthorizationServiceConfiguration({ + authorization_endpoint: 'https://idp.example.org/authorize', + token_endpoint: 'https://idp.example.org/token', + revocation_endpoint: 'https://idp.example.org/revoke', + }); + + // Never touch real storage or the network. + storageGetItem = spyOn(internals.storage, 'getItem').and.resolveTo(null); + storageSetItem = spyOn(internals.storage, 'setItem').and.resolveTo(); + storageRemoveItem = spyOn(internals.storage, 'removeItem').and.resolveTo(); + tokenRequest = spyOn(internals.tokenHandler, 'performTokenRequest').and.rejectWith( + new Error('Unexpected token request'), + ); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + }); + + describe('isAuthenticated$', () => { + it('should provide false through isAuthenticated$ when there is no token response', async () => { + // Start authenticated so the test cannot pass on the initial value alone. + await givenLoadedToken(makeToken()); + expect(await isAuthenticated()).toBeTrue(); + + await givenLoadedToken(null); + + expect(internals.tokenSubject.value).toBeUndefined(); + expect(await isAuthenticated()).toBeFalse(); + }); + + it('should provide true through isAuthenticated$ when access token is valid', async () => { + await givenLoadedToken(makeToken()); + + expect(await isAuthenticated()).toBeTrue(); + }); + + it('should provide false through isAuthenticated$ when access token is invalid', async () => { + await givenLoadedToken(makeToken({remainingSeconds: -TOKEN_LIFETIME})); + + expect(await isAuthenticated()).toBeFalse(); + // Still logged in: a refresh token is available. + expect(await isLoggedIn()).toBeTrue(); + }); + + it('should provide true through isAuthenticated$ after an expired token was refreshed', async () => { + await givenLoadedToken(makeToken({remainingSeconds: -60})); + expect(await isAuthenticated()).toBeFalse(); + + tokenRequest.and.resolveTo(makeToken({accessToken: 'new-access-token'})); + await provider.getValidToken(); + + expect(await isAuthenticated()).toBeTrue(); + }); + }); + + describe('getValidToken', () => { + it('returns the current token without refreshing when it is valid', async () => { + await givenLoadedToken(makeToken({accessToken: 'current-access-token'})); + + const token = await provider.getValidToken(); + + expect(tokenRequest).not.toHaveBeenCalled(); + expect(token.accessToken).toBe('current-access-token'); + }); + + it('refreshes the token when the access token is expired', async () => { + await givenLoadedToken(makeToken({remainingSeconds: -60})); + tokenRequest.and.resolveTo( + makeToken({accessToken: 'new-access-token', refreshToken: 'new-refresh-token'}), + ); + + const token = await provider.getValidToken(); + + expect(tokenRequest).toHaveBeenCalledTimes(1); + expect(token.accessToken).toBe('new-access-token'); + expect(token.refreshToken).toBe('new-refresh-token'); + expect(storageSetItem).toHaveBeenCalledOnceWith(TOKEN_RESPONSE_KEY, jasmine.any(String)); + }); + + it('refreshes the token when the access token expires within the buffer', async () => { + // 5 minutes left, default buffer requires more than 10. + await givenLoadedToken(makeToken({remainingSeconds: 5 * 60})); + tokenRequest.and.resolveTo(makeToken({accessToken: 'new-access-token'})); + + const token = await provider.getValidToken(); + + expect(tokenRequest).toHaveBeenCalledTimes(1); + expect(token.accessToken).toBe('new-access-token'); + }); + + it('keeps the previous refresh token when the refresh response contains none', async () => { + await givenLoadedToken(makeToken({remainingSeconds: -60, refreshToken: 'old-refresh-token'})); + tokenRequest.and.resolveTo(makeToken({accessToken: 'new-access-token', refreshToken: null})); + + const token = await provider.getValidToken(); + + expect(token.accessToken).toBe('new-access-token'); + expect(token.refreshToken).toBe('old-refresh-token'); + }); + + it('rejects without a request when the token is expired and has no refresh token', async () => { + await givenLoadedToken(makeToken({remainingSeconds: -60, refreshToken: null})); + + await expectAsync(provider.getValidToken()).toBeRejectedWithError(/No Refresh Token Available/); + expect(tokenRequest).not.toHaveBeenCalled(); + }); + + describe('when refresh fails with invalid_grant', () => { + const refreshError = new AppAuthError(INVALID_GRANT); + + beforeEach(async () => { + await givenLoadedToken(makeToken({remainingSeconds: -60, refreshToken: 'invalid-refresh-token'})); + expect(await isLoggedIn()).toBeTrue(); + + tokenRequest.and.rejectWith(refreshError); + + await expectAsync(provider.getValidToken()).toBeRejectedWith(refreshError); + }); + + it('changes isLoggedIn to false', async () => { + expect(await isLoggedIn()).toBeFalse(); + }); + + it('removes the token from storage', () => { + expect(storageRemoveItem).toHaveBeenCalledOnceWith(TOKEN_RESPONSE_KEY); + }); + + it('clears the current token', () => { + expect(internals.tokenSubject.value).toBeUndefined(); + }); + }); + + describe('when refresh fails with a temporary error', () => { + // An AppAuthError that is not invalid_grant, e.g. a network failure. + const refreshError = new AppAuthError('Network Error'); + + beforeEach(async () => { + await givenLoadedToken(makeToken({remainingSeconds: -60})); + tokenRequest.and.rejectWith(refreshError); + + await expectAsync(provider.getValidToken()).toBeRejectedWith(refreshError); + }); + + it('keeps the user logged in', async () => { + expect(await isLoggedIn()).toBeTrue(); + }); + + it('does not remove the token from storage', () => { + expect(storageRemoveItem).not.toHaveBeenCalled(); + }); + + it('keeps the current token for a later retry', () => { + expect(internals.tokenSubject.value?.refreshToken).toBe('refresh-token'); + }); + }); + }); +}); diff --git a/frontend/app/src/app/modules/auth/auth.provider.ts b/frontend/app/src/app/modules/auth/auth.provider.ts new file mode 100644 index 00000000..acf6e3cc --- /dev/null +++ b/frontend/app/src/app/modules/auth/auth.provider.ts @@ -0,0 +1,1122 @@ +/* + * Copyright (C) 2023 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +import {Injectable} from '@angular/core'; +import {Browser} from '@capacitor/browser'; +import {Platform} from '@ionic/angular/standalone'; +import { + AuthorizationError, + AuthorizationNotifier, + AuthorizationRequest, + AuthorizationRequestJson, + AuthorizationResponse, + AuthorizationServiceConfiguration, + AppAuthError, + BaseTokenRequestHandler, + BasicQueryStringUtils, + DefaultCrypto, + ErrorType, + GRANT_TYPE_AUTHORIZATION_CODE, + GRANT_TYPE_REFRESH_TOKEN, + LocationLike, + RedirectRequestHandler, + Requestor, + RevokeTokenRequest, + RevokeTokenRequestJson, + StorageBackend, + StringMap, + TokenRequest, + TokenRequestHandler, + TokenRequestJson, + TokenResponse, +} from '@openid/appauth'; +import {SCAuthorizationProvider} from '@openstapps/core'; +import {BehaviorSubject, Observable} from 'rxjs'; + +import {ConfigProvider} from '../config/config.provider'; +import {CapacitorAuthorizationRequestHandler} from './capacitor-authorization-request-handler'; +import { + getClientConfig, + getOidcEndpoints, + getServiceConfiguration, + OidcEndpoints, +} from './auth.provider.methods'; +import {requestorFactory} from './factories/requestor.factory'; +import {storageFactory} from './factories/storage.factory'; + +const TOKEN_RESPONSE_KEY = 'token_response'; +export const INVALID_GRANT: ErrorType = 'invalid_grant'; +const AUTH_EXPIRY_BUFFER = -3 * 60; + +export interface AuthConfig { + server_host: string; + client_id: string; + redirect_url: string; + end_session_redirect_url?: string; + scopes: string; + pkce: boolean; +} + +export enum AuthActions { + Init = 'Init', + + SignInSuccess = 'SignInSuccess', + SignInFailed = 'SignInFailed', + + SignOutSuccess = 'SignOutSuccess', + SignOutFailed = 'SignOutFailed', + + RefreshSuccess = 'RefreshSuccess', + RefreshFailed = 'RefreshFailed', + + LoadTokenFromStorageSuccess = + 'LoadTokenFromStorageSuccess', + + LoadTokenFromStorageFailed = + 'LoadTokenFromStorageFailed', + + RevokeTokensSuccess = + 'RevokeTokensSuccess', + + RevokeTokensFailed = + 'RevokeTokensFailed', + + LoadUserInfoSuccess = + 'LoadUserInfoSuccess', + + LoadUserInfoFailed = + 'LoadUserInfoFailed', +} + +export interface IAuthAction { + action: AuthActions; + tokenResponse?: TokenResponse; + user?: unknown; + error?: unknown; +} + +/** + * Common subset implemented by AppAuth's RedirectRequestHandler + * and our CapacitorAuthorizationRequestHandler. + */ +interface AuthorizationHandler { + setAuthorizationNotifier( + notifier: AuthorizationNotifier, + ): unknown; + + performAuthorizationRequest( + configuration: AuthorizationServiceConfiguration, + request: AuthorizationRequest, + ): void | Promise; + + completeAuthorizationRequestIfPossible(): + Promise; +} + +class NoHashQueryStringUtils extends BasicQueryStringUtils { + override parse( + input: LocationLike, + _useHash?: boolean, + ): StringMap { + return super.parse( + input, + false, + ); + } +} + +@Injectable({ + providedIn: 'root', +}) +export class AuthProvider { + private authConfigValue?: AuthConfig; + + private localConfiguration?: + AuthorizationServiceConfiguration; + + private oidcEndpoints?: OidcEndpoints; + + private readonly authSubject = + new BehaviorSubject({ + action: AuthActions.Init, + }); + + private readonly tokenSubject = + new BehaviorSubject( + undefined, + ); + + private readonly userSubject = + new BehaviorSubject( + undefined, + ); + + private readonly authenticatedSubject = + new BehaviorSubject(false); + + private readonly loggedInSubject = + new BehaviorSubject(false); + + private readonly initCompleteSubject = + new BehaviorSubject(false); + + private readonly storage: StorageBackend; + private readonly requestor: Requestor; + + private readonly requestHandler: + AuthorizationHandler; + + private readonly tokenHandler: + TokenRequestHandler; + + private notifierInitialized = false; + + constructor( + private readonly platform: Platform, + private readonly configProvider: ConfigProvider, + ) { + this.storage = + storageFactory(platform); + + this.requestor = + requestorFactory(platform); + + this.requestHandler = + platform.is('capacitor') + ? new CapacitorAuthorizationRequestHandler( + this.storage, + ) + : new RedirectRequestHandler( + this.storage, + new NoHashQueryStringUtils(), + ); + + this.tokenHandler = + new BaseTokenRequestHandler( + this.requestor, + ); + } + + get token$(): + Observable { + return this.tokenSubject.asObservable(); + } + + get isAuthenticated$(): + Observable { + return this.authenticatedSubject.asObservable(); + } + + get isLoggedIn$(): + Observable { + return this.loggedInSubject.asObservable(); + } + + get initComplete$(): + Observable { + return this.initCompleteSubject.asObservable(); + } + + get user$(): + Observable { + return this.userSubject.asObservable(); + } + + get events$(): + Observable { + return this.authSubject.asObservable(); + } + + get authConfig(): AuthConfig { + if (!this.authConfigValue) { + throw new Error( + 'AuthConfig Not Defined', + ); + } + + return this.authConfigValue; + } + + get configuration(): + Promise { + if (!this.localConfiguration) { + throw new Error( + 'Local Configuration Not Defined', + ); + } + + return Promise.resolve( + this.localConfiguration, + ); + } + + public async init(): Promise { + this.setupConfiguration(); + this.setupAuthorizationNotifier(); + + await this.loadTokenFromStorage(); + } + + private setupConfiguration(): void { + const config = + this.configProvider.getAnyValue( + 'auth', + ) as { + default: SCAuthorizationProvider; + }; + + this.authConfigValue = + getClientConfig(config); + + this.localConfiguration = + getServiceConfiguration(config); + + this.oidcEndpoints = + getOidcEndpoints(config); + } + + public async signIn( + authExtras?: StringMap, + state?: string, + ): Promise { + try { + const requestJson: + AuthorizationRequestJson = { + response_type: + AuthorizationRequest + .RESPONSE_TYPE_CODE, + + client_id: + this.authConfig.client_id, + + redirect_uri: + this.authConfig.redirect_url, + + scope: + this.authConfig.scopes, + + extras: + authExtras, + + state, + }; + + const request = + new AuthorizationRequest( + requestJson, + new DefaultCrypto(), + this.authConfig.pkce, + ); + + await this.requestHandler + .performAuthorizationRequest( + await this.configuration, + request, + ); + } catch (error) { + this.notify({ + action: + AuthActions.SignInFailed, + + error, + }); + } + } + + public authorizationCallback( + callbackUrl: string, + ): void { + void this.handleAuthorizationCallback( + callbackUrl, + ).catch(error => { + this.notify({ + action: + AuthActions.SignInFailed, + + error, + }); + }); + } + + public async refreshToken(): + Promise { + try { + await this.requestTokenRefresh(); + } catch (error) { + await this.handleRefreshFailure( + error, + ); + } + } + + public async loadUserInfo(): + Promise { + try { + const token = + await this.getValidToken(0); + + const endpoint = + this.oidcEndpoints?.userinfo; + + if (!endpoint) { + throw new Error( + 'OIDC provider does not expose a userinfo endpoint.', + ); + } + + const user = + await this.requestor.xhr< + Record + >({ + url: + endpoint, + + method: + 'GET', + + dataType: + 'json', + + headers: { + Authorization: + `Bearer ${token.accessToken}`, + }, + }); + + this.notify({ + action: + AuthActions.LoadUserInfoSuccess, + + user, + }); + } catch (error) { + this.notify({ + action: + AuthActions.LoadUserInfoFailed, + + error, + }); + } + } + + public async getEndSessionEndpoint(): + Promise { + return this.oidcEndpoints + ?.endSession; + } + + public async signOut( + _state?: string, + revokeTokens = false, + ): Promise { + try { + if (revokeTokens) { + await this.revokeTokens(); + } + + await this.storage.removeItem( + TOKEN_RESPONSE_KEY, + ); + + /* + * Local logout only. + * + * The optional logout from the identity provider is + * deliberately handled by AuthHelperService after the + * confirmation dialog. + */ + this.notify({ + action: + AuthActions.SignOutSuccess, + }); + } catch (error) { + this.notify({ + action: + AuthActions.SignOutFailed, + + error, + }); + } + } + + public async revokeTokens(): + Promise { + try { + await this.requestTokenRevoke(); + + this.notify({ + action: + AuthActions.RevokeTokensSuccess, + }); + } catch (error) { + await this.storage.removeItem( + TOKEN_RESPONSE_KEY, + ); + + this.notify({ + action: + AuthActions.RevokeTokensFailed, + + error, + }); + } + } + + public endSessionCallback(): void { + void this.internalEndSessionCallback() + .catch(error => { + this.notify({ + action: + AuthActions.SignOutFailed, + + error, + }); + }); + } + + public async loadTokenFromStorage(): + Promise { + try { + const stored = + await this.storage.getItem( + TOKEN_RESPONSE_KEY, + ); + + if (!stored) { + throw new Error( + 'No Token In Storage', + ); + } + + const token = + new TokenResponse( + JSON.parse(stored), + ); + + this.notify({ + action: + AuthActions + .LoadTokenFromStorageSuccess, + + tokenResponse: + token, + }); + } catch (error) { + this.notify({ + action: + AuthActions + .LoadTokenFromStorageFailed, + + error, + }); + } + } + + public async getValidToken( + buffer = AUTH_EXPIRY_BUFFER, + ): Promise { + const currentToken = + this.tokenSubject.value; + + /* + * Access token still valid for longer than the configured + * expiry buffer. + */ + if (currentToken?.isValid(buffer)) { + console.log(currentToken?.isValid(buffer)); + return currentToken; + } + + /* + * Access token is expired or will expire soon. + * We can only recover automatically if a refresh token exists. + */ + if (!currentToken?.refreshToken) { + throw new Error( + 'Unable To Obtain Valid Token: No Refresh Token Available', + ); + } + + /* + * Refresh immediately. + * + * requestTokenRefresh() stores the newly returned token + * and updates tokenSubject via RefreshSuccess. + */ + try { + await this.requestTokenRefresh(); + } catch (error) { + await this.handleRefreshFailure( + error, + ); + + throw error; + } + + const refreshedToken = + this.tokenSubject.value; + + if (!refreshedToken) { + throw new Error( + 'Unable To Obtain Valid Token: Refresh Returned No Token', + ); + } + + if (!refreshedToken.isValid(buffer)) { + throw new Error( + 'Unable To Obtain Valid Token: Refreshed Token Is Already Too Close To Expiry', + ); + } + + return refreshedToken; + } + + private async handleRefreshFailure( + error: unknown, + ): Promise { + /* + * AppAuth maps OAuth token endpoint errors to AppAuthError. + * This OAuth error means that the refresh token is no longer + * usable, for example because it expired, was revoked or + * the refresh-token chain lifetime has been reached. + * + * Network and other temporary errors must not log the user + * out. + */ + if ( + error instanceof AppAuthError && + error.message === INVALID_GRANT + ) { + try { + await this.storage.removeItem( + TOKEN_RESPONSE_KEY, + ); + } catch { + /* + * Do not hide the original OAuth error if removing the + * locally stored token fails. + */ + } + + this.tokenSubject.next( + undefined, + ); + + this.userSubject.next( + undefined, + ); + + this.authenticatedSubject.next( + false, + ); + + this.loggedInSubject.next( + false, + ); + } + + this.notify({ + action: + AuthActions.RefreshFailed, + + error, + }); + } + + private setupAuthorizationNotifier(): + void { + if (this.notifierInitialized) { + return; + } + + const notifier = + new AuthorizationNotifier(); + + this.requestHandler + .setAuthorizationNotifier( + notifier, + ); + + notifier.setAuthorizationListener( + ( + request, + response, + error, + ) => { + void this + .onAuthorizationNotification( + request, + response, + error, + ) + .catch( + notificationError => { + this.notify({ + action: + AuthActions + .SignInFailed, + + error: + notificationError, + }); + }, + ); + }, + ); + + this.notifierInitialized = true; + } + + private async onAuthorizationNotification( + request: AuthorizationRequest, + response: AuthorizationResponse | null, + error: AuthorizationError | null, + ): Promise { + if (error) { + throw new Error( + error.errorDescription || + error.error || + 'Authorization failed.', + ); + } + + if (!response) { + throw new Error( + 'Authorization response is missing.', + ); + } + + const codeVerifier = + this.authConfig.pkce + ? request.internal?.[ + 'code_verifier' + ] + : undefined; + + await this.requestAccessToken( + response.code, + codeVerifier, + ); + } + + private async requestAccessToken( + code: string, + codeVerifier?: string, + ): Promise { + const requestJson: + TokenRequestJson = { + grant_type: + GRANT_TYPE_AUTHORIZATION_CODE, + + code, + + refresh_token: + undefined, + + redirect_uri: + this.authConfig.redirect_url, + + client_id: + this.authConfig.client_id, + + extras: + codeVerifier + ? { + code_verifier: + codeVerifier, + } + : undefined, + }; + + const token = + await this.tokenHandler + .performTokenRequest( + await this.configuration, + + new TokenRequest( + requestJson, + ), + ); + + await this.storeToken( + token, + ); + + this.notify({ + action: + AuthActions.SignInSuccess, + + tokenResponse: + token, + }); + } + + private async requestTokenRefresh(): + Promise { + const currentToken = + this.tokenSubject.value; + + if (!currentToken) { + throw new Error( + 'No Token Defined!', + ); + } + + if (!currentToken.refreshToken) { + throw new Error( + 'No Refresh Token Defined!', + ); + } + + const requestJson: + TokenRequestJson = { + grant_type: + GRANT_TYPE_REFRESH_TOKEN, + + code: + undefined, + + refresh_token: + currentToken.refreshToken, + + redirect_uri: + this.authConfig.redirect_url, + + client_id: + this.authConfig.client_id, + }; + + const response = + await this.tokenHandler + .performTokenRequest( + await this.configuration, + + new TokenRequest( + requestJson, + ), + ); + + if (!response.accessToken) { + throw new Error( + 'No Access Token Defined In Refresh Response', + ); + } + + const token = + new TokenResponse({ + ...response.toJson(), + + refresh_token: + response.refreshToken ?? + currentToken.refreshToken, + + id_token: + response.idToken ?? + currentToken.idToken, + }); + + await this.storeToken( + token, + ); + + this.notify({ + action: + AuthActions.RefreshSuccess, + + tokenResponse: + token, + }); + } + + private async requestTokenRevoke(): + Promise { + const token = + this.tokenSubject.value; + + if (!token) { + throw new Error( + 'No Token Defined!', + ); + } + + const configuration = + await this.configuration; + + if ( + !configuration.revocationEndpoint + ) { + /* + * No revocation endpoint is configured by the backend. + * Remove the local token instead. + */ + await this.storage.removeItem( + TOKEN_RESPONSE_KEY, + ); + + return; + } + + if (token.refreshToken) { + const refreshRequest: + RevokeTokenRequestJson = { + token: + token.refreshToken, + + token_type_hint: + 'refresh_token', + + client_id: + this.authConfig.client_id, + }; + + await this.tokenHandler + .performRevokeTokenRequest( + configuration, + + new RevokeTokenRequest( + refreshRequest, + ), + ); + } + + const accessRequest: + RevokeTokenRequestJson = { + token: + token.accessToken, + + token_type_hint: + 'access_token', + + client_id: + this.authConfig.client_id, + }; + + await this.tokenHandler + .performRevokeTokenRequest( + configuration, + + new RevokeTokenRequest( + accessRequest, + ), + ); + + await this.storage.removeItem( + TOKEN_RESPONSE_KEY, + ); + } + + private async handleAuthorizationCallback( + callbackUrl: string, + ): Promise { + if ( + this.requestHandler instanceof + CapacitorAuthorizationRequestHandler + ) { + await this.requestHandler + .completeAuthorizationRequestFromUrl( + callbackUrl, + ); + + return; + } + + await this.requestHandler + .completeAuthorizationRequestIfPossible(); + } + + private async internalEndSessionCallback(): + Promise { + await this.closeBrowser(); + + await this.storage.removeItem( + TOKEN_RESPONSE_KEY, + ); + + this.notify({ + action: + AuthActions.SignOutSuccess, + }); + } + + private async closeBrowser(): + Promise { + if ( + !this.platform.is('capacitor') + ) { + return; + } + + try { + await Browser.close(); + } catch { + // Browser may already be closed. + } + } + + private async storeToken( + token: TokenResponse, + ): Promise { + await this.storage.setItem( + TOKEN_RESPONSE_KEY, + + JSON.stringify( + token.toJson(), + ), + ); + } + + private notify( + action: IAuthAction, + ): void { + switch (action.action) { + case AuthActions.SignInFailed: + case AuthActions.SignOutSuccess: + case AuthActions.SignOutFailed: { + this.tokenSubject.next( + undefined, + ); + + this.userSubject.next( + undefined, + ); + + this.authenticatedSubject.next( + false, + ); + + this.loggedInSubject.next( + false, + ); + + break; + } + + case AuthActions + .LoadTokenFromStorageFailed: { + this.tokenSubject.next( + undefined, + ); + + this.userSubject.next( + undefined, + ); + + this.authenticatedSubject.next( + false, + ); + + this.loggedInSubject.next( + false, + ); + + this.initCompleteSubject.next( + true, + ); + + break; + } + + case AuthActions.SignInSuccess: + case AuthActions.RefreshSuccess: { + if (action.tokenResponse) { + this.tokenSubject.next( + action.tokenResponse, + ); + + this.authenticatedSubject.next( + true, + ); + + this.loggedInSubject.next( + true, + ); + } + + break; + } + + case AuthActions + .LoadTokenFromStorageSuccess: { + if (action.tokenResponse) { + this.tokenSubject.next( + action.tokenResponse, + ); + + this.authenticatedSubject.next( + action.tokenResponse.isValid( + 0, + ), + ); + + this.loggedInSubject.next( + true, + ); + } + + this.initCompleteSubject.next( + true, + ); + + break; + } + + case AuthActions + .RevokeTokensSuccess: { + this.tokenSubject.next( + undefined, + ); + + this.userSubject.next( + undefined, + ); + + this.authenticatedSubject.next( + false, + ); + + this.loggedInSubject.next( + false, + ); + + break; + } + + case AuthActions + .LoadUserInfoSuccess: { + this.userSubject.next( + action.user, + ); + + break; + } + + case AuthActions + .LoadUserInfoFailed: { + this.userSubject.next( + undefined, + ); + + break; + } + + default: + break; + } + + this.authSubject.next( + action, + ); + } +} diff --git a/frontend/app/src/app/modules/auth/auth.service.ts b/frontend/app/src/app/modules/auth/auth.service.ts deleted file mode 100644 index 1b38118f..00000000 --- a/frontend/app/src/app/modules/auth/auth.service.ts +++ /dev/null @@ -1,526 +0,0 @@ -/* - * Copyright (C) 2023 StApps - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ -// Temporary use of direct file until the version with bug fix is released -// https://github.com/wi3land/ionic-appauth/blob/3716f4fc6b5491b0b75e049be0a47a5af8c4da6f/src/auth-service.ts -// Bug: https://github.com/wi3land/ionic-appauth/issues/154 -import { - AuthorizationError, - AuthorizationNotifier, - AuthorizationRequest, - AuthorizationRequestHandler, - AuthorizationRequestJson, - AuthorizationResponse, - AuthorizationServiceConfiguration, - BaseTokenRequestHandler, - DefaultCrypto, - GRANT_TYPE_AUTHORIZATION_CODE, - GRANT_TYPE_REFRESH_TOKEN, - JQueryRequestor, - LocalStorageBackend, - Requestor, - RevokeTokenRequest, - RevokeTokenRequestJson, - StorageBackend, - StringMap, - TokenRequest, - TokenRequestHandler, - TokenRequestJson, - TokenResponse, -} from '@openid/appauth'; -import { - ActionHistoryObserver, - AuthActionBuilder, - AuthActions, - AuthObserver, - AUTHORIZATION_RESPONSE_KEY, - AuthSubject, - BaseAuthObserver, - Browser, - DefaultBrowser, - EndSessionHandler, - EndSessionRequest, - EndSessionRequestJson, - IAuthAction, - IAuthConfig, - IAuthService, - IonicAuthorizationRequestHandler, - IonicEndSessionHandler, - IonicUserInfoHandler, - SessionObserver, - UserInfoHandler, -} from 'ionic-appauth'; -import {BehaviorSubject, Observable} from 'rxjs'; - -const TOKEN_RESPONSE_KEY = 'token_response'; -const AUTH_EXPIRY_BUFFER = 10 * 60 * -1; // 10 mins in seconds - -export abstract class AuthService implements IAuthService { - private _configuration?: AuthorizationServiceConfiguration; - - private _authConfig?: IAuthConfig; - - private _authSubject: AuthSubject = new AuthSubject(); - - private _actionHistory: ActionHistoryObserver = new ActionHistoryObserver(); - - private _session: SessionObserver = new SessionObserver(); - - private _authSubjectV2 = new BehaviorSubject(AuthActionBuilder.Init()); - - private _tokenSubject = new BehaviorSubject(undefined); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private _userSubject = new BehaviorSubject(undefined); - - private _authenticatedSubject = new BehaviorSubject(false); - - private _loggedInSubject = new BehaviorSubject(false); - - private _initComplete = new BehaviorSubject(false); - - protected tokenHandler: TokenRequestHandler; - - protected userInfoHandler: UserInfoHandler; - - protected requestHandler: AuthorizationRequestHandler; - - protected endSessionHandler: EndSessionHandler; - - constructor( - protected browser: Browser = new DefaultBrowser(), - protected storage: StorageBackend = new LocalStorageBackend(), - protected requestor: Requestor = new JQueryRequestor(), - ) { - this.tokenHandler = new BaseTokenRequestHandler(requestor); - this.userInfoHandler = new IonicUserInfoHandler(requestor); - this.requestHandler = new IonicAuthorizationRequestHandler(browser, storage); - this.endSessionHandler = new IonicEndSessionHandler(browser); - } - - /** - * @deprecated independant observers have been replaced by Rxjs - * this will be removed in a future release - * please use $ suffixed observers in future - */ - get history(): IAuthAction[] { - return [...this._actionHistory.history]; - } - - /** - * @deprecated independant observers have been replaced by Rxjs - * this will be removed in a future release - * please use $ suffixed observers in future - */ - get session() { - return this._session.session; - } - - get token$(): Observable { - return this._tokenSubject.asObservable(); - } - - get isAuthenticated$(): Observable { - return this._authenticatedSubject.asObservable(); - } - - /** - * Similar to isAuthenticated$, but will also return true if the token is expired - */ - get isLoggedIn$(): Observable { - return this._loggedInSubject.asObservable(); - } - - get initComplete$(): Observable { - return this._initComplete.asObservable(); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - get user$(): Observable { - return this._userSubject.asObservable(); - } - - get events$(): Observable { - return this._authSubjectV2.asObservable(); - } - - get authConfig(): IAuthConfig { - if (!this._authConfig) throw new Error('AuthConfig Not Defined'); - - return this._authConfig; - } - - set authConfig(value: IAuthConfig) { - this._authConfig = value; - } - - get configuration(): Promise { - if (!this._configuration) { - return AuthorizationServiceConfiguration.fetchFromIssuer( - this.authConfig.server_host, - this.requestor, - ).catch(() => { - throw new Error('Unable To Obtain Server Configuration'); - }); - } - - if (this._configuration == undefined) { - throw new Error('Unable To Obtain Server Configuration'); - } else { - return Promise.resolve(this._configuration); - } - } - - public async init() { - this.setupAuthorizationNotifier(); - this.loadTokenFromStorage(); - this.addActionObserver(this._actionHistory); - this.addActionObserver(this._session); - } - - protected notifyActionListers(action: IAuthAction) { - /* eslint-disable unicorn/no-useless-undefined */ - switch (action.action) { - case AuthActions.SignInFailed: - case AuthActions.SignOutSuccess: - case AuthActions.SignOutFailed: { - this._tokenSubject.next(undefined); - this._userSubject.next(undefined); - this._authenticatedSubject.next(false); - this._loggedInSubject.next(false); - break; - } - case AuthActions.LoadTokenFromStorageFailed: { - this._tokenSubject.next(undefined); - this._userSubject.next(undefined); - this._authenticatedSubject.next(false); - this._loggedInSubject.next(false); - this._initComplete.next(true); - break; - } - case AuthActions.SignInSuccess: - case AuthActions.RefreshSuccess: { - this._tokenSubject.next(action.tokenResponse); - this._authenticatedSubject.next(true); - this._loggedInSubject.next(true); - break; - } - case AuthActions.LoadTokenFromStorageSuccess: { - this._tokenSubject.next(action.tokenResponse); - this._authenticatedSubject.next((action.tokenResponse as TokenResponse).isValid(0)); - this._loggedInSubject.next(true); - this._initComplete.next(true); - break; - } - case AuthActions.RevokeTokensSuccess: { - this._tokenSubject.next(undefined); - break; - } - case AuthActions.LoadUserInfoSuccess: { - this._userSubject.next(action.user); - break; - } - case AuthActions.LoadUserInfoFailed: { - this._userSubject.next(undefined); - break; - } - } - - this._authSubjectV2.next(action); - this._authSubject.notify(action); - } - - protected setupAuthorizationNotifier() { - const notifier = new AuthorizationNotifier(); - this.requestHandler.setAuthorizationNotifier(notifier); - notifier.setAuthorizationListener((request, response, error) => - this.onAuthorizationNotification(request, response, error), - ); - } - - protected onAuthorizationNotification( - request: AuthorizationRequest, - response: AuthorizationResponse | null, - error: AuthorizationError | null, - ) { - const codeVerifier: string | undefined = - request.internal != undefined && this.authConfig.pkce ? request.internal.code_verifier : undefined; - - if (response != undefined) { - this.requestAccessToken(response.code, codeVerifier); - } else if (error == undefined) { - throw new Error('Unknown Error With Authentication'); - } else { - throw new Error(error.errorDescription); - } - } - - protected async internalAuthorizationCallback(url: string) { - this.browser.closeWindow(); - await this.storage.setItem(AUTHORIZATION_RESPONSE_KEY, url); - return this.requestHandler.completeAuthorizationRequestIfPossible(); - } - - protected async internalEndSessionCallback() { - this.browser.closeWindow(); - this._actionHistory.clear(); - this.notifyActionListers(AuthActionBuilder.SignOutSuccess()); - } - - protected async performEndSessionRequest(state?: string): Promise { - if (this._tokenSubject.value == undefined) { - //if user has no token they should not be logged in in the first place - this.endSessionCallback(); - } else { - const requestJson: EndSessionRequestJson = { - postLogoutRedirectURI: this.authConfig.end_session_redirect_url, - idTokenHint: this._tokenSubject.value.idToken || '', - state: state || undefined, - }; - - const request: EndSessionRequest = new EndSessionRequest(requestJson); - const returnedUrl: string | undefined = await this.endSessionHandler.performEndSessionRequest( - await this.configuration, - request, - ); - - //callback may come from showWindow or via another method - if (returnedUrl != undefined) { - this.endSessionCallback(); - } - } - } - - protected async performAuthorizationRequest(authExtras?: StringMap, state?: string): Promise { - const requestJson: AuthorizationRequestJson = { - response_type: AuthorizationRequest.RESPONSE_TYPE_CODE, - client_id: this.authConfig.client_id, - redirect_uri: this.authConfig.redirect_url, - scope: this.authConfig.scopes, - extras: authExtras, - state: state || undefined, - }; - - const request = new AuthorizationRequest(requestJson, new DefaultCrypto(), this.authConfig.pkce); - - if (this.authConfig.pkce) await request.setupCodeVerifier(); - - return this.requestHandler.performAuthorizationRequest(await this.configuration, request); - } - - protected async requestAccessToken(code: string, codeVerifier?: string): Promise { - const requestJSON: TokenRequestJson = { - grant_type: GRANT_TYPE_AUTHORIZATION_CODE, - code: code, - refresh_token: undefined, - redirect_uri: this.authConfig.redirect_url, - client_id: this.authConfig.client_id, - extras: codeVerifier - ? { - code_verifier: codeVerifier, - client_secret: this.authConfig.client_secret as string, - } - : { - client_secret: this.authConfig.client_secret as string, - }, - }; - - const token: TokenResponse = await this.tokenHandler.performTokenRequest( - await this.configuration, - new TokenRequest(requestJSON), - ); - await this.storage.setItem(TOKEN_RESPONSE_KEY, JSON.stringify(token.toJson())); - this.notifyActionListers(AuthActionBuilder.SignInSuccess(token)); - } - - protected async requestTokenRefresh() { - if (!this._tokenSubject.value) { - throw new Error('No Token Defined!'); - } - - const requestJSON: TokenRequestJson = { - grant_type: GRANT_TYPE_REFRESH_TOKEN, - refresh_token: this._tokenSubject.value?.refreshToken, - redirect_uri: this.authConfig.redirect_url, - client_id: this.authConfig.client_id, - }; - - const token: TokenResponse = await this.tokenHandler.performTokenRequest( - await this.configuration, - new TokenRequest(requestJSON), - ); - if (!token.accessToken) { - throw new Error('No Access Token Defined In Refresh Response'); - } - await this.storage.setItem(TOKEN_RESPONSE_KEY, JSON.stringify(token.toJson())); - this.notifyActionListers(AuthActionBuilder.RefreshSuccess(token)); - } - - protected async internalLoadTokenFromStorage() { - let token: TokenResponse | undefined; - const tokenResponseString: string | null = await this.storage.getItem(TOKEN_RESPONSE_KEY); - - if (tokenResponseString != undefined) { - token = new TokenResponse(JSON.parse(tokenResponseString)); - - if (token) { - return this.notifyActionListers(AuthActionBuilder.LoadTokenFromStorageSuccess(token)); - } - } - - throw new Error('No Token In Storage'); - } - - protected async requestTokenRevoke() { - const revokeRefreshJson: RevokeTokenRequestJson = { - token: (this._tokenSubject.value as TokenResponse).refreshToken as string, - token_type_hint: 'refresh_token', - client_id: this.authConfig.client_id, - }; - - const revokeAccessJson: RevokeTokenRequestJson = { - token: (this._tokenSubject.value as TokenResponse).accessToken, - token_type_hint: 'access_token', - client_id: this.authConfig.client_id, - }; - - await this.tokenHandler.performRevokeTokenRequest( - await this.configuration, - new RevokeTokenRequest(revokeRefreshJson), - ); - await this.tokenHandler.performRevokeTokenRequest( - await this.configuration, - new RevokeTokenRequest(revokeAccessJson), - ); - await this.storage.removeItem(TOKEN_RESPONSE_KEY); - this.notifyActionListers(AuthActionBuilder.RevokeTokensSuccess()); - } - - protected async internalRequestUserInfo() { - if (this._tokenSubject.value) { - const userInfo = await this.userInfoHandler.performUserInfoRequest( - await this.configuration, - this._tokenSubject.value, - ); - this.notifyActionListers(AuthActionBuilder.LoadUserInfoSuccess(userInfo)); - } else { - throw new Error('No Token Available'); - } - } - - public async loadTokenFromStorage() { - await this.internalLoadTokenFromStorage().catch(error => { - this.notifyActionListers(AuthActionBuilder.LoadTokenFromStorageFailed(error)); - }); - } - - public async signIn(authExtras?: StringMap, state?: string) { - await this.performAuthorizationRequest(authExtras, state).catch(error => { - this.notifyActionListers(AuthActionBuilder.SignInFailed(error)); - }); - } - - public async signOut(state?: string, revokeTokens?: boolean) { - if (revokeTokens) { - await this.revokeTokens(); - } - - await this.storage.removeItem(TOKEN_RESPONSE_KEY); - - if ((await this.configuration).endSessionEndpoint) { - await this.performEndSessionRequest(state).catch(error => { - this.notifyActionListers(AuthActionBuilder.SignOutFailed(error)); - }); - } - } - - public async revokeTokens() { - await this.requestTokenRevoke().catch(error => { - this.storage.removeItem(TOKEN_RESPONSE_KEY); - this.notifyActionListers(AuthActionBuilder.RevokeTokensFailed(error)); - }); - } - - public async refreshToken() { - await this.requestTokenRefresh().catch(error => { - this.notifyActionListers(AuthActionBuilder.RefreshFailed(error)); - }); - } - - public async loadUserInfo() { - await this.internalRequestUserInfo().catch(error => { - this.notifyActionListers(AuthActionBuilder.LoadUserInfoFailed(error)); - }); - } - - public authorizationCallback(callbackUrl: string): void { - this.internalAuthorizationCallback(callbackUrl).catch(error => { - this.notifyActionListers(AuthActionBuilder.SignInFailed(error)); - }); - } - - public endSessionCallback(): void { - this.internalEndSessionCallback().catch(error => { - this.notifyActionListers(AuthActionBuilder.SignOutFailed(error)); - }); - } - - public async getValidToken(buffer: number = AUTH_EXPIRY_BUFFER): Promise { - if (this._tokenSubject.value) { - if (this._tokenSubject.value.isValid(buffer)) { - return this._tokenSubject.value; - } else { - await this.refreshToken(); - if (this._tokenSubject.value) { - return this._tokenSubject.value; - } - } - } - - throw new Error('Unable To Obtain Valid Token'); - } - - /** - * @deprecated independant observers have been replaced by Rxjs - * this will be removed in a future release - * please use $ suffixed observers in future - */ - public addActionListener(function_: (action: IAuthAction) => void): AuthObserver { - const observer: AuthObserver = AuthObserver.Create(function_); - this.addActionObserver(observer); - return observer; - } - - /** - * @deprecated independant observers have been replaced by Rxjs - * this will be removed in a future release - * please use $ suffixed observers in future - */ - public addActionObserver(observer: BaseAuthObserver): void { - if (this._actionHistory.lastAction) { - observer.update(this._actionHistory.lastAction); - } - - this._authSubject.attach(observer); - } - - /** - * @deprecated independant observers have been replaced by Rxjs - * this will be removed in a future release - * please use $ suffixed observers in future - */ - public removeActionObserver(observer: BaseAuthObserver): void { - this._authSubject.detach(observer); - } -} diff --git a/frontend/app/src/app/modules/auth/capacitor-authorization-request-handler.ts b/frontend/app/src/app/modules/auth/capacitor-authorization-request-handler.ts new file mode 100644 index 00000000..ac32791f --- /dev/null +++ b/frontend/app/src/app/modules/auth/capacitor-authorization-request-handler.ts @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2026 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +import {Browser} from '@capacitor/browser'; +import { + AuthorizationError, + AuthorizationErrorJson, + AuthorizationRequest, + AuthorizationRequestHandler, + AuthorizationRequestJson, + AuthorizationRequestResponse, + AuthorizationResponse, + AuthorizationResponseJson, + AuthorizationServiceConfiguration, + BasicQueryStringUtils, + DefaultCrypto, + StorageBackend, + StringMap, +} from '@openid/appauth'; + +const AUTHORIZATION_REQUEST_KEY = + 'appauth_capacitor_authorization_request'; + +/** + * AppAuth authorization request handler for native Capacitor applications. + * + * AppAuth-JS handles: + * - state generation and validation data + * - PKCE code_verifier / code_challenge + * - authorization URL construction + * - AuthorizationNotifier + * + * This handler adds the Capacitor-specific parts: + * - persist the pending authorization request + * - open the authorization URL using @capacitor/browser + * - process the callback URL delivered through appUrlOpen + */ +export class CapacitorAuthorizationRequestHandler extends AuthorizationRequestHandler { + private callbackUrl?: string; + + constructor(private readonly storage: StorageBackend) { + super( + new BasicQueryStringUtils(), + new DefaultCrypto(), + ); + } + + public async performAuthorizationRequest( + configuration: AuthorizationServiceConfiguration, + request: AuthorizationRequest, + ): Promise { + /* + * Important: + * + * toJson() calls setupCodeVerifier() internally. + * Therefore PKCE is initialized before buildRequestUrl() + * constructs the authorization URL. + */ + const requestJson = + await request.toJson(); + + await this.storage.setItem( + AUTHORIZATION_REQUEST_KEY, + JSON.stringify(requestJson), + ); + + const url = + this.buildRequestUrl( + configuration, + request, + ); + + await Browser.open({url}); + } + + /** + * Completes the authorization flow using the URL received from + * Capacitor's App.addListener('appUrlOpen', ...). + */ + public async completeAuthorizationRequestFromUrl( + callbackUrl: string, + ): Promise { + this.callbackUrl = callbackUrl; + + /* + * The deep link has returned control to the application. + * Close the SFSafariViewController / Custom Tab if it is still open. + */ + try { + await Browser.close(); + } catch { + // Browser might already have been closed by the platform. + } + + await this.completeAuthorizationRequestIfPossible(); + } + + protected async completeAuthorizationRequest(): + Promise { + if (!this.callbackUrl) { + return null; + } + + const storedRequest = + await this.storage.getItem( + AUTHORIZATION_REQUEST_KEY, + ); + + if (!storedRequest) { + this.callbackUrl = undefined; + + throw new Error( + 'No pending authorization request found.', + ); + } + + const requestJson = + JSON.parse( + storedRequest, + ) as AuthorizationRequestJson; + + /* + * The serialized request also contains `internal`, including + * AppAuth's PKCE code_verifier. + */ + const request = + new AuthorizationRequest(requestJson); + + const parameters = + this.parseCallbackUrl( + this.callbackUrl, + ); + + const state = + parameters['state']; + + /* + * Never accept an authorization response for a different request. + */ + if (!state || state !== request.state) { + this.callbackUrl = undefined; + + throw new Error( + 'Authorization response state does not match the authorization request.', + ); + } + + const error = + parameters['error']; + + let response: + AuthorizationResponse | null = null; + + let authorizationError: + AuthorizationError | null = null; + + if (error) { + const errorJson: + AuthorizationErrorJson = { + error, + + error_description: + parameters[ + 'error_description' + ], + + error_uri: + parameters['error_uri'], + + state, + }; + + authorizationError = + new AuthorizationError( + errorJson, + ); + } else { + const code = + parameters['code']; + + if (!code) { + this.callbackUrl = undefined; + + throw new Error( + 'Authorization callback contains neither an authorization code nor an error.', + ); + } + + const responseJson: + AuthorizationResponseJson = { + code, + state, + }; + + response = + new AuthorizationResponse( + responseJson, + ); + } + + /* + * The response has been successfully associated with the + * pending request. It can now be consumed exactly once. + */ + await this.storage.removeItem( + AUTHORIZATION_REQUEST_KEY, + ); + + this.callbackUrl = undefined; + + return { + request, + response, + error: authorizationError, + }; + } + + private parseCallbackUrl( + callbackUrl: string, + ): StringMap { + const result: StringMap = {}; + + const url = new URL(callbackUrl); + + /* + * Authorization Code Flow normally returns code/state in + * the query string: + * + * openstapps:/callback?code=...&state=... + */ + url.searchParams.forEach( + (value, key) => { + result[key] = value; + }, + ); + + /* + * Also accept parameters in the fragment as a fallback. + */ + if (url.hash.length > 1) { + const hash = + url.hash.substring(1); + + const hashParameters = + new URLSearchParams(hash); + + hashParameters.forEach( + (value, key) => { + if (!(key in result)) { + result[key] = value; + } + }, + ); + } + + return result; + } +} diff --git a/frontend/app/src/app/modules/auth/capacitor-requestor.ts b/frontend/app/src/app/modules/auth/capacitor-requestor.ts index cdd2d601..204a94a3 100644 --- a/frontend/app/src/app/modules/auth/capacitor-requestor.ts +++ b/frontend/app/src/app/modules/auth/capacitor-requestor.ts @@ -13,61 +13,77 @@ * this program. If not, see . */ -import {Requestor} from '@openid/appauth'; -import {CapacitorHttp, HttpHeaders, HttpResponse} from '@capacitor/core'; -import {XhrSettings} from 'ionic-appauth/lib/cordova'; +import { + AppAuthError, + Requestor, +} from '@openid/appauth'; +import { + CapacitorHttp, + HttpHeaders, + HttpOptions, +} from '@capacitor/core'; -// REQUIRES CAPACITOR PLUGIN -// @capacitor-community/http -export class CapacitorRequestor extends Requestor { - constructor() { - super(); - } +type XhrSettings = Parameters[0]; +export class CapacitorRequestor implements Requestor { public async xhr(settings: XhrSettings): Promise { - if (!settings.method) settings.method = 'GET'; + if (!settings.url) { + throw new AppAuthError('A URL must be provided.'); + } - switch (settings.method) { - case 'GET': { - return this.get(settings.url, settings.headers); - } - case 'POST': { - return this.post(settings.url, settings.data, settings.headers); - } - case 'PUT': { - return this.put(settings.url, settings.data, settings.headers); - } - case 'DELETE': { - return this.delete(settings.url, settings.headers); + const method = (settings.method ?? 'GET').toUpperCase(); + const url = new URL(settings.url); + + let data: unknown; + + if (settings.data) { + if (method === 'POST') { + data = settings.data; + } else { + const searchParams = new URLSearchParams(settings.data); + + searchParams.forEach((value, key) => { + url.searchParams.append(key, value); + }); } } - } - private async get(url: string, headers: HttpHeaders) { - return CapacitorHttp.get({url, headers}).then((response: HttpResponse) => response.data as T); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private async post(url: string, data: any, headers: HttpHeaders) { - return CapacitorHttp.post({ - url, + const options: HttpOptions = { + url: url.toString(), + method, + headers: this.getHeaders(settings.headers), data, - headers, - }).then((response: HttpResponse) => { - return response.data as T; - }); + }; + + if (settings.dataType?.toLowerCase() === 'json') { + options.responseType = 'json'; + } + + const response = await CapacitorHttp.request(options); + + if (response.status < 200 || response.status >= 300) { + throw new AppAuthError( + `HTTP ${response.status}`, + response.data, + ); + } + + return response.data as T; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private async put(url: string, data: any, headers: HttpHeaders) { - return CapacitorHttp.put({ - url, - data, - headers, - }).then((response: HttpResponse) => response.data as T); - } + private getHeaders(headers: XhrSettings['headers']): HttpHeaders { + const result: HttpHeaders = {}; - private async delete(url: string, headers: HttpHeaders) { - return CapacitorHttp.delete({url, headers}).then((response: HttpResponse) => response.data as T); + if (!headers) { + return result; + } + + for (const [key, value] of Object.entries(headers)) { + if (value !== undefined && value !== null) { + result[key] = String(value); + } + } + + return result; } } diff --git a/frontend/app/src/app/modules/auth/default-auth.service.spec.ts b/frontend/app/src/app/modules/auth/default-auth.service.spec.ts deleted file mode 100644 index 4b5e1e0c..00000000 --- a/frontend/app/src/app/modules/auth/default-auth.service.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2023 StApps - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ -import {TestBed} from '@angular/core/testing'; -import {ConfigProvider} from '../config/config.provider'; -import {StorageProvider} from '../storage/storage.provider'; -import {DefaultAuthService} from './default-auth.service'; -import {Browser} from 'ionic-appauth'; -import {nowInSeconds, Requestor, StorageBackend} from '@openid/appauth'; -import {TranslateService} from '@ngx-translate/core'; -import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider'; -import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http'; -import {IonicStorage} from 'ionic-appauth/lib'; -import {RouterModule} from '@angular/router'; -import {LoggerTestingModule} from 'ngx-logger/testing'; - -describe('AuthService', () => { - let defaultAuthService: DefaultAuthService; - let storageBackendSpy: jasmine.SpyObj; - const storageProviderSpy = jasmine.createSpyObj('StorageProvider', ['init', 'get', 'has', 'put', 'search']); - const translateServiceSpy = jasmine.createSpyObj('TranslateService', ['setDefaultLang', 'use']); - - beforeEach(() => { - storageBackendSpy = jasmine.createSpyObj('StorageBackend', ['getItem']); - - TestBed.configureTestingModule({ - imports: [LoggerTestingModule, RouterModule.forRoot([])], - providers: [ - StAppsWebHttpClient, - { - provide: TranslateService, - useValue: translateServiceSpy, - }, - { - provide: StorageProvider, - useValue: storageProviderSpy, - }, - IonicStorage, - ConfigProvider, - Browser, - { - provide: StorageBackend, - useValue: storageBackendSpy, - }, - Requestor, - provideHttpClient(withInterceptorsFromDi()), - ], - }); - defaultAuthService = TestBed.inject(DefaultAuthService); - }); - - describe('loadTokenFromStorage', () => { - it('should provide false through isAuthenticated$ when there is no token response', async () => { - // eslint-disable-next-line unicorn/no-null - storageBackendSpy.getItem.and.returnValue(Promise.resolve(null)); - let loggedInHolder; - defaultAuthService.isAuthenticated$.subscribe(loggedIn => { - loggedInHolder = loggedIn; - }); - await defaultAuthService.loadTokenFromStorage(); - - expect(loggedInHolder).toBeFalse(); - }); - - it('should provide true through isAuthenticated$ when access token is valid', async () => { - const validToken = `{"access_token":"AT-XXXX","refresh_token":"RT-XXXX","scope":"","token_type":"bearer","issued_at":${nowInSeconds()},"expires_in":"${ - 8 * 60 * 60 - }"}`; - storageBackendSpy.getItem.and.returnValue(Promise.resolve(validToken)); - let loggedInHolder; - defaultAuthService.isAuthenticated$.subscribe(loggedIn => { - loggedInHolder = loggedIn; - }); - await defaultAuthService.loadTokenFromStorage(); - - expect(loggedInHolder).toBeTrue(); - }); - - it('should provide false through isAuthenticated$ when access token is invalid', async () => { - const invalidToken = `{"access_token":"AT-INVALID-XXXX","refresh_token":"RT-XXXX","scope":"","token_type":"bearer","issued_at":${ - nowInSeconds() - 9 * 60 * 60 - },"expires_in":"${8 * 60 * 60}"}`; - storageBackendSpy.getItem.and.returnValue(Promise.resolve(invalidToken)); - let loggedInHolder; - defaultAuthService.isAuthenticated$.subscribe(loggedIn => { - loggedInHolder = loggedIn; - }); - await defaultAuthService.loadTokenFromStorage(); - - expect(loggedInHolder).toBeFalse(); - }); - }); -}); diff --git a/frontend/app/src/app/modules/auth/default-auth.service.ts b/frontend/app/src/app/modules/auth/default-auth.service.ts deleted file mode 100644 index e9659d9e..00000000 --- a/frontend/app/src/app/modules/auth/default-auth.service.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2023 StApps - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -import { - AuthorizationRequestHandler, - AuthorizationServiceConfiguration, - JQueryRequestor, - LocalStorageBackend, - Requestor, - StorageBackend, - TokenRequestHandler, -} from '@openid/appauth'; -import {AuthActionBuilder, Browser, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth'; -import {ConfigProvider} from '../config/config.provider'; -import {SCAuthorizationProvider} from '@openstapps/core'; -import {getClientConfig, getEndpointsConfig} from './auth.provider.methods'; -import {Injectable, inject} from '@angular/core'; -import {AuthService} from './auth.service'; - -const TOKEN_RESPONSE_KEY = 'token_response'; - -@Injectable({ - providedIn: 'root', -}) -export class DefaultAuthService extends AuthService { - protected browser: Browser; - - protected storage: StorageBackend; - - protected requestor: Requestor; - - private readonly configProvider = inject(ConfigProvider); - - public localConfiguration: AuthorizationServiceConfiguration; - - protected tokenHandler: TokenRequestHandler; - - protected userInfoHandler: UserInfoHandler; - - protected requestHandler: AuthorizationRequestHandler; - - protected endSessionHandler: EndSessionHandler; - - constructor() { - const browser = inject(Browser) ?? new DefaultBrowser(); - const storage = inject(StorageBackend) ?? new LocalStorageBackend(); - const requestor = inject(Requestor) ?? new JQueryRequestor(); - - super(browser, storage, requestor); - - this.browser = browser; - this.storage = storage; - this.requestor = requestor; - } - - get configuration(): Promise { - if (!this.localConfiguration) throw new Error('Local Configuration Not Defined'); - - return Promise.resolve(this.localConfiguration); - } - - public async init() { - this.setupConfiguration(); - this.setupAuthorizationNotifier(); - await this.loadTokenFromStorage(); - } - - setupConfiguration() { - const authConfig = this.configProvider.getAnyValue('auth') as { - default: SCAuthorizationProvider; - }; - this.authConfig = getClientConfig('default', authConfig); - this.localConfiguration = new AuthorizationServiceConfiguration( - getEndpointsConfig('default', authConfig), - ); - } - - public async signOut() { - await this.revokeTokens().catch(error => { - this.notifyActionListers(AuthActionBuilder.SignOutFailed(error)); - }); - this.notifyActionListers(AuthActionBuilder.SignOutSuccess()); - } - - public async revokeTokens() { - // Note: only locally - await this.storage.removeItem(TOKEN_RESPONSE_KEY); - this.notifyActionListers(AuthActionBuilder.RevokeTokensSuccess()); - } -} diff --git a/frontend/app/src/app/modules/auth/factories/http.factory.ts b/frontend/app/src/app/modules/auth/factories/requestor.factory.ts similarity index 76% rename from frontend/app/src/app/modules/auth/factories/http.factory.ts rename to frontend/app/src/app/modules/auth/factories/requestor.factory.ts index 0a7e28b3..24345917 100644 --- a/frontend/app/src/app/modules/auth/factories/http.factory.ts +++ b/frontend/app/src/app/modules/auth/factories/requestor.factory.ts @@ -14,9 +14,11 @@ */ import {Platform} from '@ionic/angular/standalone'; +import {FetchRequestor, Requestor} from '@openid/appauth'; import {CapacitorRequestor} from '../capacitor-requestor'; -import {NgHttpService} from '../ng-http.service'; -export const httpFactory = (platform: Platform) => { - return platform.is('capacitor') ? new CapacitorRequestor() : new NgHttpService(); +export const requestorFactory = (platform: Platform): Requestor => { + return platform.is('capacitor') + ? new CapacitorRequestor() + : new FetchRequestor(); }; diff --git a/frontend/app/src/app/modules/auth/factories/storage.factory.ts b/frontend/app/src/app/modules/auth/factories/storage.factory.ts index 2b9a3449..cd4a2c7c 100644 --- a/frontend/app/src/app/modules/auth/factories/storage.factory.ts +++ b/frontend/app/src/app/modules/auth/factories/storage.factory.ts @@ -14,9 +14,11 @@ */ import {Platform} from '@ionic/angular/standalone'; -import {IonicStorage} from 'ionic-appauth/lib'; +import {LocalStorageBackend, StorageBackend} from '@openid/appauth'; import {SafeCapacitorSecureStorage} from '../../storage/capacitor-secure-storage'; -export const storageFactory = (platform: Platform) => { - return platform.is('capacitor') ? new SafeCapacitorSecureStorage() : new IonicStorage(); +export const storageFactory = (platform: Platform): StorageBackend => { + return platform.is('capacitor') + ? new SafeCapacitorSecureStorage() + : new LocalStorageBackend(); }; diff --git a/frontend/app/src/app/modules/auth/ng-http.service.ts b/frontend/app/src/app/modules/auth/ng-http.service.ts deleted file mode 100644 index bbb13a4f..00000000 --- a/frontend/app/src/app/modules/auth/ng-http.service.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2023 StApps - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -import {Injectable, inject} from '@angular/core'; -import {Requestor} from '@openid/appauth'; -import {HttpClient, HttpHeaders} from '@angular/common/http'; -import {XhrSettings} from 'ionic-appauth/lib/cordova'; -import {firstValueFrom, Observable} from 'rxjs'; - -@Injectable({ - providedIn: 'root', -}) -export class NgHttpService implements Requestor { - private http = inject(HttpClient); - - public async xhr(settings: XhrSettings): Promise { - if (!settings.method) { - settings.method = 'GET'; - } - - let observable: Observable; - - switch (settings.method) { - case 'GET': { - observable = this.http.get(settings.url, { - headers: this.getHeaders(settings.headers), - }); - break; - } - case 'POST': { - observable = this.http.post(settings.url, settings.data, { - headers: this.getHeaders(settings.headers), - }); - break; - } - case 'PUT': { - observable = this.http.put(settings.url, settings.data, { - headers: this.getHeaders(settings.headers), - }); - break; - } - case 'DELETE': { - observable = this.http.delete(settings.url, { - headers: this.getHeaders(settings.headers), - }); - break; - } - } - - return firstValueFrom(observable); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private getHeaders(headers: any): HttpHeaders { - let httpHeaders: HttpHeaders = new HttpHeaders(); - - if (headers !== undefined) { - for (const key of Object.keys(headers)) { - httpHeaders = httpHeaders.append(key, headers[key]); - } - } - - return httpHeaders; - } -} diff --git a/frontend/app/src/app/modules/auth/user-info.model.ts b/frontend/app/src/app/modules/auth/user-info.model.ts deleted file mode 100644 index c8bf3aff..00000000 --- a/frontend/app/src/app/modules/auth/user-info.model.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (C) 2022 StApps - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -export interface IUserInfo { - display_name: string; - role: string; - email: string; - user_name: string; -} diff --git a/frontend/app/src/app/modules/profile/id-cards.provider.ts b/frontend/app/src/app/modules/profile/id-cards.provider.ts index 1abd0ea6..ec9b7c97 100644 --- a/frontend/app/src/app/modules/profile/id-cards.provider.ts +++ b/frontend/app/src/app/modules/profile/id-cards.provider.ts @@ -1,16 +1,19 @@ import {Injectable, inject} from '@angular/core'; import {SCIdCard, SCThingOriginType, SCThingType, SCUserConfiguration} from '@openstapps/core'; import {from, of, Observable} from 'rxjs'; -import {AuthHelperService} from '../auth/auth-helper.service'; import {mergeMap, concatWith, filter, map, startWith, catchError, tap} from 'rxjs/operators'; import {ConfigProvider} from '../config/config.provider'; import {HttpClient} from '@angular/common/http'; import {EncryptedStorageProvider} from '../storage/encrypted-storage.provider'; +import {AuthProvider} from "../auth/auth.provider"; +import {AuthHelperService} from "../auth/auth-helper.service"; @Injectable({providedIn: 'root'}) export class IdCardsProvider { private authHelper = inject(AuthHelperService); + private authProvider = inject(AuthProvider); + private config = inject(ConfigProvider); private httpClient = inject(HttpClient); @@ -19,7 +22,7 @@ export class IdCardsProvider { getIdCards(): Observable { const feature = this.config.config.app.features.extern?.['idCards']; - const auth = this.authHelper.getProvider(); + const auth = this.authProvider; const storedIdCards = from( this.encryptedStorageProvider.get('id-cards') as Promise, ).pipe(filter(it => it !== undefined)); diff --git a/frontend/app/src/app/modules/profile/id-cards.spec.ts b/frontend/app/src/app/modules/profile/id-cards.spec.ts index 66d710f0..52dbc6b4 100644 --- a/frontend/app/src/app/modules/profile/id-cards.spec.ts +++ b/frontend/app/src/app/modules/profile/id-cards.spec.ts @@ -7,7 +7,6 @@ import {EncryptedStorageProvider} from '../storage/encrypted-storage.provider'; import {TestBed} from '@angular/core/testing'; import {TranslateService} from '@ngx-translate/core'; import {StorageBackend, Requestor} from '@openid/appauth'; -import {IonicStorage, Browser} from 'ionic-appauth'; import {LoggerTestingModule} from 'ngx-logger/testing'; import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider'; import {SimpleBrowser} from '../../util/browser.factory'; diff --git a/frontend/app/src/app/modules/profile/page/profile-page-section.component.ts b/frontend/app/src/app/modules/profile/page/profile-page-section.component.ts index 160a7d4d..25c6934c 100644 --- a/frontend/app/src/app/modules/profile/page/profile-page-section.component.ts +++ b/frontend/app/src/app/modules/profile/page/profile-page-section.component.ts @@ -19,6 +19,7 @@ import {AuthHelperService} from '../../auth/auth-helper.service'; import {mergeMap, of} from 'rxjs'; import Swiper from 'swiper'; import {toObservable, toSignal} from '@angular/core/rxjs-interop'; +import {AuthProvider} from "../../auth/auth.provider"; @Component({ selector: 'stapps-profile-page-section', @@ -32,10 +33,11 @@ export class ProfilePageSectionComponent { minSlideWidth = input(110); authHelper = inject(AuthHelperService); + authProvider = inject(AuthProvider); loggedIn = toSignal( toObservable(this.item).pipe( - mergeMap(item => (item.authProvider ? this.authHelper.getProvider().isLoggedIn$ : of(false))), + mergeMap(item => (item.authProvider ? this.authProvider.isLoggedIn$ : of(false))), ), ); @@ -66,13 +68,11 @@ export class ProfilePageSectionComponent { } async toggleLogIn() { - const providerType = this.item().authProvider; - if (!providerType) return; if (this.loggedIn()) { - await this.authHelper.getProvider().signOut(); - await this.authHelper.endBrowserSession(providerType); + await this.authProvider.signOut(); + await this.authHelper.endBrowserSession(); } else { - await this.authHelper.getProvider().signIn(); + await this.authProvider.signIn(); } } diff --git a/frontend/app/src/app/modules/profile/page/profile-page.component.ts b/frontend/app/src/app/modules/profile/page/profile-page.component.ts index cee601d3..79fd8522 100644 --- a/frontend/app/src/app/modules/profile/page/profile-page.component.ts +++ b/frontend/app/src/app/modules/profile/page/profile-page.component.ts @@ -13,7 +13,7 @@ * this program. If not, see . */ import {Component, inject} from '@angular/core'; -import {AuthHelperService} from '../../auth/auth-helper.service'; +import {AuthProvider} from '../../auth/auth.provider'; import {ActivatedRoute} from '@angular/router'; import {ScheduleProvider} from '../../calendar/schedule.provider'; import {profilePageSections} from '../../../../config/profile-page-sections'; @@ -25,7 +25,7 @@ import {profilePageSections} from '../../../../config/profile-page-sections'; standalone: false, }) export class ProfilePageComponent { - readonly authHelper = inject(AuthHelperService); + readonly authProvider = inject(AuthProvider); readonly activatedRoute = inject(ActivatedRoute); @@ -33,23 +33,7 @@ export class ProfilePageComponent { sections = profilePageSections; - async signIn() { - const originPath = this.activatedRoute.snapshot.queryParamMap.get('origin_path'); - await (originPath ? this.authHelper.setOriginPath(originPath) : this.authHelper.deleteOriginPath()); - await this.authHelper.getProvider().signIn(); - } - - async signOut() { - await this.authHelper.getProvider().signOut(); - } - - ionViewWillEnter() { - this.authHelper - .getProvider() - .getValidToken() - .then(() => void this.authHelper.getProvider().loadUserInfo()) - .catch(() => { - // noop - }); + ionViewWillEnter(): void { + void this.authProvider.loadUserInfo(); } } diff --git a/frontend/app/src/app/modules/storage/capacitor-secure-storage.ts b/frontend/app/src/app/modules/storage/capacitor-secure-storage.ts index 8db8a3dd..3ac28c39 100644 --- a/frontend/app/src/app/modules/storage/capacitor-secure-storage.ts +++ b/frontend/app/src/app/modules/storage/capacitor-secure-storage.ts @@ -12,19 +12,58 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {CapacitorSecureStorage} from 'ionic-appauth/lib/capacitor'; -/* - * Removes an item from storage before entering the new one to avoid issues - * after iOS upgrade (iOS 16) +import {StorageBackend} from '@openid/appauth'; +import {SecureStoragePlugin} from 'capacitor-secure-storage-plugin'; + +/** + * Secure storage backend for AppAuth on native Capacitor platforms. + * + * Removes an existing item before writing it again to avoid issues + * observed after iOS upgrades. */ -export class SafeCapacitorSecureStorage extends CapacitorSecureStorage { - async setItem(name: string, value: string): Promise { - if (!Storage) throw new Error('Capacitor Storage Is Undefined!'); - +export class SafeCapacitorSecureStorage implements StorageBackend { + public async getItem(name: string): Promise { try { - await super.removeItem(name); - } catch {} - return super.setItem(name, value); + const {value} = await SecureStoragePlugin.get({ + key: name, + }); + + return value; + } catch { + return null; + } + } + + public async setItem( + name: string, + value: string, + ): Promise { + try { + await SecureStoragePlugin.remove({ + key: name, + }); + } catch { + // Item does not exist yet. + } + + await SecureStoragePlugin.set({ + key: name, + value, + }); + } + + public async removeItem(name: string): Promise { + try { + await SecureStoragePlugin.remove({ + key: name, + }); + } catch { + // Removing a non-existing item is fine. + } + } + + public async clear(): Promise { + await SecureStoragePlugin.clear(); } } diff --git a/frontend/app/src/environments/environment.production.ts b/frontend/app/src/environments/environment.production.ts index edaf0db0..4e8662f7 100644 --- a/frontend/app/src/environments/environment.production.ts +++ b/frontend/app/src/environments/environment.production.ts @@ -19,9 +19,9 @@ export const environment = { backend_url: 'https://mobile.server.uni-frankfurt.de', - app_host: 'mobile.app.uni-frankfurt.de', + app_host: 'dev.app.uni-frankfurt.de', custom_url_scheme: 'de.anyschool.app', - backend_version: '4.0.0', + backend_version: '999.0.0', production: true, }; diff --git a/frontend/app/src/environments/environment.ts b/frontend/app/src/environments/environment.ts index b89ada44..fc1777e9 100644 --- a/frontend/app/src/environments/environment.ts +++ b/frontend/app/src/environments/environment.ts @@ -19,8 +19,9 @@ export const environment = { backend_url: 'https://mobile.server.uni-frankfurt.de', + // backend_url: 'http://localhost:3000', app_host: 'mobile.app.uni-frankfurt.de', - custom_url_scheme: 'de.anyschool.app', + custom_url_scheme: 'de.unifrankfurt.app', backend_version: '999.0.0', production: false, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a785e9e..53f55934 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,7 +248,7 @@ importers: version: 10.9.2(@types/node@22.15.31)(typescript@5.9.3) tsup: specifier: 8.5.0 - version: 8.5.0(jiti@1.21.7)(postcss@8.5.26)(typescript@5.9.3) + version: 8.5.0(jiti@1.21.7)(postcss@8.5.28)(typescript@5.9.3) typescript: specifier: 5.9.3 version: 5.9.3 @@ -371,7 +371,7 @@ importers: version: 10.9.2(@types/node@22.15.31)(typescript@5.9.3) tsup: specifier: 8.5.0 - version: 8.5.0(jiti@1.21.7)(postcss@8.5.26)(typescript@5.9.3) + version: 8.5.0(jiti@1.21.7)(postcss@8.5.28)(typescript@5.9.3) configuration/backend-config: devDependencies: @@ -547,7 +547,7 @@ importers: version: 10.9.2(@types/node@22.15.31)(typescript@5.9.3) tsup: specifier: 8.5.0 - version: 8.5.0(jiti@1.21.7)(postcss@8.5.26)(typescript@5.9.3) + version: 8.5.0(jiti@1.21.7)(postcss@8.5.28)(typescript@5.9.3) typescript: specifier: 5.9.3 version: 5.9.3 @@ -643,7 +643,7 @@ importers: version: 10.9.2(@types/node@22.15.31)(typescript@5.9.3) tsup: specifier: 8.5.0 - version: 8.5.0(jiti@1.21.7)(postcss@8.5.26)(typescript@5.9.3) + version: 8.5.0(jiti@1.21.7)(postcss@8.5.28)(typescript@5.9.3) typescript: specifier: 5.9.3 version: 5.9.3 @@ -718,7 +718,7 @@ importers: version: 3.1.1 tsup: specifier: 8.5.0 - version: 8.5.0(jiti@1.21.7)(postcss@8.5.26)(typescript@5.9.3) + version: 8.5.0(jiti@1.21.7)(postcss@8.5.28)(typescript@5.9.3) typescript: specifier: 5.9.3 version: 5.9.3 @@ -825,8 +825,8 @@ importers: specifier: 8.0.0 version: 8.0.0(@angular/common@20.3.31(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2))(@ngx-translate/core@15.0.0(@angular/common@20.3.31(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(rxjs@7.8.2) '@openid/appauth': - specifier: 1.3.2 - version: 1.3.2 + specifier: 1.4.0 + version: 1.4.0 '@openstapps/api': specifier: workspace:* version: link:../../packages/api @@ -860,12 +860,9 @@ importers: geojson: specifier: 0.5.0 version: 0.5.0 - ionic-appauth: - specifier: 2.1.0 - version: 2.1.0(rxjs@7.8.2) ionicons: - specifier: 8.1.0 - version: 8.1.0 + specifier: 8.0.13 + version: 8.0.13 jsonpath-plus: specifier: 10.3.0 version: 10.3.0 @@ -1106,10 +1103,10 @@ importers: version: 1.0.0(stylelint@17.14.1(typescript@5.9.3)) stylelint-config-recommended-scss: specifier: 17.0.1 - version: 17.0.1(postcss@8.5.26)(stylelint@17.14.1(typescript@5.9.3)) + version: 17.0.1(postcss@8.5.28)(stylelint@17.14.1(typescript@5.9.3)) stylelint-config-standard-scss: specifier: 17.0.0 - version: 17.0.0(postcss@8.5.26)(stylelint@17.14.1(typescript@5.9.3)) + version: 17.0.0(postcss@8.5.28)(stylelint@17.14.1(typescript@5.9.3)) surge: specifier: 0.23.1 version: 0.23.1 @@ -1220,7 +1217,7 @@ importers: version: 10.9.2(@types/node@22.15.31)(typescript@5.9.3) tsup: specifier: 8.5.0 - version: 8.5.0(jiti@1.21.7)(postcss@8.5.26)(typescript@5.9.3) + version: 8.5.0(jiti@1.21.7)(postcss@8.5.28)(typescript@5.9.3) typedoc: specifier: 0.25.12 version: 0.25.12(typescript@5.9.3) @@ -1353,7 +1350,7 @@ importers: version: 10.9.2(@types/node@22.15.31)(typescript@5.9.3) tsup: specifier: 8.5.0 - version: 8.5.0(jiti@1.21.7)(postcss@8.5.26)(typescript@5.9.3) + version: 8.5.0(jiti@1.21.7)(postcss@8.5.28)(typescript@5.9.3) typescript: specifier: 5.9.3 version: 5.9.3 @@ -1471,7 +1468,7 @@ importers: version: 10.9.2(@types/node@22.15.31)(typescript@5.9.3) tsup: specifier: 8.5.0 - version: 8.5.0(jiti@1.21.7)(postcss@8.5.26)(typescript@5.9.3) + version: 8.5.0(jiti@1.21.7)(postcss@8.5.28)(typescript@5.9.3) typedoc: specifier: 0.25.12 version: 0.25.12(typescript@5.9.3) @@ -1540,7 +1537,7 @@ importers: version: 10.9.2(@types/node@22.15.31)(typescript@5.9.3) tsup: specifier: 8.5.0 - version: 8.5.0(jiti@1.21.7)(postcss@8.5.26)(typescript@5.9.3) + version: 8.5.0(jiti@1.21.7)(postcss@8.5.28)(typescript@5.9.3) typedoc: specifier: 0.25.12 version: 0.25.12(typescript@5.9.3) @@ -1652,7 +1649,7 @@ importers: version: 10.9.2(@types/node@22.15.31)(typescript@5.9.3) tsup: specifier: 8.5.0 - version: 8.5.0(jiti@1.21.7)(postcss@8.5.26)(typescript@5.9.3) + version: 8.5.0(jiti@1.21.7)(postcss@8.5.28)(typescript@5.9.3) typedoc: specifier: 0.25.12 version: 0.25.12(typescript@5.9.3) @@ -1797,7 +1794,7 @@ importers: version: 10.9.2(@types/node@22.15.31)(typescript@5.9.3) tsup: specifier: 8.5.0 - version: 8.5.0(jiti@1.21.7)(postcss@8.5.26)(typescript@5.9.3) + version: 8.5.0(jiti@1.21.7)(postcss@8.5.28)(typescript@5.9.3) typedoc: specifier: 0.25.12 version: 0.25.12(typescript@5.9.3) @@ -1879,7 +1876,7 @@ importers: version: 10.9.2(@types/node@22.15.31)(typescript@5.9.3) tsup: specifier: 8.5.0 - version: 8.5.0(jiti@1.21.7)(postcss@8.5.26)(typescript@5.9.3) + version: 8.5.0(jiti@1.21.7)(postcss@8.5.28)(typescript@5.9.3) typedoc: specifier: 0.25.12 version: 0.25.12(typescript@5.9.3) @@ -2025,7 +2022,7 @@ importers: version: 10.9.2(@types/node@22.15.31)(typescript@5.9.3) tsup: specifier: 8.5.0 - version: 8.5.0(jiti@1.21.7)(postcss@8.5.26)(typescript@5.9.3) + version: 8.5.0(jiti@1.21.7)(postcss@8.5.28)(typescript@5.9.3) typedoc: specifier: 0.25.12 version: 0.25.12(typescript@5.9.3) @@ -2119,7 +2116,7 @@ importers: version: 10.9.2(@types/node@22.15.31)(typescript@5.9.3) tsup: specifier: 8.5.0 - version: 8.5.0(jiti@1.21.7)(postcss@8.5.26)(typescript@5.9.3) + version: 8.5.0(jiti@1.21.7)(postcss@8.5.28)(typescript@5.9.3) typedoc: specifier: 0.25.12 version: 0.25.12(typescript@5.9.3) @@ -2505,40 +2502,11 @@ packages: '@awesome-cordova-plugins/core': ^8.0.2 rxjs: ^5.5.0 || ^6.5.0 || ^7.3.0 - '@awesome-cordova-plugins/core@6.16.0': - resolution: {integrity: sha512-ep+nkDY6CyFBfqS/HS03PE+MZWiQoJWb/nLMwfj2ndC14UgZeVO3ecmEFknYjJOBf04emTbs7hbLewFrVkazmg==} - peerDependencies: - rxjs: ^5.5.0 || ^6.5.0 || ^7.3.0 - '@awesome-cordova-plugins/core@8.1.0': resolution: {integrity: sha512-7NkUC2FcXSlIuIH2xc1miIdN8/by2uewrIg5JBWauN1KCzCXLhCwZ39C8nBsWxCloAxCvec+MMU5SzJGGWfjTg==} peerDependencies: rxjs: ^5.5.0 || ^6.5.0 || ^7.3.0 - '@awesome-cordova-plugins/http@6.16.0': - resolution: {integrity: sha512-12YBMzyTXgWv9jnuiY1gh7ZKESLHQQNFTtsAvo78uNkEEk4ATPX9nLpWfC8kHzjI5aHNP+15bP85xt5nwntOcA==} - peerDependencies: - '@awesome-cordova-plugins/core': ^6.0.1 - rxjs: ^5.5.0 || ^6.5.0 || ^7.3.0 - - '@awesome-cordova-plugins/in-app-browser@6.16.0': - resolution: {integrity: sha512-5PANbiM2a5oTU4ce+UGZAMFgbuKi1XDXVhSuYrbCbZTiroWcfA4myj6y9s4sawtA8psWeUh+dksApr0ytrslBQ==} - peerDependencies: - '@awesome-cordova-plugins/core': ^6.0.1 - rxjs: ^5.5.0 || ^6.5.0 || ^7.3.0 - - '@awesome-cordova-plugins/safari-view-controller@6.16.0': - resolution: {integrity: sha512-K754S93w4itxiivOpJZjG/CpiAfczsRq6LFZtnuigoeZ6UOCIYAFvxEjmxXUsCHII9LJkmdwdrO0cKf1Ys8SpQ==} - peerDependencies: - '@awesome-cordova-plugins/core': ^6.0.1 - rxjs: ^5.5.0 || ^6.5.0 || ^7.3.0 - - '@awesome-cordova-plugins/secure-storage@6.16.0': - resolution: {integrity: sha512-8urVrC6HxgblmTexeHN+hZZXbuu+Ru9ifpOvPokih/4LJrHaoaB3vrSUAxsZdh/yulqxo0fTtdDtCKpKx58tUA==} - peerDependencies: - '@awesome-cordova-plugins/core': ^6.0.1 - rxjs: ^5.5.0 || ^6.5.0 || ^7.3.0 - '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -2713,8 +2681,8 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.29.8': - resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} engines: {node: '>=6.0.0'} hasBin: true @@ -3190,11 +3158,6 @@ packages: engines: {node: '>=10.3.0'} hasBin: true - '@capacitor/browser@6.0.6': - resolution: {integrity: sha512-VHOPkMR+JqKz2mf5YncnnOWvFdVYFTTLnPW+JRcsP1LobXmA0gtchER35PQ7XXqXU16eeacrehX+XF+rC6wmQw==} - peerDependencies: - '@capacitor/core': ^6.0.0 - '@capacitor/browser@8.0.4': resolution: {integrity: sha512-ORZ4u/y+7aMuu6yVuk6SP529fUZyqcbnuHB1q5McpA3o2SYfVoi28iB8rsQjI2ad1qlKJd29ZUuGtspXLBDlWg==} peerDependencies: @@ -3215,9 +3178,6 @@ packages: peerDependencies: '@capacitor/core': '>=8.0.0' - '@capacitor/core@6.2.1': - resolution: {integrity: sha512-urZwxa7hVE/BnA18oCFAdizXPse6fCKanQyEqpmz6cBJ2vObwMpyJDG5jBeoSsgocS9+Ax+9vb4ducWJn0y2qQ==} - '@capacitor/core@8.2.0': resolution: {integrity: sha512-oKaoNeNtH2iIZMDFVrb1atoyRECDGHcfLMunJ5KWN8DtvpVBeeA4c41e20NTuhMxw1cSYbpq2PV2hb+/9CJxlQ==} @@ -3266,11 +3226,6 @@ packages: peerDependencies: '@capacitor/core': '>=8.0.0' - '@capacitor/preferences@6.0.4': - resolution: {integrity: sha512-ziauSI1pgdyl+gduvvf8lInvzF3Wdyu/ok+u7NlnhKp8XOj9plJgtnXZWFiR8CiCK5wMvo+gFaNh3zhMyEUwpA==} - peerDependencies: - '@capacitor/core': ^6.0.0 - '@capacitor/preferences@8.0.1': resolution: {integrity: sha512-T6no3ebi79XJCk91U3Jp/liJUwgBdvHR+s6vhvPkPxSuch7z3zx5Rv1bdWM6sWruNx+pViuEGqZvbfCdyBvcHQ==} peerDependencies: @@ -3372,40 +3327,40 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@csstools/css-calc@3.3.0': - resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} + '@csstools/css-calc@3.4.3': + resolution: {integrity: sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==} engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.0 - '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-parser-algorithms': ^4.0.2 + '@csstools/css-tokenizer': ^4.0.2 - '@csstools/css-parser-algorithms@4.0.0': - resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + '@csstools/css-parser-algorithms@4.0.2': + resolution: {integrity: sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==} engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.2 - '@csstools/css-syntax-patches-for-csstree@1.1.9': - resolution: {integrity: sha512-iGGw4OsAYsS6pD29MdJ2bX/nJx65a04ZZiw6x+VwWlP2DdXf6f++Zmuv/OzALpdyfVhjbduIIF2cXM7HWBIe9A==} + '@csstools/css-syntax-patches-for-csstree@1.1.15': + resolution: {integrity: sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==} peerDependencies: css-tree: ^3.2.1 peerDependenciesMeta: css-tree: optional: true - '@csstools/css-tokenizer@4.0.0': - resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + '@csstools/css-tokenizer@4.0.2': + resolution: {integrity: sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==} engines: {node: '>=20.19.0'} - '@csstools/media-query-list-parser@5.0.0': - resolution: {integrity: sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==} + '@csstools/media-query-list-parser@5.0.2': + resolution: {integrity: sha512-qx19O8AbbLLbDSAcI6YOby/Vq8rV51psgIvlF5OZT/L8fQJDCyQifL6k+kYU4a8ySpzJ4RBzZkIs530bAn8y4Q==} engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.0 - '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-parser-algorithms': ^4.0.2 + '@csstools/css-tokenizer': ^4.0.2 - '@csstools/selector-resolve-nested@4.0.1': - resolution: {integrity: sha512-j3vdQu0XwLME5qOTWxm8cnmvsf423R2YL6DbKklCHZwkDm7UdKNu6RPlw4REIJhSlKBICY3B70/7QZdicLqZgg==} + '@csstools/selector-resolve-nested@4.0.2': + resolution: {integrity: sha512-JpjNBJFA+4OnAHKRK4AMp8xrYyLhI9XapOeBlP2PhRU/IylSw+1ep3BFNTksBJnrTS3KT6kqKNVfpzLLwZUW5Q==} engines: {node: '>=20.19.0'} peerDependencies: postcss-selector-parser: ^7.1.1 @@ -3790,8 +3745,8 @@ packages: '@hapi/topo@5.1.0': resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} - '@hono/node-server@2.1.1': - resolution: {integrity: sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==} + '@hono/node-server@2.1.3': + resolution: {integrity: sha512-TA//nWMqPhbfdfneACk6t5a9eqbS9lABEPyKn0/xZTah3H3U2XaVg85rJFl0/Fyit0I552YDHgXGVSf3GwqbUw==} engines: {node: '>=20'} peerDependencies: hono: ^4 @@ -4503,10 +4458,6 @@ packages: '@ngx-translate/core': '>=15.0.0' rxjs: ^6.5.5 || ^7.4.0 - '@noble/hashes@1.4.0': - resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} - engines: {node: '>= 16'} - '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -4584,8 +4535,8 @@ packages: resolution: {integrity: sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==} engines: {node: '>=8.0'} - '@openid/appauth@1.3.2': - resolution: {integrity: sha512-NoOejniaqzOEbHg3RcBZtTriYqhqpQFgTC4lDNaRbgRCnpz6n8PlxWlCbh2N1K5qKawfxRP29/Wiho3FrXQ3Qw==} + '@openid/appauth@1.4.0': + resolution: {integrity: sha512-/l6GlJwloaWu1yiojMSrrR1zu7EQ84DK88RxBK2xw6uqfup+gKFEzbDoG2LOVUnBM3L5HMJNYAJfDFelKri3qA==} '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} @@ -4604,126 +4555,120 @@ packages: '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} - '@parcel/watcher-android-arm64@2.5.6': - resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + '@parcel/watcher-android-arm64@2.6.0': + resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [android] - '@parcel/watcher-darwin-arm64@2.5.6': - resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + '@parcel/watcher-darwin-arm64@2.6.0': + resolution: {integrity: sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [darwin] - '@parcel/watcher-darwin-x64@2.5.6': - resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + '@parcel/watcher-darwin-x64@2.6.0': + resolution: {integrity: sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [darwin] - '@parcel/watcher-freebsd-x64@2.5.6': - resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + '@parcel/watcher-freebsd-x64@2.6.0': + resolution: {integrity: sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [freebsd] - '@parcel/watcher-linux-arm-glibc@2.5.6': - resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + '@parcel/watcher-linux-arm-glibc@2.6.0': + resolution: {integrity: sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - '@parcel/watcher-linux-arm-musl@2.5.6': - resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + '@parcel/watcher-linux-arm-musl@2.6.0': + resolution: {integrity: sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - '@parcel/watcher-linux-arm64-glibc@2.5.6': - resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + '@parcel/watcher-linux-arm64-glibc@2.6.0': + resolution: {integrity: sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - '@parcel/watcher-linux-arm64-musl@2.5.6': - resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + '@parcel/watcher-linux-arm64-musl@2.6.0': + resolution: {integrity: sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - '@parcel/watcher-linux-x64-glibc@2.5.6': - resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + '@parcel/watcher-linux-x64-glibc@2.6.0': + resolution: {integrity: sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - '@parcel/watcher-linux-x64-musl@2.5.6': - resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + '@parcel/watcher-linux-x64-musl@2.6.0': + resolution: {integrity: sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - '@parcel/watcher-win32-arm64@2.5.6': - resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + '@parcel/watcher-win32-arm64@2.6.0': + resolution: {integrity: sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [win32] - '@parcel/watcher-win32-ia32@2.5.6': - resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.5.6': - resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + '@parcel/watcher-win32-x64@2.6.0': + resolution: {integrity: sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [win32] - '@parcel/watcher@2.5.6': - resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + '@parcel/watcher@2.6.0': + resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==} engines: {node: '>= 10.0.0'} - '@peculiar/asn1-cms@2.9.4': - resolution: {integrity: sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw==} + '@peculiar/asn1-cms@2.10.0': + resolution: {integrity: sha512-CkX0H4NCIOMHOU3rh2xXZywinYZ/EnJlaOlJiGI0e5L4XjFqHf4iH30LMbXWChVppdA9ljbanjtzQFOhDNfTWg==} engines: {node: '>=14'} - '@peculiar/asn1-csr@2.9.4': - resolution: {integrity: sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw==} + '@peculiar/asn1-csr@2.10.0': + resolution: {integrity: sha512-jTPTr/9rxKM+niQLMF3jiAMb0lWHUhUHIuSOhdGCIfpB4FAQYf9SoiXOgxP8bS/68IB+bj+1OHvVnDVCQfVUZw==} engines: {node: '>=14'} - '@peculiar/asn1-ecc@2.9.4': - resolution: {integrity: sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ==} + '@peculiar/asn1-ecc@2.10.0': + resolution: {integrity: sha512-GFd3iOjFrWX+QWH2R2dO5QSJyyRGv9CIBKtRlGlPNCvb2RvmCbBDexe91MJgV76c69d998Xsa+CvuQ98seoiPg==} engines: {node: '>=14'} - '@peculiar/asn1-pfx@2.9.4': - resolution: {integrity: sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw==} + '@peculiar/asn1-pfx@2.10.0': + resolution: {integrity: sha512-1y3QK9ZH1IPleAMmRoJlPS10eGkAWnULETW8zDFNUK1ffqpG7zmDY+kYBMYQgwlcQZ2iitLm2z2EBP0xzRXpaA==} engines: {node: '>=14'} - '@peculiar/asn1-pkcs8@2.9.4': - resolution: {integrity: sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ==} + '@peculiar/asn1-pkcs8@2.10.0': + resolution: {integrity: sha512-Ri+BZT9bnwqlHWmhs7lvWjvJRxKev2hA2+4EbZajgeYWCbR8hyv0znF4DhsFouOhMj2nSDV/iGJ6DMQtwITnCw==} engines: {node: '>=14'} - '@peculiar/asn1-pkcs9@2.9.4': - resolution: {integrity: sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ==} + '@peculiar/asn1-pkcs9@2.10.0': + resolution: {integrity: sha512-XIXsbDQFezYk6fudczkuvawkRD4GNpISGCqfYhdkJlvcGF/RFknU+0DDJagOaJqBf+PdPzk4J9oMqDGcvfR9HQ==} engines: {node: '>=14'} - '@peculiar/asn1-rsa@2.9.4': - resolution: {integrity: sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA==} + '@peculiar/asn1-rsa@2.10.0': + resolution: {integrity: sha512-4Jvmwlh3gZAhNZ4/u7JSyLuce9hofOFZ4o3PYKAccCImGnyaT5CMym/ATgvAvqpOYFNW531oPZvJlEBhJy9VfA==} engines: {node: '>=14'} - '@peculiar/asn1-schema@2.9.4': - resolution: {integrity: sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==} + '@peculiar/asn1-schema@2.10.0': + resolution: {integrity: sha512-GhokD41lV4gQrrLYm3wCkHfBOnJrnhDMgt4XeMW8gzfE1UdJqIuSwsE+ggf82XBjUXRJylcm+KIGQFa4utIVLw==} engines: {node: '>=14'} - '@peculiar/asn1-x509-attr@2.9.4': - resolution: {integrity: sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg==} + '@peculiar/asn1-x509-attr@2.10.0': + resolution: {integrity: sha512-/85GtKOKmgvuSJNlaFfwGWNdRSZZ+hpF02NyM01XiCdzpaZONiBltDyfluFPvFx966CR+ZHNSG1jniwpy07oGg==} engines: {node: '>=14'} - '@peculiar/asn1-x509@2.9.4': - resolution: {integrity: sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q==} + '@peculiar/asn1-x509@2.10.0': + resolution: {integrity: sha512-ucNVg8+ANveTpMN3fy9lA2alryONdXc2A4cEG2hMniWbvQt+YOZoe8BI80YYNO8FBcuDY1qbDQ4uQGQVraHxGA==} engines: {node: '>=14'} '@peculiar/utils@2.0.3': @@ -5081,13 +5026,15 @@ packages: '@sinonjs/text-encoding@0.7.3': resolution: {integrity: sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==} - deprecated: |- - Deprecated: no longer maintained and no longer used by Sinon packages. See - https://github.com/sinonjs/nise/issues/243 for replacement details. '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@stencil/core@4.43.2': + resolution: {integrity: sha512-hOprMw/n1fyu5OOtZG7N8sqiKxPshR1oss/y1qr6r98cVV9NcVoCMz2x2TT2enkHan6pCMpiQgUU7vmN90lIVw==} + engines: {node: '>=16.0.0', npm: '>=7.10.0'} + hasBin: true + '@stencil/core@4.43.5': resolution: {integrity: sha512-cgWD+GeuvJpTe1WQn40p02+BJ2j0j1YJ17GdkF2qKIQ23s2e3Zivq5yISXS3dcuV6oUJFN93jprdk+nk/sq99Q==} engines: {node: '>=16.0.0', npm: '>=7.10.0'} @@ -5295,8 +5242,8 @@ packages: '@types/jasminewd2@2.0.13': resolution: {integrity: sha512-aJ3wj8tXMpBrzQ5ghIaqMisD8C3FIrcO6sDKHqFbuqAsI7yOxj0fA7MrRCPLZHIVUjERIwsMmGn/vB0UQ9u0Hg==} - '@types/jquery@3.5.34': - resolution: {integrity: sha512-3m3939S3erqmTLJANS/uy0B6V7BorKx7RorcGZVjZ62dF5PAGbKEDZK1CuLtKombJkFA2T1jl8LAIIs7IV6gBQ==} + '@types/jquery@4.0.1': + resolution: {integrity: sha512-9a59A/tycXgYuPABcp6/3spSShn0NT2UOM4EfHvMumjYi4lJWTsK5SZWjhx3yRm9IHGCeWXdV2YfNsrWrft/CA==} '@types/json-patch@0.0.30': resolution: {integrity: sha512-MhCUjojzDhVLnZnxwPwa+rETFRDQ0ffjxYdrqOP6TBO2O0/Z64PV5tNeYApo4bc4y4frbWOrRwv/eEkXlI13Rw==} @@ -5540,7 +5487,6 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@vitejs/plugin-basic-ssl@2.1.0': resolution: {integrity: sha512-dOxxrhgyDIEUADhb/8OlV9JIqYLgos03YorAueTIeOUskLJSEsfwCByjbu98ctXitUN3znXKp0bYD/WHSudCeA==} @@ -5599,12 +5545,11 @@ packages: '@xmldom/xmldom@0.7.13': resolution: {integrity: sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==} engines: {node: '>=10.0.0'} - deprecated: this version has critical issues, please update to the latest version + deprecated: this version is no longer supported, please update to at least 0.8.* '@xmldom/xmldom@0.8.11': resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} - deprecated: this version has critical issues, please update to the latest version '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -5993,7 +5938,6 @@ packages: basic-ftp@5.2.0: resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==} engines: {node: '>=10.0.0'} - deprecated: Security vulnerability fixed in 5.2.1, please upgrade batch@0.6.1: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} @@ -6049,10 +5993,6 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@1.20.8: - resolution: {integrity: sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@2.3.0: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} @@ -6212,11 +6152,6 @@ packages: caniuse-lite@1.0.30001778: resolution: {integrity: sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==} - capacitor-secure-storage-plugin@0.10.0: - resolution: {integrity: sha512-dV4E+HTZAJWC3gef7sBXaAkkb6wvcZHyXjJIHXNb3yz9gRQ/5VMLqCxa0khqpwgWh5oIbo4XFxg3g5tEkfaNMg==} - peerDependencies: - '@capacitor/core': ^6.0.0 - capacitor-secure-storage-plugin@0.13.0: resolution: {integrity: sha512-+rLC/9Z0LTaRRt6L6HjBwcDh5gqgI3NPmDSwo4hk41XQOy3EBrRo81VleIqFsowsMA3oMT+E59Bl8/HiWk0nhQ==} peerDependencies: @@ -6262,8 +6197,8 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - chardet@2.1.1: - resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} @@ -6558,7 +6493,6 @@ packages: conventional-changelog-atom@2.0.8: resolution: {integrity: sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==} engines: {node: '>=10'} - deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead. conventional-changelog-cli@2.2.2: resolution: {integrity: sha512-8grMV5Jo8S0kP3yoMeJxV2P5R6VJOqK72IiSV9t/4H5r/HiRqEBQ83bYGuz4Yzfdj4bjaAEhZN/FFbsFXr5bOA==} @@ -6568,7 +6502,6 @@ packages: conventional-changelog-codemirror@2.0.8: resolution: {integrity: sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==} engines: {node: '>=10'} - deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead. conventional-changelog-conventionalcommits@4.6.3: resolution: {integrity: sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==} @@ -6577,32 +6510,26 @@ packages: conventional-changelog-core@4.2.4: resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==} engines: {node: '>=10'} - deprecated: Deprecated and no longer maintained. Please use conventional-changelog instead. conventional-changelog-ember@2.0.9: resolution: {integrity: sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==} engines: {node: '>=10'} - deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead. conventional-changelog-eslint@3.0.9: resolution: {integrity: sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==} engines: {node: '>=10'} - deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead. conventional-changelog-express@2.0.6: resolution: {integrity: sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==} engines: {node: '>=10'} - deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead. conventional-changelog-jquery@3.0.11: resolution: {integrity: sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==} engines: {node: '>=10'} - deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead. conventional-changelog-jshint@2.0.9: resolution: {integrity: sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==} engines: {node: '>=10'} - deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead. conventional-changelog-preset-loader@2.3.4: resolution: {integrity: sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==} @@ -6635,6 +6562,9 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -6706,6 +6636,15 @@ packages: typescript: optional: true + cosmiconfig@9.0.1: + resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + cosmiconfig@9.0.2: resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==} engines: {node: '>=14'} @@ -7280,8 +7219,8 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - earcut@3.2.3: - resolution: {integrity: sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==} + earcut@3.2.4: + resolution: {integrity: sha512-FHc6yNpnTORB0tM5kmZ03iD2mDuNakDE2lvf4Hw1XkfxtEHIGVi01B/GqUSwx+NcC1WGjidAfpTXzeALtPtfpg==} eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -7631,8 +7570,8 @@ packages: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} - express@4.22.3: - resolution: {integrity: sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==} + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} express@5.2.1: @@ -7756,6 +7695,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -7816,6 +7759,15 @@ packages: debug: optional: true + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + fontkit@2.0.4: resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==} @@ -7838,10 +7790,6 @@ packages: resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} engines: {node: '>= 0.12'} - form-data@4.0.4: - resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} - engines: {node: '>= 6'} - form-data@4.0.6: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} @@ -7986,7 +7934,6 @@ packages: git-raw-commits@2.0.11: resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==} engines: {node: '>=10'} - deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. hasBin: true git-remote-origin-url@2.0.0: @@ -7996,7 +7943,6 @@ packages: git-semver-tags@4.1.1: resolution: {integrity: sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==} engines: {node: '>=10'} - deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. hasBin: true gitconfiglocal@1.0.0: @@ -8178,8 +8124,8 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - hono@4.13.8: - resolution: {integrity: sha512-/Gng7NfoykZl2pjukW5Z6+8Yxm3BPRf86GTbQnt0SbySkvax4fyL4H3HhY1cCpBGmiW9XDRFzRV+CXK2W8QudQ==} + hono@4.13.12: + resolution: {integrity: sha512-6E2QDAc9Ick9Sq77ZrGS/dk2WUYni91aufTw6LJKpV7w8kW5/GxVUc650FOADOmlwg3K+f7Pun6XlV+pYmW6gw==} engines: {node: '>=16.9.0'} hookified@1.15.1: @@ -8366,8 +8312,8 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - immutable@5.1.5: - resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + immutable@5.1.9: + resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} @@ -8437,20 +8383,15 @@ packages: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} - ionic-appauth@2.1.0: - resolution: {integrity: sha512-KSNavekWYe1xD3vn9IOxa48ZM8gVuiUnUuhxKzPLzvLwE4Nr4aEs3XMUyM0HEcoCytiVhuO9ddN3HA+eqhb4Hw==} - peerDependencies: - rxjs: ^6.5.5 || ^7.4.0 - - ionicons@8.1.0: - resolution: {integrity: sha512-XSM2gYWTXxSYTwjmKWAoy4yR7AbAFDjpc5ZIivAiGXskM0fe5CdMPfs6InQRUcrYtVb2WZ/0HQ1/jeh3JW78SA==} + ionicons@8.0.13: + resolution: {integrity: sha512-2QQVyG2P4wszne79jemMjWYLp0DBbDhr4/yFroPCxvPP1wtMxgdIV3l5n+XZ5E9mgoXU79w7yTWpm2XzJsISxQ==} ip-address@10.1.0: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} - ip-address@10.7.2: - resolution: {integrity: sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==} + ip-address@10.7.3: + resolution: {integrity: sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==} engines: {node: '>= 12'} ip-regex@4.3.0: @@ -8643,6 +8584,10 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} + is-plain-object@5.1.0: + resolution: {integrity: sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==} + engines: {node: '>=0.10.0'} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -9607,6 +9552,10 @@ packages: resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} engines: {node: '>=8'} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -9670,6 +9619,10 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -9717,8 +9670,8 @@ packages: nan@2.22.2: resolution: {integrity: sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==} - nanoid@3.3.18: - resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -9751,9 +9704,9 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} + negotiator@1.1.0: + resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} + engines: {node: '>=18'} neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -10100,8 +10053,8 @@ packages: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} - p-map@7.0.4: - resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} + p-map@7.0.8: + resolution: {integrity: sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw==} engines: {node: '>=18'} p-retry@6.2.1: @@ -10206,9 +10159,6 @@ packages: path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} - path-to-regexp@0.1.13: - resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} - path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -10292,8 +10242,8 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - pkijs@3.4.0: - resolution: {integrity: sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==} + pkijs@3.4.1: + resolution: {integrity: sha512-Oo/NZcSWccq8KyoG7gLE9fnltgHns+pNCjCAp/WmjsUySi+sX7y4z4Xqu4fVb42CDHzRPl33fjzT15V1wvcyhA==} engines: {node: '>=16.0.0'} plantuml-encoder@1.4.0: @@ -10398,8 +10348,8 @@ packages: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} - postcss-selector-parser@7.1.5: - resolution: {integrity: sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==} + postcss-selector-parser@7.1.6: + resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==} engines: {node: '>=4'} postcss-sorting@8.0.2: @@ -10414,8 +10364,8 @@ packages: resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.26: - resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} potpack@2.1.0: @@ -10628,18 +10578,10 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - range-parser@1.3.0: - resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} - engines: {node: '>= 0.6'} - raw-body@2.5.2: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} - raw-body@2.5.3: - resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} - engines: {node: '>= 0.8'} - raw-body@3.0.2: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} @@ -10914,9 +10856,6 @@ packages: resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} engines: {npm: '>=2.0.0'} - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} @@ -11020,6 +10959,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -11039,6 +10983,10 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} @@ -11046,8 +10994,8 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - serialize-javascript@7.1.1: - resolution: {integrity: sha512-k3CMsaIvvdSwm8oLB4MXSl0wH2/cwlH7xGcnRd2DaeRmBkbzYmyT8j0tsX60DwD1eRwHTpNpH8ljKu9oUT1MeQ==} + serialize-javascript@7.1.2: + resolution: {integrity: sha512-GL2BWwVa6JydKO6l/ljVgjAZF4QJ3S7dWDWi53s5GZT8PJzD2fNxi67HANQGKAqPrwEpL5ba7gUBGi0Ls/sEoQ==} engines: {node: '>=20.0.0'} serve-index@1.9.2: @@ -11058,6 +11006,10 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -11108,8 +11060,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.10.0: - resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} + shell-quote@1.12.0: + resolution: {integrity: sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==} engines: {node: '>= 0.4'} shelljs@0.8.5: @@ -11408,8 +11360,8 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} - string-width@8.2.2: - resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} + string-width@8.3.0: + resolution: {integrity: sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==} engines: {node: '>=20'} string.prototype.trim@1.2.10: @@ -11523,8 +11475,8 @@ packages: peerDependencies: stylelint: ^14.0.0 || ^15.0.0 || ^16.0.1 - stylelint-scss@7.2.0: - resolution: {integrity: sha512-6E79Bachv0Iz0gqRUZgdqdXCsiq26DWBWIBNHYtjTmAp3wJu6cp/I37VfW7BPntmh2puF3bY09XWl4HZGrLhzw==} + stylelint-scss@7.3.0: + resolution: {integrity: sha512-FBz075AUFoEJ1WQKsgsRr6EJKcjy7jxQjV8xsAqbPcnhNiXnJ7C489vqUyDZccBy1GorewlshERkaMuCyOKXaw==} engines: {node: '>=20.19.0'} peerDependencies: stylelint: ^16.8.2 || ^17.0.0 @@ -11576,8 +11528,8 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} - supports-hyperlinks@4.5.0: - resolution: {integrity: sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==} + supports-hyperlinks@4.6.0: + resolution: {integrity: sha512-FL/q/YePS4tFFTNiBOR+zPk2ZrKheqNtmTJ/9OUZ4e1wKHJofDbGQSmSsxS0XNnabSkqexryCaXGvEKz1vf9UQ==} engines: {node: '>=20'} supports-preserve-symlinks-flag@1.0.0: @@ -12092,8 +12044,8 @@ packages: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} - undici@6.28.1: - resolution: {integrity: sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==} + undici@6.29.0: + resolution: {integrity: sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==} engines: {node: '>=18.17'} undici@7.24.0: @@ -12122,8 +12074,8 @@ packages: unicode-trie@2.0.0: resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} - unicorn-magic@0.4.0: - resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + unicorn-magic@0.4.1: + resolution: {integrity: sha512-lzlXPoVB0Uy/FvTaUgX39RbixYiSKoc3JSoSf01+0c2B6FR3qdYIPN7Qjo/AV7n6sqLCk0509cT3LRj1oSZ1+A==} engines: {node: '>=20'} union@0.5.0: @@ -12199,17 +12151,15 @@ packages: uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. hasBin: true uuid@7.0.3: resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@9.0.1: @@ -12682,8 +12632,8 @@ packages: resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} - yargs@18.1.0: - resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} + yargs@18.2.0: + resolution: {integrity: sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} yauzl@2.10.0: @@ -12820,7 +12770,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2003.37(chokidar@5.0.0) - '@angular-devkit/build-webpack': 0.2003.37(chokidar@5.0.0)(webpack-dev-server@5.2.6(tslib@2.8.1)(webpack@5.105.0))(webpack@5.105.0(esbuild@0.28.1)) + '@angular-devkit/build-webpack': 0.2003.37(chokidar@5.0.0)(webpack-dev-server@5.2.6(tslib@2.8.1)(webpack@5.105.0(esbuild@0.28.1)))(webpack@5.105.0(esbuild@0.28.1)) '@angular-devkit/core': 20.3.37(chokidar@5.0.0) '@angular/build': 20.3.37(bc31431be26dbfb88fdd00e555208d44) '@angular/compiler-cli': 20.3.31(@angular/compiler@20.3.31)(typescript@5.9.3) @@ -12870,8 +12820,8 @@ snapshots: tslib: 2.8.1 typescript: 5.9.3 webpack: 5.105.0(esbuild@0.28.1) - webpack-dev-middleware: 7.4.2(tslib@2.8.1)(webpack@5.105.0) - webpack-dev-server: 5.2.6(tslib@2.8.1)(webpack@5.105.0) + webpack-dev-middleware: 7.4.2(tslib@2.8.1)(webpack@5.105.0(esbuild@0.28.1)) + webpack-dev-server: 5.2.6(tslib@2.8.1)(webpack@5.105.0(esbuild@0.28.1)) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.105.0(esbuild@0.28.1)) optionalDependencies: @@ -12903,12 +12853,12 @@ snapshots: - webpack-cli - yaml - '@angular-devkit/build-webpack@0.2003.37(chokidar@5.0.0)(webpack-dev-server@5.2.6(tslib@2.8.1)(webpack@5.105.0))(webpack@5.105.0(esbuild@0.28.1))': + '@angular-devkit/build-webpack@0.2003.37(chokidar@5.0.0)(webpack-dev-server@5.2.6(tslib@2.8.1)(webpack@5.105.0(esbuild@0.28.1)))(webpack@5.105.0(esbuild@0.28.1))': dependencies: '@angular-devkit/architect': 0.2003.37(chokidar@5.0.0) rxjs: 7.8.2 webpack: 5.105.0(esbuild@0.28.1) - webpack-dev-server: 5.2.6(tslib@2.8.1)(webpack@5.105.0) + webpack-dev-server: 5.2.6(tslib@2.8.1)(webpack@5.105.0(esbuild@0.28.1)) transitivePeerDependencies: - chokidar @@ -13146,7 +13096,7 @@ snapshots: reflect-metadata: 0.2.2 semver: 7.6.0 tslib: 2.6.2 - yargs: 18.1.0 + yargs: 18.2.0 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -13227,45 +13177,11 @@ snapshots: '@types/cordova': 11.0.3 rxjs: 7.8.2 - '@awesome-cordova-plugins/core@6.16.0(rxjs@7.8.2)': - dependencies: - '@types/cordova': 11.0.3 - rxjs: 7.8.2 - optional: true - '@awesome-cordova-plugins/core@8.1.0(rxjs@7.8.2)': dependencies: '@types/cordova': 11.0.3 rxjs: 7.8.2 - '@awesome-cordova-plugins/http@6.16.0(@awesome-cordova-plugins/core@6.16.0(rxjs@7.8.2))(rxjs@7.8.2)': - dependencies: - '@awesome-cordova-plugins/core': 6.16.0(rxjs@7.8.2) - '@types/cordova': 11.0.3 - rxjs: 7.8.2 - optional: true - - '@awesome-cordova-plugins/in-app-browser@6.16.0(@awesome-cordova-plugins/core@6.16.0(rxjs@7.8.2))(rxjs@7.8.2)': - dependencies: - '@awesome-cordova-plugins/core': 6.16.0(rxjs@7.8.2) - '@types/cordova': 11.0.3 - rxjs: 7.8.2 - optional: true - - '@awesome-cordova-plugins/safari-view-controller@6.16.0(@awesome-cordova-plugins/core@6.16.0(rxjs@7.8.2))(rxjs@7.8.2)': - dependencies: - '@awesome-cordova-plugins/core': 6.16.0(rxjs@7.8.2) - '@types/cordova': 11.0.3 - rxjs: 7.8.2 - optional: true - - '@awesome-cordova-plugins/secure-storage@6.16.0(@awesome-cordova-plugins/core@6.16.0(rxjs@7.8.2))(rxjs@7.8.2)': - dependencies: - '@awesome-cordova-plugins/core': 6.16.0(rxjs@7.8.2) - '@types/cordova': 11.0.3 - rxjs: 7.8.2 - optional: true - '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -13335,7 +13251,7 @@ snapshots: '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/template': 7.29.7 '@babel/traverse': 7.29.8 '@babel/types': 7.29.8 @@ -13366,7 +13282,7 @@ snapshots: '@babel/generator@7.29.8': dependencies: - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/types': 7.29.8 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 @@ -13602,7 +13518,7 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/parser@7.29.8': + '@babel/parser@7.29.9': dependencies: '@babel/types': 7.29.8 @@ -14593,7 +14509,7 @@ snapshots: '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/types': 7.29.8 '@babel/traverse@7.29.0': @@ -14613,7 +14529,7 @@ snapshots: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.8 '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/template': 7.29.7 '@babel/types': 7.29.8 debug: 4.4.3(supports-color@8.1.1) @@ -14690,11 +14606,6 @@ snapshots: - supports-color - typescript - '@capacitor/browser@6.0.6(@capacitor/core@6.2.1)': - dependencies: - '@capacitor/core': 6.2.1 - optional: true - '@capacitor/browser@8.0.4(@capacitor/core@8.2.0)': dependencies: '@capacitor/core': 8.2.0 @@ -14706,7 +14617,7 @@ snapshots: '@ionic/utils-subprocess': 2.1.14 '@ionic/utils-terminal': 2.3.5 commander: 9.5.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.3.4 env-paths: 2.2.1 kleur: 4.1.5 native-run: 2.0.3 @@ -14747,11 +14658,6 @@ snapshots: dependencies: '@capacitor/core': 8.2.0 - '@capacitor/core@6.2.1': - dependencies: - tslib: 2.6.2 - optional: true - '@capacitor/core@8.2.0': dependencies: tslib: 2.6.2 @@ -14794,11 +14700,6 @@ snapshots: dependencies: '@capacitor/core': 8.2.0 - '@capacitor/preferences@6.0.4(@capacitor/core@6.2.1)': - dependencies: - '@capacitor/core': 6.2.1 - optional: true - '@capacitor/preferences@8.0.1(@capacitor/core@8.2.0)': dependencies: '@capacitor/core': 8.2.0 @@ -14819,7 +14720,7 @@ snapshots: '@changesets/apply-release-plan@6.1.4': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.27.6 '@changesets/config': 2.3.1 '@changesets/get-version-range-type': 0.3.2 '@changesets/git': 2.0.0 @@ -14831,16 +14732,16 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.6.0 + semver: 7.7.4 '@changesets/assemble-release-plan@5.2.4': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.27.6 '@changesets/errors': 0.1.4 '@changesets/get-dependents-graph': 1.3.6 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 - semver: 7.6.0 + semver: 7.7.4 '@changesets/changelog-git@0.1.14': dependencies: @@ -14902,11 +14803,11 @@ snapshots: '@manypkg/get-packages': 1.1.3 chalk: 2.4.2 fs-extra: 7.0.1 - semver: 7.6.0 + semver: 7.7.4 '@changesets/get-release-plan@3.0.17': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.27.6 '@changesets/assemble-release-plan': 5.2.4 '@changesets/config': 2.3.1 '@changesets/pre': 1.0.14 @@ -14918,7 +14819,7 @@ snapshots: '@changesets/git@2.0.0': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.27.6 '@changesets/errors': 0.1.4 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 @@ -14937,7 +14838,7 @@ snapshots: '@changesets/pre@1.0.14': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.27.6 '@changesets/errors': 0.1.4 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 @@ -14945,7 +14846,7 @@ snapshots: '@changesets/read@0.5.9': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.27.6 '@changesets/git': 2.0.0 '@changesets/logger': 0.0.5 '@changesets/parse': 0.3.16 @@ -14960,7 +14861,7 @@ snapshots: '@changesets/write@0.2.3': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.27.6 '@changesets/types': 5.2.1 fs-extra: 7.0.1 human-id: 1.0.2 @@ -14983,7 +14884,7 @@ snapshots: chokidar: 5.0.0 colors: 1.4.0 commander: 14.0.3 - cosmiconfig: 9.0.2(typescript@5.9.3) + cosmiconfig: 9.0.1(typescript@5.9.3) decache: 4.6.2 es6-shim: 0.35.8 fancy-log: 2.0.0 @@ -15058,33 +14959,33 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@csstools/css-calc@3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': dependencies: - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 - '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + '@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2)': dependencies: - '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-tokenizer': 4.0.2 - '@csstools/css-syntax-patches-for-csstree@1.1.9(css-tree@3.2.1)': + '@csstools/css-syntax-patches-for-csstree@1.1.15(css-tree@3.2.1)': optionalDependencies: css-tree: 3.2.1 - '@csstools/css-tokenizer@4.0.0': {} + '@csstools/css-tokenizer@4.0.2': {} - '@csstools/media-query-list-parser@5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@csstools/media-query-list-parser@5.0.2(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': dependencies: - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 - '@csstools/selector-resolve-nested@4.0.1(postcss-selector-parser@7.1.5)': + '@csstools/selector-resolve-nested@4.0.2(postcss-selector-parser@7.1.6)': dependencies: - postcss-selector-parser: 7.1.5 + postcss-selector-parser: 7.1.6 - '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.5)': + '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.6)': dependencies: - postcss-selector-parser: 7.1.5 + postcss-selector-parser: 7.1.6 '@cypress/request@3.0.10': dependencies: @@ -15342,9 +15243,9 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 - '@hono/node-server@2.1.1(hono@4.13.8)': + '@hono/node-server@2.1.3(hono@4.13.12)': dependencies: - hono: 4.13.8 + hono: 4.13.12 '@humanwhocodes/config-array@0.11.14': dependencies: @@ -15419,7 +15320,7 @@ snapshots: '@inquirer/external-editor@1.0.3(@types/node@22.15.31)': dependencies: - chardet: 2.1.1 + chardet: 2.2.0 iconv-lite: 0.7.2 optionalDependencies: '@types/node': 22.15.31 @@ -15517,7 +15418,7 @@ snapshots: '@angular/forms': 20.3.31(@angular/common@20.3.31(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2))(@angular/platform-browser@20.3.31(@angular/animations@20.3.31(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2)))(@angular/common@20.3.31(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2)))(rxjs@7.8.2) '@angular/router': 20.3.31(@angular/common@20.3.31(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2))(@angular/platform-browser@20.3.31(@angular/animations@20.3.31(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2)))(@angular/common@20.3.31(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2)))(rxjs@7.8.2) '@ionic/core': 8.8.19 - ionicons: 8.1.0 + ionicons: 8.0.13 jsonc-parser: 3.3.1 rxjs: 7.8.2 tslib: 2.6.2 @@ -15594,7 +15495,7 @@ snapshots: '@ionic/core@8.8.19': dependencies: '@stencil/core': 4.43.5 - ionicons: 8.1.0 + ionicons: 8.0.13 tslib: 2.6.2 '@ionic/storage-angular@4.0.0(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2)': @@ -15610,7 +15511,7 @@ snapshots: '@ionic/utils-array@2.1.6': dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.3.4 tslib: 2.6.2 transitivePeerDependencies: - supports-color @@ -15618,7 +15519,7 @@ snapshots: '@ionic/utils-fs@3.1.7': dependencies: '@types/fs-extra': 8.1.5 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.3.4 fs-extra: 9.1.0 tslib: 2.6.2 transitivePeerDependencies: @@ -15961,14 +15862,14 @@ snapshots: '@manypkg/find-root@1.1.0': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.27.6 '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 '@manypkg/get-packages@1.1.3': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.27.6 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -16022,7 +15923,7 @@ snapshots: '@modelcontextprotocol/sdk@1.30.0(zod@4.1.13)': dependencies: - '@hono/node-server': 2.1.1(hono@4.13.8) + '@hono/node-server': 2.1.3(hono@4.13.12) ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) content-type: 1.0.5 @@ -16032,7 +15933,7 @@ snapshots: eventsource-parser: 3.1.1 express: 5.2.1 express-rate-limit: 8.7.0(express@5.2.1) - hono: 4.13.8 + hono: 4.13.12 jose: 6.2.12 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -16151,8 +16052,6 @@ snapshots: '@ngx-translate/core': 15.0.0(@angular/common@20.3.31(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@20.3.31(@angular/compiler@20.3.31)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2) rxjs: 7.8.2 - '@noble/hashes@1.4.0': {} - '@noble/hashes@1.8.0': {} '@nodelib/fs.scandir@2.1.5': @@ -16189,7 +16088,7 @@ snapshots: '@npmcli/fs@3.1.1': dependencies: - semver: 7.6.0 + semver: 7.6.3 '@npmcli/fs@5.0.0': dependencies: @@ -16254,12 +16153,12 @@ snapshots: '@oozcitak/util@8.3.8': {} - '@openid/appauth@1.3.2': + '@openid/appauth@1.4.0': dependencies: '@types/base64-js': 1.5.0 - '@types/jquery': 3.5.34 + '@types/jquery': 4.0.1 base64-js: 1.5.1 - follow-redirects: 1.15.11(debug@4.4.3) + follow-redirects: 1.16.0 form-data: 4.0.6 opener: 1.5.2 transitivePeerDependencies: @@ -16278,139 +16177,135 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 - '@parcel/watcher-android-arm64@2.5.6': + '@parcel/watcher-android-arm64@2.6.0': optional: true - '@parcel/watcher-darwin-arm64@2.5.6': + '@parcel/watcher-darwin-arm64@2.6.0': optional: true - '@parcel/watcher-darwin-x64@2.5.6': + '@parcel/watcher-darwin-x64@2.6.0': optional: true - '@parcel/watcher-freebsd-x64@2.5.6': + '@parcel/watcher-freebsd-x64@2.6.0': optional: true - '@parcel/watcher-linux-arm-glibc@2.5.6': + '@parcel/watcher-linux-arm-glibc@2.6.0': optional: true - '@parcel/watcher-linux-arm-musl@2.5.6': + '@parcel/watcher-linux-arm-musl@2.6.0': optional: true - '@parcel/watcher-linux-arm64-glibc@2.5.6': + '@parcel/watcher-linux-arm64-glibc@2.6.0': optional: true - '@parcel/watcher-linux-arm64-musl@2.5.6': + '@parcel/watcher-linux-arm64-musl@2.6.0': optional: true - '@parcel/watcher-linux-x64-glibc@2.5.6': + '@parcel/watcher-linux-x64-glibc@2.6.0': optional: true - '@parcel/watcher-linux-x64-musl@2.5.6': + '@parcel/watcher-linux-x64-musl@2.6.0': optional: true - '@parcel/watcher-win32-arm64@2.5.6': + '@parcel/watcher-win32-arm64@2.6.0': optional: true - '@parcel/watcher-win32-ia32@2.5.6': + '@parcel/watcher-win32-x64@2.6.0': optional: true - '@parcel/watcher-win32-x64@2.5.6': - optional: true - - '@parcel/watcher@2.5.6': + '@parcel/watcher@2.6.0': dependencies: detect-libc: 2.1.2 is-glob: 4.0.3 node-addon-api: 7.1.1 picomatch: 4.0.4 optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.6 - '@parcel/watcher-darwin-arm64': 2.5.6 - '@parcel/watcher-darwin-x64': 2.5.6 - '@parcel/watcher-freebsd-x64': 2.5.6 - '@parcel/watcher-linux-arm-glibc': 2.5.6 - '@parcel/watcher-linux-arm-musl': 2.5.6 - '@parcel/watcher-linux-arm64-glibc': 2.5.6 - '@parcel/watcher-linux-arm64-musl': 2.5.6 - '@parcel/watcher-linux-x64-glibc': 2.5.6 - '@parcel/watcher-linux-x64-musl': 2.5.6 - '@parcel/watcher-win32-arm64': 2.5.6 - '@parcel/watcher-win32-ia32': 2.5.6 - '@parcel/watcher-win32-x64': 2.5.6 + '@parcel/watcher-android-arm64': 2.6.0 + '@parcel/watcher-darwin-arm64': 2.6.0 + '@parcel/watcher-darwin-x64': 2.6.0 + '@parcel/watcher-freebsd-x64': 2.6.0 + '@parcel/watcher-linux-arm-glibc': 2.6.0 + '@parcel/watcher-linux-arm-musl': 2.6.0 + '@parcel/watcher-linux-arm64-glibc': 2.6.0 + '@parcel/watcher-linux-arm64-musl': 2.6.0 + '@parcel/watcher-linux-x64-glibc': 2.6.0 + '@parcel/watcher-linux-x64-musl': 2.6.0 + '@parcel/watcher-win32-arm64': 2.6.0 + '@parcel/watcher-win32-x64': 2.6.0 optional: true - '@peculiar/asn1-cms@2.9.4': + '@peculiar/asn1-cms@2.10.0': dependencies: - '@peculiar/asn1-schema': 2.9.4 - '@peculiar/asn1-x509': 2.9.4 - '@peculiar/asn1-x509-attr': 2.9.4 + '@peculiar/asn1-schema': 2.10.0 + '@peculiar/asn1-x509': 2.10.0 + '@peculiar/asn1-x509-attr': 2.10.0 asn1js: 3.0.10 tslib: 2.8.1 - '@peculiar/asn1-csr@2.9.4': + '@peculiar/asn1-csr@2.10.0': dependencies: - '@peculiar/asn1-schema': 2.9.4 - '@peculiar/asn1-x509': 2.9.4 + '@peculiar/asn1-schema': 2.10.0 + '@peculiar/asn1-x509': 2.10.0 asn1js: 3.0.10 tslib: 2.8.1 - '@peculiar/asn1-ecc@2.9.4': + '@peculiar/asn1-ecc@2.10.0': dependencies: - '@peculiar/asn1-schema': 2.9.4 - '@peculiar/asn1-x509': 2.9.4 + '@peculiar/asn1-schema': 2.10.0 + '@peculiar/asn1-x509': 2.10.0 asn1js: 3.0.10 tslib: 2.8.1 - '@peculiar/asn1-pfx@2.9.4': + '@peculiar/asn1-pfx@2.10.0': dependencies: - '@peculiar/asn1-cms': 2.9.4 - '@peculiar/asn1-pkcs8': 2.9.4 - '@peculiar/asn1-rsa': 2.9.4 - '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-cms': 2.10.0 + '@peculiar/asn1-pkcs8': 2.10.0 + '@peculiar/asn1-rsa': 2.10.0 + '@peculiar/asn1-schema': 2.10.0 asn1js: 3.0.10 tslib: 2.8.1 - '@peculiar/asn1-pkcs8@2.9.4': + '@peculiar/asn1-pkcs8@2.10.0': dependencies: - '@peculiar/asn1-schema': 2.9.4 - '@peculiar/asn1-x509': 2.9.4 + '@peculiar/asn1-schema': 2.10.0 + '@peculiar/asn1-x509': 2.10.0 asn1js: 3.0.10 tslib: 2.8.1 - '@peculiar/asn1-pkcs9@2.9.4': + '@peculiar/asn1-pkcs9@2.10.0': dependencies: - '@peculiar/asn1-cms': 2.9.4 - '@peculiar/asn1-pfx': 2.9.4 - '@peculiar/asn1-pkcs8': 2.9.4 - '@peculiar/asn1-schema': 2.9.4 - '@peculiar/asn1-x509': 2.9.4 - '@peculiar/asn1-x509-attr': 2.9.4 + '@peculiar/asn1-cms': 2.10.0 + '@peculiar/asn1-pfx': 2.10.0 + '@peculiar/asn1-pkcs8': 2.10.0 + '@peculiar/asn1-schema': 2.10.0 + '@peculiar/asn1-x509': 2.10.0 + '@peculiar/asn1-x509-attr': 2.10.0 asn1js: 3.0.10 tslib: 2.8.1 - '@peculiar/asn1-rsa@2.9.4': + '@peculiar/asn1-rsa@2.10.0': dependencies: - '@peculiar/asn1-schema': 2.9.4 - '@peculiar/asn1-x509': 2.9.4 + '@peculiar/asn1-schema': 2.10.0 + '@peculiar/asn1-x509': 2.10.0 asn1js: 3.0.10 tslib: 2.8.1 - '@peculiar/asn1-schema@2.9.4': + '@peculiar/asn1-schema@2.10.0': dependencies: '@peculiar/utils': 2.0.3 asn1js: 3.0.10 tslib: 2.8.1 - '@peculiar/asn1-x509-attr@2.9.4': + '@peculiar/asn1-x509-attr@2.10.0': dependencies: - '@peculiar/asn1-schema': 2.9.4 - '@peculiar/asn1-x509': 2.9.4 + '@peculiar/asn1-schema': 2.10.0 + '@peculiar/asn1-x509': 2.10.0 asn1js: 3.0.10 tslib: 2.8.1 - '@peculiar/asn1-x509@2.9.4': + '@peculiar/asn1-x509@2.10.0': dependencies: - '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-schema': 2.10.0 '@peculiar/utils': 2.0.3 asn1js: 3.0.10 tslib: 2.8.1 @@ -16421,13 +16316,13 @@ snapshots: '@peculiar/x509@1.14.3': dependencies: - '@peculiar/asn1-cms': 2.9.4 - '@peculiar/asn1-csr': 2.9.4 - '@peculiar/asn1-ecc': 2.9.4 - '@peculiar/asn1-pkcs9': 2.9.4 - '@peculiar/asn1-rsa': 2.9.4 - '@peculiar/asn1-schema': 2.9.4 - '@peculiar/asn1-x509': 2.9.4 + '@peculiar/asn1-cms': 2.10.0 + '@peculiar/asn1-csr': 2.10.0 + '@peculiar/asn1-ecc': 2.10.0 + '@peculiar/asn1-pkcs9': 2.10.0 + '@peculiar/asn1-rsa': 2.10.0 + '@peculiar/asn1-schema': 2.10.0 + '@peculiar/asn1-x509': 2.10.0 pvtsutils: 1.3.6 reflect-metadata: 0.2.2 tslib: 2.8.1 @@ -16690,6 +16585,17 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@stencil/core@4.43.2': + optionalDependencies: + '@rollup/rollup-darwin-arm64': 4.44.0 + '@rollup/rollup-darwin-x64': 4.44.0 + '@rollup/rollup-linux-arm64-gnu': 4.44.0 + '@rollup/rollup-linux-arm64-musl': 4.44.0 + '@rollup/rollup-linux-x64-gnu': 4.44.0 + '@rollup/rollup-linux-x64-musl': 4.44.0 + '@rollup/rollup-win32-arm64-msvc': 4.44.0 + '@rollup/rollup-win32-x64-msvc': 4.44.0 + '@stencil/core@4.43.5': optionalDependencies: '@rollup/rollup-darwin-arm64': 4.44.0 @@ -16709,7 +16615,7 @@ snapshots: eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 - picomatch: 4.0.4 + picomatch: 4.0.3 '@swc/helpers@0.5.23': dependencies: @@ -16959,9 +16865,7 @@ snapshots: dependencies: '@types/jasmine': 5.1.4 - '@types/jquery@3.5.34': - dependencies: - '@types/sizzle': 2.3.10 + '@types/jquery@4.0.1': {} '@types/json-patch@0.0.30': {} @@ -17130,7 +17034,7 @@ snapshots: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 '@types/node': 22.15.31 - form-data: 4.0.4 + form-data: 4.0.6 '@types/supertest@2.0.12': dependencies: @@ -17452,7 +17356,7 @@ snapshots: accepts@2.0.0: dependencies: mime-types: 3.0.2 - negotiator: 1.0.0 + negotiator: 1.1.0 acorn-import-phases@1.0.4(acorn@8.16.0): dependencies: @@ -17820,7 +17724,7 @@ snapshots: domhandler: 5.0.3 htmlparser2: 10.1.0 picocolors: 1.1.1 - postcss: 8.5.26 + postcss: 8.5.23 postcss-media-query-parser: 0.2.3 better-ajv-errors@1.2.0(ajv@8.12.0): @@ -17875,23 +17779,6 @@ snapshots: transitivePeerDependencies: - supports-color - body-parser@1.20.8: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.1 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.16.0 - raw-body: 2.5.3 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - body-parser@2.3.0: dependencies: bytes: 3.1.2 @@ -18034,7 +17921,7 @@ snapshots: minipass-collect: 2.0.1 minipass-flush: 1.0.5 minipass-pipeline: 1.2.4 - p-map: 7.0.4 + p-map: 7.0.8 ssri: 13.0.1 cacheable-lookup@7.0.0: {} @@ -18092,11 +17979,6 @@ snapshots: caniuse-lite@1.0.30001778: {} - capacitor-secure-storage-plugin@0.10.0(@capacitor/core@6.2.1): - dependencies: - '@capacitor/core': 6.2.1 - optional: true - capacitor-secure-storage-plugin@0.13.0(@capacitor/core@8.2.0): dependencies: '@capacitor/core': 8.2.0 @@ -18142,7 +18024,7 @@ snapshots: chardet@0.7.0: {} - chardet@2.1.1: {} + chardet@2.2.0: {} charenc@0.0.2: {} @@ -18450,7 +18332,7 @@ snapshots: conventional-changelog-conventionalcommits@4.6.3: dependencies: compare-func: 2.0.0 - lodash: 4.17.23 + lodash: 4.17.21 q: 1.5.1 conventional-changelog-core@4.2.4: @@ -18463,7 +18345,7 @@ snapshots: git-raw-commits: 2.0.11 git-remote-origin-url: 2.0.0 git-semver-tags: 4.1.1 - lodash: 4.17.23 + lodash: 4.17.21 normalize-package-data: 3.0.3 q: 1.5.1 read-pkg: 3.0.0 @@ -18499,7 +18381,7 @@ snapshots: dateformat: 3.0.3 handlebars: 4.7.9 json-stringify-safe: 5.0.1 - lodash: 4.17.23 + lodash: 4.17.21 meow: 8.1.2 semver: 6.3.1 split: 1.0.1 @@ -18528,7 +18410,7 @@ snapshots: dependencies: JSONStream: 1.3.5 is-text-path: 1.0.1 - lodash: 4.17.23 + lodash: 4.17.21 meow: 8.1.2 split2: 3.2.2 through2: 4.0.2 @@ -18539,6 +18421,8 @@ snapshots: cookie-signature@1.0.6: {} + cookie-signature@1.0.7: {} + cookie-signature@1.2.2: {} cookie@0.7.1: {} @@ -18556,7 +18440,7 @@ snapshots: glob-parent: 6.0.2 normalize-path: 3.0.0 schema-utils: 4.3.3 - serialize-javascript: 7.1.1 + serialize-javascript: 7.1.2 tinyglobby: 0.2.14 webpack: 5.105.0(esbuild@0.28.1) @@ -18614,6 +18498,15 @@ snapshots: optionalDependencies: typescript: 5.9.3 + cosmiconfig@9.0.1(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + cosmiconfig@9.0.2(typescript@5.9.3): dependencies: env-paths: 2.2.1 @@ -18655,12 +18548,12 @@ snapshots: css-loader@7.1.2(webpack@5.105.0(esbuild@0.28.1)): dependencies: - icss-utils: 5.1.0(postcss@8.5.26) - postcss: 8.5.26 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.26) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.26) - postcss-modules-scope: 3.2.1(postcss@8.5.26) - postcss-modules-values: 4.0.0(postcss@8.5.26) + icss-utils: 5.1.0(postcss@8.5.23) + postcss: 8.5.23 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.23) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.23) + postcss-modules-scope: 3.2.1(postcss@8.5.23) + postcss-modules-values: 4.0.0(postcss@8.5.23) postcss-value-parser: 4.2.0 semver: 7.6.0 optionalDependencies: @@ -19280,7 +19173,7 @@ snapshots: duplexer@0.1.2: {} - earcut@3.2.3: {} + earcut@3.2.4: {} eastasianwidth@0.2.0: {} @@ -19466,7 +19359,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.4 + hasown: 2.0.2 es-shim-unscopables@1.1.0: dependencies: @@ -19769,7 +19662,7 @@ snapshots: dependencies: debug: 4.4.3(supports-color@8.1.1) express: 5.2.1 - ip-address: 10.7.2 + ip-address: 10.7.3 transitivePeerDependencies: - supports-color @@ -19809,34 +19702,34 @@ snapshots: transitivePeerDependencies: - supports-color - express@4.22.3: + express@4.22.1: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.8 + body-parser: 1.20.3 content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 - cookie-signature: 1.0.6 + cookie-signature: 1.0.7 debug: 2.6.9 depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.3.1 + finalhandler: 1.3.2 fresh: 0.5.2 http-errors: 2.0.1 merge-descriptors: 1.0.3 methods: 1.1.2 on-finished: 2.4.1 parseurl: 1.3.3 - path-to-regexp: 0.1.13 + path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.16.0 + qs: 6.14.2 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.19.0 - serve-static: 1.16.2 + send: 0.19.2 + serve-static: 1.16.3 setprototypeof: 1.2.0 statuses: 2.0.2 type-is: 1.6.18 @@ -19867,8 +19760,8 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.16.0 - range-parser: 1.3.0 + qs: 6.15.0 + range-parser: 1.2.1 router: 2.2.0 send: 1.2.1 serve-static: 2.2.1 @@ -20002,6 +19895,18 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + finalhandler@2.1.1: dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -20064,6 +19969,8 @@ snapshots: follow-redirects@1.15.9: {} + follow-redirects@1.16.0: {} + fontkit@2.0.4: dependencies: '@swc/helpers': 0.5.23 @@ -20095,14 +20002,6 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 - form-data@4.0.4: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - form-data@4.0.6: dependencies: asynckit: 0.4.0 @@ -20218,7 +20117,7 @@ snapshots: get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.4 + hasown: 2.0.2 math-intrinsics: 1.1.0 get-pkg-repo@4.2.1: @@ -20266,7 +20165,7 @@ snapshots: git-raw-commits@2.0.11: dependencies: dargs: 7.0.0 - lodash: 4.17.23 + lodash: 4.17.21 meow: 8.1.2 split2: 3.2.2 through2: 4.0.2 @@ -20382,7 +20281,7 @@ snapshots: is-path-inside: 4.0.0 micromatch: 4.0.8 slash: 5.1.0 - unicorn-magic: 0.4.0 + unicorn-magic: 0.4.1 globjoin@0.1.4: {} @@ -20480,7 +20379,7 @@ snapshots: highlight.js@10.7.3: {} - hono@4.13.8: {} + hono@4.13.12: {} hookified@1.15.1: {} @@ -20684,9 +20583,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.26): + icss-utils@5.1.0(postcss@8.5.23): dependencies: - postcss: 8.5.26 + postcss: 8.5.23 ieee754@1.2.1: {} @@ -20700,7 +20599,7 @@ snapshots: immediate@3.0.6: {} - immutable@5.1.5: {} + immutable@5.1.9: {} import-fresh@3.3.1: dependencies: @@ -20778,34 +20677,13 @@ snapshots: interpret@1.4.0: {} - ionic-appauth@2.1.0(rxjs@7.8.2): + ionicons@8.0.13: dependencies: - '@openid/appauth': 1.3.2 - '@types/chai-as-promised': 7.1.8 - rxjs: 7.8.2 - tslib: 2.6.2 - uuid: 9.0.1 - optionalDependencies: - '@awesome-cordova-plugins/core': 6.16.0(rxjs@7.8.2) - '@awesome-cordova-plugins/http': 6.16.0(@awesome-cordova-plugins/core@6.16.0(rxjs@7.8.2))(rxjs@7.8.2) - '@awesome-cordova-plugins/in-app-browser': 6.16.0(@awesome-cordova-plugins/core@6.16.0(rxjs@7.8.2))(rxjs@7.8.2) - '@awesome-cordova-plugins/safari-view-controller': 6.16.0(@awesome-cordova-plugins/core@6.16.0(rxjs@7.8.2))(rxjs@7.8.2) - '@awesome-cordova-plugins/secure-storage': 6.16.0(@awesome-cordova-plugins/core@6.16.0(rxjs@7.8.2))(rxjs@7.8.2) - '@capacitor/browser': 6.0.6(@capacitor/core@6.2.1) - '@capacitor/core': 6.2.1 - '@capacitor/preferences': 6.0.4(@capacitor/core@6.2.1) - '@ionic/storage': 4.0.0 - capacitor-secure-storage-plugin: 0.10.0(@capacitor/core@6.2.1) - transitivePeerDependencies: - - debug - - ionicons@8.1.0: - dependencies: - '@stencil/core': 4.43.5 + '@stencil/core': 4.43.2 ip-address@10.1.0: {} - ip-address@10.7.2: {} + ip-address@10.7.3: {} ip-regex@4.3.0: {} @@ -20957,6 +20835,8 @@ snapshots: is-plain-object@5.0.0: {} + is-plain-object@5.1.0: {} + is-promise@4.0.0: {} is-regex@1.2.1: @@ -21337,7 +21217,7 @@ snapshots: launch-editor@2.14.1: dependencies: picocolors: 1.1.1 - shell-quote: 1.10.0 + shell-quote: 1.12.0 layout-base@1.0.2: optional: true @@ -21657,7 +21537,7 @@ snapshots: cacache: 18.0.4 http-cache-semantics: 4.2.0 is-lambda: 1.0.1 - minipass: 7.1.3 + minipass: 7.1.2 minipass-fetch: 3.0.5 minipass-flush: 1.0.5 minipass-pipeline: 1.2.4 @@ -21679,7 +21559,7 @@ snapshots: minipass-fetch: 5.0.2 minipass-flush: 1.0.5 minipass-pipeline: 1.2.4 - negotiator: 1.0.0 + negotiator: 1.1.0 proc-log: 6.1.0 ssri: 13.0.1 transitivePeerDependencies: @@ -21702,7 +21582,7 @@ snapshots: '@maplibre/mlt': 1.3.0 '@maplibre/vt-pbf': 4.3.2 '@types/geojson': 7946.0.16 - earcut: 3.2.3 + earcut: 3.2.4 gl-matrix: 3.4.4 kdbush: 4.1.0 murmurhash-js: 1.0.0 @@ -22144,6 +22024,8 @@ snapshots: minipass@4.2.8: {} + minipass@7.1.2: {} + minipass@7.1.3: {} minizlib@2.1.2: @@ -22234,6 +22116,8 @@ snapshots: mri@1.2.0: optional: true + mrmime@2.0.0: {} + mrmime@2.0.1: {} ms@2.0.0: {} @@ -22282,7 +22166,7 @@ snapshots: nan@2.22.2: {} - nanoid@3.3.18: {} + nanoid@3.3.19: {} napi-build-utils@2.0.0: {} @@ -22323,7 +22207,9 @@ snapshots: negotiator@0.6.4: {} - negotiator@1.0.0: {} + negotiator@1.1.0: + dependencies: + content-type: 2.1.0 neo-async@2.6.2: {} @@ -22441,7 +22327,7 @@ snapshots: semver: 7.6.0 tar: 7.5.22 tinyglobby: 0.2.14 - undici: 6.28.1 + undici: 6.29.0 which: 6.0.1 node-html-parser@5.4.2: @@ -22472,7 +22358,7 @@ snapshots: normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.11 + resolve: 1.22.10 semver: 5.7.2 validate-npm-package-license: 3.0.4 @@ -22480,7 +22366,7 @@ snapshots: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.16.1 - semver: 7.6.0 + semver: 7.7.4 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -22505,7 +22391,7 @@ snapshots: dependencies: hosted-git-info: 7.0.2 proc-log: 3.0.0 - semver: 7.6.0 + semver: 7.5.4 validate-npm-package-name: 5.0.1 npm-package-arg@13.0.0: @@ -22671,7 +22557,7 @@ snapshots: is-unicode-supported: 2.1.0 log-symbols: 7.0.1 stdin-discarder: 0.2.2 - string-width: 8.2.2 + string-width: 8.3.0 strip-ansi: 7.2.0 ordered-binary@1.6.1: @@ -22737,7 +22623,7 @@ snapshots: dependencies: aggregate-error: 3.1.0 - p-map@7.0.4: {} + p-map@7.0.8: {} p-retry@6.2.1: dependencies: @@ -22864,8 +22750,6 @@ snapshots: path-to-regexp@0.1.12: {} - path-to-regexp@0.1.13: {} - path-to-regexp@6.3.0: {} path-to-regexp@8.4.2: {} @@ -22926,9 +22810,9 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 - pkijs@3.4.0: + pkijs@3.4.1: dependencies: - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.8.0 asn1js: 3.0.10 bytestreamjs: 2.0.1 pvtsutils: 1.3.6 @@ -22964,16 +22848,16 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.26): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.28): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 - postcss: 8.5.26 + postcss: 8.5.28 postcss-loader@8.1.1(postcss@8.5.23)(typescript@5.9.3)(webpack@5.105.0(esbuild@0.28.1)): dependencies: - cosmiconfig: 9.0.2(typescript@5.9.3) + cosmiconfig: 9.0.1(typescript@5.9.3) jiti: 1.21.7 postcss: 8.5.23 semver: 7.6.0 @@ -22984,62 +22868,62 @@ snapshots: postcss-media-query-parser@0.2.3: {} - postcss-modules-extract-imports@3.1.0(postcss@8.5.26): + postcss-modules-extract-imports@3.1.0(postcss@8.5.23): dependencies: - postcss: 8.5.26 + postcss: 8.5.23 - postcss-modules-local-by-default@4.2.0(postcss@8.5.26): + postcss-modules-local-by-default@4.2.0(postcss@8.5.23): dependencies: - icss-utils: 5.1.0(postcss@8.5.26) - postcss: 8.5.26 - postcss-selector-parser: 7.1.5 + icss-utils: 5.1.0(postcss@8.5.23) + postcss: 8.5.23 + postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.5.26): + postcss-modules-scope@3.2.1(postcss@8.5.23): dependencies: - postcss: 8.5.26 - postcss-selector-parser: 7.1.5 + postcss: 8.5.23 + postcss-selector-parser: 7.1.1 - postcss-modules-values@4.0.0(postcss@8.5.26): + postcss-modules-values@4.0.0(postcss@8.5.23): dependencies: - icss-utils: 5.1.0(postcss@8.5.26) - postcss: 8.5.26 + icss-utils: 5.1.0(postcss@8.5.23) + postcss: 8.5.23 postcss-resolve-nested-selector@0.1.6: {} - postcss-safe-parser@7.0.1(postcss@8.5.26): + postcss-safe-parser@7.0.1(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 - postcss-scss@4.0.9(postcss@8.5.26): + postcss-scss@4.0.9(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@7.1.5: + postcss-selector-parser@7.1.6: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-sorting@8.0.2(postcss@8.5.26): + postcss-sorting@8.0.2(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-value-parser@4.2.0: {} postcss@8.5.23: dependencies: - nanoid: 3.3.18 + nanoid: 3.3.19 picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.26: + postcss@8.5.28: dependencies: - nanoid: 3.3.18 + nanoid: 3.3.19 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -23228,8 +23112,6 @@ snapshots: range-parser@1.2.1: {} - range-parser@1.3.0: {} - raw-body@2.5.2: dependencies: bytes: 3.1.2 @@ -23237,13 +23119,6 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - raw-body@2.5.3: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - raw-body@3.0.2: dependencies: bytes: 3.1.2 @@ -23357,7 +23232,7 @@ snapshots: rechoir@0.6.2: dependencies: - resolve: 1.22.11 + resolve: 1.22.10 redent@3.0.0: dependencies: @@ -23473,7 +23348,7 @@ snapshots: adjust-sourcemap-loader: 4.0.0 convert-source-map: 1.9.0 loader-utils: 2.0.4 - postcss: 8.5.26 + postcss: 8.5.23 source-map: 0.6.1 resolve@1.22.10: @@ -23625,10 +23500,6 @@ snapshots: dependencies: tslib: 1.14.1 - rxjs@7.8.1: - dependencies: - tslib: 2.6.2 - rxjs@7.8.2: dependencies: tslib: 2.6.2 @@ -23675,10 +23546,10 @@ snapshots: sass@1.90.0: dependencies: chokidar: 4.0.3 - immutable: 5.1.5 + immutable: 5.1.9 source-map-js: 1.2.1 optionalDependencies: - '@parcel/watcher': 2.5.6 + '@parcel/watcher': 2.6.0 sax@1.1.4: {} @@ -23703,7 +23574,7 @@ snapshots: selfsigned@5.5.0: dependencies: '@peculiar/x509': 1.14.3 - pkijs: 3.4.0 + pkijs: 3.4.1 semver@5.7.2: {} @@ -23717,6 +23588,8 @@ snapshots: dependencies: lru-cache: 6.0.0 + semver@7.6.3: {} + semver@7.7.2: {} semver@7.7.3: {} @@ -23741,6 +23614,24 @@ snapshots: transitivePeerDependencies: - supports-color + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + send@1.2.1: dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -23752,7 +23643,7 @@ snapshots: mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 - range-parser: 1.3.0 + range-parser: 1.2.1 statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -23761,7 +23652,7 @@ snapshots: dependencies: randombytes: 2.1.0 - serialize-javascript@7.1.1: {} + serialize-javascript@7.1.2: {} serve-index@1.9.2: dependencies: @@ -23784,6 +23675,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -23861,7 +23761,7 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.10.0: {} + shell-quote@1.12.0: {} shelljs@0.8.5: dependencies: @@ -23971,7 +23871,7 @@ snapshots: sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 - mrmime: 2.0.1 + mrmime: 2.0.0 totalist: 3.0.1 sisteransi@1.0.5: {} @@ -24270,7 +24170,7 @@ snapshots: get-east-asian-width: 1.5.0 strip-ansi: 7.2.0 - string-width@8.2.2: + string-width@8.3.0: dependencies: get-east-asian-width: 1.5.0 strip-ansi: 7.2.0 @@ -24347,26 +24247,26 @@ snapshots: dependencies: stylelint: 17.14.1(typescript@5.9.3) - stylelint-config-recommended-scss@17.0.1(postcss@8.5.26)(stylelint@17.14.1(typescript@5.9.3)): + stylelint-config-recommended-scss@17.0.1(postcss@8.5.28)(stylelint@17.14.1(typescript@5.9.3)): dependencies: - postcss-scss: 4.0.9(postcss@8.5.26) + postcss-scss: 4.0.9(postcss@8.5.28) stylelint: 17.14.1(typescript@5.9.3) stylelint-config-recommended: 18.0.0(stylelint@17.14.1(typescript@5.9.3)) - stylelint-scss: 7.2.0(stylelint@17.14.1(typescript@5.9.3)) + stylelint-scss: 7.3.0(stylelint@17.14.1(typescript@5.9.3)) optionalDependencies: - postcss: 8.5.26 + postcss: 8.5.28 stylelint-config-recommended@18.0.0(stylelint@17.14.1(typescript@5.9.3)): dependencies: stylelint: 17.14.1(typescript@5.9.3) - stylelint-config-standard-scss@17.0.0(postcss@8.5.26)(stylelint@17.14.1(typescript@5.9.3)): + stylelint-config-standard-scss@17.0.0(postcss@8.5.28)(stylelint@17.14.1(typescript@5.9.3)): dependencies: stylelint: 17.14.1(typescript@5.9.3) - stylelint-config-recommended-scss: 17.0.1(postcss@8.5.26)(stylelint@17.14.1(typescript@5.9.3)) + stylelint-config-recommended-scss: 17.0.1(postcss@8.5.28)(stylelint@17.14.1(typescript@5.9.3)) stylelint-config-standard: 40.0.0(stylelint@17.14.1(typescript@5.9.3)) optionalDependencies: - postcss: 8.5.26 + postcss: 8.5.28 stylelint-config-standard@40.0.0(stylelint@17.14.1(typescript@5.9.3)): dependencies: @@ -24375,34 +24275,34 @@ snapshots: stylelint-order@6.0.4(stylelint@17.14.1(typescript@5.9.3)): dependencies: - postcss: 8.5.26 - postcss-sorting: 8.0.2(postcss@8.5.26) + postcss: 8.5.28 + postcss-sorting: 8.0.2(postcss@8.5.28) stylelint: 17.14.1(typescript@5.9.3) - stylelint-scss@7.2.0(stylelint@17.14.1(typescript@5.9.3)): + stylelint-scss@7.3.0(stylelint@17.14.1(typescript@5.9.3)): dependencies: - '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-syntax-patches-for-csstree': 1.1.9(css-tree@3.2.1) - '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-syntax-patches-for-csstree': 1.1.15(css-tree@3.2.1) + '@csstools/css-tokenizer': 4.0.2 css-tree: 3.2.1 - is-plain-object: 5.0.0 + is-plain-object: 5.1.0 known-css-properties: 0.37.0 postcss-media-query-parser: 0.2.3 postcss-resolve-nested-selector: 0.1.6 - postcss-selector-parser: 7.1.1 + postcss-selector-parser: 7.1.6 postcss-value-parser: 4.2.0 stylelint: 17.14.1(typescript@5.9.3) stylelint@17.14.1(typescript@5.9.3): dependencies: - '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-syntax-patches-for-csstree': 1.1.9(css-tree@3.2.1) - '@csstools/css-tokenizer': 4.0.0 - '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/selector-resolve-nested': 4.0.1(postcss-selector-parser@7.1.5) - '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.5) + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-syntax-patches-for-csstree': 1.1.15(css-tree@3.2.1) + '@csstools/css-tokenizer': 4.0.2 + '@csstools/media-query-list-parser': 5.0.2(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/selector-resolve-nested': 4.0.2(postcss-selector-parser@7.1.6) + '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.6) colord: 2.9.3 cosmiconfig: 9.0.2(typescript@5.9.3) css-functions-list: 3.3.3 @@ -24422,12 +24322,12 @@ snapshots: micromatch: 4.0.8 normalize-path: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.26 - postcss-safe-parser: 7.0.1(postcss@8.5.26) - postcss-selector-parser: 7.1.5 + postcss: 8.5.28 + postcss-safe-parser: 7.0.1(postcss@8.5.28) + postcss-selector-parser: 7.1.6 postcss-value-parser: 4.2.0 - string-width: 8.2.2 - supports-hyperlinks: 4.5.0 + string-width: 8.3.0 + supports-hyperlinks: 4.6.0 svg-tags: 1.0.0 table: 6.9.0 write-file-atomic: 7.0.1 @@ -24456,7 +24356,7 @@ snapshots: cookiejar: 2.1.4 debug: 4.4.3(supports-color@8.1.1) fast-safe-stringify: 2.1.1 - form-data: 4.0.4 + form-data: 4.0.6 formidable: 2.1.5 methods: 1.1.2 mime: 2.6.0 @@ -24500,7 +24400,7 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-hyperlinks@4.5.0: + supports-hyperlinks@4.6.0: dependencies: has-flag: 5.0.1 supports-color: 10.2.2 @@ -24873,7 +24773,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.0(jiti@1.21.7)(postcss@8.5.26)(typescript@5.9.3): + tsup@8.5.0(jiti@1.21.7)(postcss@8.5.28)(typescript@5.9.3): dependencies: bundle-require: 5.1.0(esbuild@0.25.5) cac: 6.7.14 @@ -24884,7 +24784,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.26) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.28) resolve-from: 5.0.0 rollup: 4.44.1 source-map: 0.8.0-beta.0 @@ -24893,7 +24793,7 @@ snapshots: tinyglobby: 0.2.14 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.26 + postcss: 8.5.28 typescript: 5.9.3 transitivePeerDependencies: - jiti @@ -25082,7 +24982,7 @@ snapshots: undici@6.21.3: {} - undici@6.28.1: {} + undici@6.29.0: {} undici@7.24.0: {} @@ -25107,7 +25007,7 @@ snapshots: pako: 0.2.9 tiny-inflate: 1.0.3 - unicorn-magic@0.4.0: {} + unicorn-magic@0.4.1: {} union@0.5.0: dependencies: @@ -25170,7 +25070,8 @@ snapshots: uuid@8.3.2: {} - uuid@9.0.1: {} + uuid@9.0.1: + optional: true uvu@0.5.6: dependencies: @@ -25229,7 +25130,7 @@ snapshots: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.26 + postcss: 8.5.23 rollup: 4.59.0 tinyglobby: 0.2.17 optionalDependencies: @@ -25282,7 +25183,7 @@ snapshots: joi: 17.13.3 lodash: 4.17.21 minimist: 1.2.8 - rxjs: 7.8.1 + rxjs: 7.8.2 transitivePeerDependencies: - debug @@ -25313,7 +25214,7 @@ snapshots: webidl-conversions@4.0.2: {} - webpack-dev-middleware@7.4.2(tslib@2.8.1)(webpack@5.105.0): + webpack-dev-middleware@7.4.2(tslib@2.8.1)(webpack@5.105.0(esbuild@0.28.1)): dependencies: colorette: 2.0.20 memfs: 4.56.11(tslib@2.8.1) @@ -25326,7 +25227,7 @@ snapshots: transitivePeerDependencies: - tslib - webpack-dev-server@5.2.6(tslib@2.8.1)(webpack@5.105.0): + webpack-dev-server@5.2.6(tslib@2.8.1)(webpack@5.105.0(esbuild@0.28.1)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -25342,7 +25243,7 @@ snapshots: colorette: 2.0.20 compression: 1.8.1 connect-history-api-fallback: 2.0.0 - express: 4.22.3 + express: 4.22.1 graceful-fs: 4.2.11 http-proxy-middleware: 2.0.9(@types/express@4.17.25) ipaddr.js: 2.3.0 @@ -25354,7 +25255,7 @@ snapshots: serve-index: 1.9.2 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(tslib@2.8.1)(webpack@5.105.0) + webpack-dev-middleware: 7.4.2(tslib@2.8.1)(webpack@5.105.0(esbuild@0.28.1)) ws: 8.19.0 optionalDependencies: webpack: 5.105.0(esbuild@0.28.1) @@ -25675,12 +25576,12 @@ snapshots: y18n: 5.0.8 yargs-parser: 22.0.0 - yargs@18.1.0: + yargs@18.2.0: dependencies: cliui: 9.0.1 escalade: 3.2.0 get-caller-file: 2.0.5 - string-width: 8.2.2 + string-width: 8.3.0 y18n: 5.0.8 yargs-parser: 22.0.0