diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d9dedac..f8040de 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -132,7 +132,7 @@ jobs: uses: actions/checkout@v5 - name: Set up Node.js for Frontend build - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '24' cache: 'npm' diff --git a/.github/workflows/fe-installation-test.yml b/.github/workflows/fe-installation-test.yml index 4b57c26..c8b017b 100644 --- a/.github/workflows/fe-installation-test.yml +++ b/.github/workflows/fe-installation-test.yml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@v5 - name: Set up Node.js environment - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: "24" diff --git a/.github/workflows/lint-frontend.yml b/.github/workflows/lint-frontend.yml index 6bdf6ce..2314b2c 100644 --- a/.github/workflows/lint-frontend.yml +++ b/.github/workflows/lint-frontend.yml @@ -29,7 +29,7 @@ jobs: uses: actions/checkout@v5 - name: Set up Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: 24 diff --git a/CHANGELOG.md b/CHANGELOG.md index a44fa07..9020a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# Version: v0.6.2 + +* [#125](https://github.com/SAP/STARS/pull/125): Re organize backend +* [#136](https://github.com/SAP/STARS/pull/136): Bump actions/setup-node from 5 to 6 +* [#137](https://github.com/SAP/STARS/pull/137): Bump the js-dependencies group in /frontend with 17 updates + + # Version: v0.6.1 * [#112](https://github.com/SAP/STARS/pull/112): Bump sentence-transformers from 5.1.0 to 5.1.1 in /backend-agent diff --git a/backend-agent/Dockerfile b/backend-agent/Dockerfile index 173aa72..7172012 100644 --- a/backend-agent/Dockerfile +++ b/backend-agent/Dockerfile @@ -1,4 +1,4 @@ -FROM astral/uv:python3.11-trixie-slim AS builder +FROM astral/uv:python3.12-trixie-slim AS builder # Install build dependencies with minimal footprint RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -38,7 +38,8 @@ RUN . ~/.cargo/env && \ # Remove test files and documentation from packages (keeping runtime libs) find /app/.venv -type d -name "tests" -exec rm -rf {} + 2>/dev/null || true && \ find /app/.venv -type d -name "test" -exec rm -rf {} + 2>/dev/null || true && \ - find /app/.venv -type d -name "docs" -exec rm -rf {} + 2>/dev/null || true && \ + # Do not remove docs folders as they may contain relevant files + # find /app/.venv -type d -name "docs" -exec rm -rf {} + 2>/dev/null || true && \ # Strip debug symbols from shared libraries to reduce size find /app/.venv -name "*.so" -exec strip {} + 2>/dev/null || true && \ # Aggressive cache and temporary file cleanup @@ -59,7 +60,7 @@ RUN . ~/.cargo/env && \ # ---------------------------------------- -FROM python:3.11-slim-trixie AS runtime +FROM python:3.12-slim-trixie AS runtime # Install minimal runtime dependencies RUN apt-get update && apt-get install -y --no-install-recommends \ diff --git a/backend-agent/app/routes.py b/backend-agent/app/routes.py new file mode 100644 index 0000000..5053d2e --- /dev/null +++ b/backend-agent/app/routes.py @@ -0,0 +1,245 @@ +import json +import os + +from flask import request, jsonify, send_file, abort +from sqlalchemy import select + +from app.db.models import Attack, ModelAttackScore, TargetModel, db +from app.utils import send_intro, verify_api_key +from attack_result import SuiteResult +from services import run_all_attacks +from status import status + + +def register_routes(app, sock, agent=None, callbacks=None): + # ---------------------- + # Health endpoints + # ---------------------- + @app.route("/health") + def check_health(): + """ + Health route is used in the CI to test that the installation was + successful. + """ + return jsonify({'status': 'ok'}) + + # ---------------------- + # Attacks endpoints + # ---------------------- + @app.route('/run_all', methods=['POST']) + def execute_all_attacks(): + """ + Run all attacks. Used for automation. + Expected JSON body: + { + "target": "string" + } + """ + verify_api_key() + data = request.get_json() + target_model = data.get('target') if data else None + if not target_model: + return jsonify({'error': 'target parameter is required'}), 400 + # Call the service to run all attacks + result = run_all_attacks( + target=target_model + ) + return jsonify(result), 200 if result.get('success') else 500 + + @app.route('/api/attacks', methods=['GET']) + def get_attacks(): + """ + Endpoint to retrieve all attacks with their weights. + Returns a JSON object with attack names and their weights. + """ + try: + attacks = db.session.query(Attack).all() + attack_list = [ + {'name': attack.name, 'weight': attack.weight} + for attack in attacks + ] + return jsonify(attack_list), 200 + except Exception as e: + return jsonify({'error': str(e)}), 500 + + @app.route('/api/attacks', methods=['PUT']) + def update_attack_weights(): + """ + Update weights for multiple attacks. + Expects a JSON object like: {"artPrompt": 2, "codeAttack": 1, ...} + """ + verify_api_key() + try: + weights = request.get_json() + if not isinstance(weights, dict): + return jsonify({'error': 'Invalid payload format'}), 400 + + for name, weight in weights.items(): + attack = db.session.query(Attack).filter_by(name=name).first() + if attack: + attack.weight = float(weight) + else: + return jsonify({'error': f'Attack not found: {name}'}), 404 + + db.session.commit() + return jsonify({'message': 'Weights updated successfully'}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + + # ---------------------- + # Reports endpoints + # ---------------------- + @app.route('/download_report') + def download_report(): + """ + This route allows to download attack suite reports by specifying + their name. + """ + name = request.args.get('name') + format = request.args.get('format', 'md') + + # Ensure that a name is provided + if not name: + abort(400) + # Ensure that only allowed chars are in the filename + # (e.g. no path traversal) + if not all([c in SuiteResult.FILENAME_ALLOWED_CHARS for c in name]): + abort(400) + + results = SuiteResult.load_from_name(name) + + generated_name = name + '_generated' + path = os.path.join(SuiteResult.DEFAULT_OUTPUT_PATH, generated_name) + result_path = results.to_file(path, format) + return send_file( + result_path, + mimetype=SuiteResult.get_mime_type(format) + ) + + @app.route('/api/heatmap', methods=['GET']) + def get_heatmap(): + """ + Endpoint to retrieve heatmap data showing model score + against various attacks. + + Queries the database for total attacks and successes per target model + and attack combination. + Calculates attack success rate and returns structured data for + visualization. + + Returns: + JSON response with: + - models: List of target models and their attack success rate + per attack. + - attacks: List of attack names and their associated weights. + + HTTP Status Codes: + 200: Data successfully retrieved. + 500: Internal server error during query execution. + """ + try: + query = ( + select( + ModelAttackScore.total_number_of_attack, + ModelAttackScore.total_success, + TargetModel.name.label('attack_model_name'), + Attack.name.label('attack_name'), + Attack.weight.label('attack_weight') + ) + .join(TargetModel, ModelAttackScore.target_model_id == TargetModel.id) # noqa: E501 + .join(Attack, ModelAttackScore.attack_id == Attack.id) + ) + + scores = db.session.execute(query).all() + all_models = {} + all_attacks = {} + + for score in scores: + model_name = score.attack_model_name + attack_name = score.attack_name + + if attack_name not in all_attacks: + all_attacks[attack_name] = score.attack_weight + + if model_name not in all_models: + all_models[model_name] = { + 'name': model_name, + 'scores': {}, + } + + # Compute attack success rate for this model/attack + success_ratio = ( + round((score.total_success / score.total_number_of_attack) * 100) # noqa: E501 + if score.total_number_of_attack else 0 + ) + + all_models[model_name]['scores'][attack_name] = success_ratio + + return jsonify({ + 'models': list(all_models.values()), + 'attacks': [ + {'name': name, 'weight': weight} + for name, weight in sorted(all_attacks.items()) + ] + }) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + # ---------------------- + # WebSocket endpoints + # ---------------------- + @sock.route('/agent') + def query_agent(sock): + """ + Websocket route for the frontend to send prompts to the agent and + receive responses as well as status updates. + + Messages received are in this JSON format: + { + "type":"message", + "data":"Start the vulnerability scan", + "key":"secretapikey" + } + """ + # Verify API key from headers before establishing session + verify_api_key() + if not agent: + sock.send(json.dumps({ + 'type': 'message', + 'data': 'Agent is disabled on this deployment.' + })) + return + status.sock = sock + # Intro is sent after connecting successfully + send_intro(sock) + while True: + try: + data_raw = sock.receive() + data = json.loads(data_raw) + assert 'data' in data + query = data['data'] + status.clear_report() + response = agent.invoke( + {'input': query}, + config=callbacks or {} + ) + ai_response = response['output'] + formatted_output = { + 'type': 'message', + 'data': ( + f'{ai_response}' + ) + } + sock.send(json.dumps(formatted_output)) + except json.JSONDecodeError: + sock.send(json.dumps({ + 'type': 'error', + 'data': 'Invalid JSON format' + })) + except Exception as e: + sock.send(json.dumps({ + 'type': 'error', + 'data': f'Error: {str(e)}' + })) diff --git a/backend-agent/app/utils.py b/backend-agent/app/utils.py new file mode 100644 index 0000000..8c68de5 --- /dev/null +++ b/backend-agent/app/utils.py @@ -0,0 +1,34 @@ +import json +import os + +from flask import request, abort + + +def send_intro(sock): + """ + Sends the intro via the websocket connection. + + The intro is meant as a short tutorial on how to use the agent. + Also it includes meaningful suggestions for prompts that should + result in predictable behavior for the agent, e.g. + "Start the vulnerability scan". + """ + intro_file = 'data/intro.txt' + try: + with open(intro_file, 'r') as f: + intro = f.read() + except FileNotFoundError: + intro = "Welcome! (intro file missing)" + sock.send(json.dumps({'type': 'message', 'data': intro})) + + +def verify_api_key(): + """ + Verifies the API key from the request headers against the env variable. + If no API key is configured, access is allowed. + If API key is configured but missing/invalid, request is rejected. + """ + if os.getenv('API_KEY'): + provided_key = request.headers.get('X-API-Key') + if provided_key != os.getenv('API_KEY'): + abort(403) diff --git a/backend-agent/cli.py b/backend-agent/cli.py index 21591d6..2901e77 100644 --- a/backend-agent/cli.py +++ b/backend-agent/cli.py @@ -3,7 +3,6 @@ import os import sys from argparse import ArgumentParser, Namespace -from pathlib import Path from typing import Callable from attack import AttackSpecification, AttackSuite @@ -17,6 +16,7 @@ test as test_textattack, ) from llm import LLM +from services import run_all_attacks from status import Trace # Library-free Subcommand utilities from @@ -385,31 +385,9 @@ def run(args): required=True), ]) def run_all(args): - """Run all LLM attacks with specified target and evaluation models.""" - default_spec_path = Path('data/all/default.json') - try: - with default_spec_path.open("r") as f: - spec = json.load(f) - except FileNotFoundError: - print(f'File not found: {args.file}', file=sys.stderr) - return - except json.JSONDecodeError as e: - print(f'Invalid JSON format: {e}', file=sys.stderr) - return - except PermissionError: - print(f'Permission denied reading file: {args.file}', file=sys.stderr) - return - if 'attacks' in spec: - suite = AttackSuite.from_dict(spec) - suite.set_target(args.target) - results = suite.run() - result_return = {'success': True, 'results': results} - else: - result_return = { - 'success': False, - 'error': 'JSON is invalid. No attacks run.' - } - return result_return + """Run all LLM attacks with specified target.""" + result = run_all_attacks(target=args.target) + return result @subcommand() diff --git a/backend-agent/main.py b/backend-agent/main.py index 325fdd9..4935d6f 100644 --- a/backend-agent/main.py +++ b/backend-agent/main.py @@ -1,47 +1,46 @@ -import json import os -from argparse import Namespace from importlib.metadata import version -from pathlib import Path from dotenv import load_dotenv -from flask import abort, jsonify, request, send_file from flask_cors import CORS from flask_sock import Sock -from sqlalchemy import select from app import create_app -from app.db.models import Attack, ModelAttackScore, TargetModel, db -from attack import AttackSuite -from attack_result import SuiteResult -from status import LangchainStatusCallbackHandler, status +from app.routes import register_routes +from status import LangchainStatusCallbackHandler +# ----------------------------- +# Version & Environment Setup +# ----------------------------- __version__ = version('stars') load_dotenv() +# ----------------------------- +# Optional Agent Initialization +# ----------------------------- +agent_instance = None if not os.getenv('DISABLE_AGENT'): - from agent import agent -############################################################################# -# Flask web server # -############################################################################# + from agent import agent as agent_instance -# app = Flask(__name__) +# ----------------------------- +# Flask App & WebSocket Setup +# ----------------------------- app = create_app() +sock = Sock(app) # Configure CORS with allowed origins, if any allowed_origins = os.getenv('ALLOWED_ORIGINS', '').split(',') # Clean up empty strings from allowed_origins allowed_origins = [origin.strip() for origin in allowed_origins if origin.strip()] -# Configure CORS if allowed_origins: CORS(app, resources={r"/*": {"origins": allowed_origins}}) else: CORS(app) -sock = Sock(app) - -# Langfuse can be used to analyze tracings and help in debugging. +# --------------------------------------------------- +# Langfuse to analyze tracings and help in debugging. +# --------------------------------------------------- langfuse_handler = None if os.getenv('ENABLE_LANGFUSE'): from langfuse.callback import CallbackHandler @@ -60,267 +59,11 @@ } if langfuse_handler else { 'callbacks': [status_callback_handler]} +register_routes(app, sock, agent_instance, callbacks) -def send_intro(sock): - """ - Sends the intro via the websocket connection. - - The intro is meant as a short tutorial on how to use the agent. - Also it includes meaningful suggestions for prompts that should - result in predictable behavior for the agent, e.g. - "Start the vulnerability scan". - """ - with open('data/intro.txt', 'r') as f: - intro = f.read() - sock.send(json.dumps({'type': 'message', 'data': intro})) - - -def verify_api_key(): - """ - Verifies the API key from the request headers against the env variable. - If the API key is not set or does not match, it aborts the request - with a 403 status code. - """ - if os.getenv('API_KEY'): - provided_key = request.headers.get('X-API-Key') - if provided_key != os.getenv('API_KEY'): - abort(403) - else: - abort(403) - - -@sock.route('/agent') -def query_agent(sock): - """ - Websocket route for the frontend to send prompts to the agent and receive - responses as well as status updates. - - Messages received are in this JSON format: - - { - "type":"message", - "data":"Start the vulnerability scan", - "key":"secretapikey" - } - - """ - status.sock = sock - # Intro is sent after connecting successfully - send_intro(sock) - while True: - data_raw = sock.receive() - data = json.loads(data_raw) - # API Key is used to protect the API if it is exposed in the public - # internet. There is only one API key at the moment. - if os.getenv('API_KEY') and data.get('key', None) != \ - os.getenv('API_KEY'): - sock.send(json.dumps( - {'type': 'message', 'data': 'Not authenticated!'})) - continue - assert 'data' in data - query = data['data'] - status.clear_report() - response = agent.invoke( - {'input': query}, - config=callbacks) - ai_response = response['output'] - formatted_output = {'type': 'message', 'data': f'{ai_response}'} - sock.send(json.dumps(formatted_output)) - - -@app.route('/download_report') -def download_report(): - """ - This route allows to download attack suite reports by specifying - their name. - """ - name = request.args.get('name') - format = request.args.get('format', 'md') - - # Ensure that only allowed chars are in the filename - # (e.g. no path traversal) - if not all([c in SuiteResult.FILENAME_ALLOWED_CHARS for c in name]): - abort(500) - - results = SuiteResult.load_from_name(name) - - path = os.path.join(SuiteResult.DEFAULT_OUTPUT_PATH, name + '_generated') - result_path = results.to_file(path, format) - return send_file(result_path, - mimetype=SuiteResult.get_mime_type(format)) - - -@app.route('/health') -def check_health(): - """ - Health route is used in the CI to test that the installation was - successful. - """ - return jsonify({'status': 'ok'}) - - -# Endpoint to fetch heatmap data from db -@app.route('/api/heatmap', methods=['GET']) -def get_heatmap(): - """ - Endpoint to retrieve heatmap data showing model score - against various attacks. - - Queries the database for total attacks and successes per target model and - attack combination. - Calculates attack success rate and returns structured data for - visualization. - - Returns: - JSON response with: - - models: List of target models and their attack success rate - per attack. - - attacks: List of attack names and their associated weights. - - HTTP Status Codes: - 200: Data successfully retrieved. - 500: Internal server error during query execution. - """ - try: - query = ( - select( - ModelAttackScore.total_number_of_attack, - ModelAttackScore.total_success, - TargetModel.name.label('attack_model_name'), - Attack.name.label('attack_name'), - Attack.weight.label('attack_weight') - ) - .join(TargetModel, ModelAttackScore.target_model_id == TargetModel.id) # noqa: E501 - .join(Attack, ModelAttackScore.attack_id == Attack.id) - ) - - scores = db.session.execute(query).all() - all_models = {} - all_attacks = {} - - for score in scores: - model_name = score.attack_model_name - attack_name = score.attack_name - - if attack_name not in all_attacks: - all_attacks[attack_name] = score.attack_weight - - if model_name not in all_models: - all_models[model_name] = { - 'name': model_name, - 'scores': {}, - } - - # Compute attack success rate for this model/attack - success_ratio = ( - round((score.total_success / score.total_number_of_attack) * 100) # noqa: E501 - if score.total_number_of_attack else 0 - ) - - all_models[model_name]['scores'][attack_name] = success_ratio - - return jsonify({ - 'models': list(all_models.values()), - 'attacks': [ - {'name': name, 'weight': weight} - for name, weight in sorted(all_attacks.items()) - ] - }) - except Exception as e: - return jsonify({'error': str(e)}), 500 - - -@app.route('/api/attacks', methods=['GET']) -def get_attacks(): - """ - Endpoint to retrieve all attacks with their weights. - Returns a JSON object with attack names and their weights. - """ - try: - attacks = db.session.query(Attack).all() - attack_list = [{'name': attack.name, 'weight': attack.weight} - for attack in attacks] - return jsonify(attack_list), 200 - except Exception as e: - return jsonify({'error': str(e)}), 500 - - -@app.route('/api/attacks', methods=['PUT']) -def update_attack_weights(): - """ - Update weights for multiple attacks. - Expects a JSON object like: {"artPrompt": 2, "codeAttack": 1, ...} - """ - verify_api_key() - try: - weights = request.get_json() - if not isinstance(weights, dict): - return jsonify({'error': 'Invalid payload format'}), 400 - - for name, weight in weights.items(): - attack = db.session.query(Attack).filter_by(name=name).first() - if attack: - attack.weight = float(weight) - else: - return jsonify({'error': f'Attack not found: {name}'}), 404 - - db.session.commit() - return jsonify({'message': 'Weights updated successfully'}), 200 - - except Exception as e: - db.session.rollback() - return jsonify({'error': str(e)}), 500 - - -@app.route('/run_all', methods=['POST']) -def execute_all_attacks(): - """ - This route allows to run all attacks. Used for automation - Expected JSON body: - { - "target": "string" - } - """ - # init args - verify_api_key() - target_model = request.get_json().get('target') - - if not target_model: - return jsonify({'error': 'target parameter is required'}), 400 - - args = Namespace( - file='data/all/default.json', - target=target_model, - ) - spec_path = Path(args.file) - try: - with spec_path.open("r") as f: - spec = json.load(f) - except FileNotFoundError: - print(f'File not found: {args.file}') - return - except json.JSONDecodeError as e: - print(f'Invalid JSON format: {e}') - return - except PermissionError: - print(f'Permission denied reading file: {args.file}') - return - try: - if 'attacks' in spec: - suite = AttackSuite.from_dict(spec) - suite.set_target(args.target) - results = suite.run() - result_return = {'success': True, 'results': results} - else: - result_return = { - 'success': False, - 'error': 'JSON is invalid. No attacks run.' - } - return jsonify(result_return) - except Exception as e: - return jsonify({'error': f'Failed to run attacks: {str(e)}'}), 500 - - +# ----------------------------- +# Main Server Entry +# ----------------------------- if __name__ == '__main__': if not os.getenv('API_KEY'): print('No API key is set! Access is unrestricted.') diff --git a/backend-agent/pyproject.toml b/backend-agent/pyproject.toml index f025c82..b00d4ce 100644 --- a/backend-agent/pyproject.toml +++ b/backend-agent/pyproject.toml @@ -1,6 +1,6 @@ [project] name = 'stars' -version = '0.6.1' +version = '0.6.2' description = 'Smart Threat AI Reporting Scanner (STARS)' readme = 'README.md' license = {text = 'Apache-2.0'} diff --git a/backend-agent/services.py b/backend-agent/services.py new file mode 100644 index 0000000..7623e25 --- /dev/null +++ b/backend-agent/services.py @@ -0,0 +1,50 @@ +import json +from pathlib import Path + +from attack import AttackSuite + + +def run_all_attacks( + spec_path: str = "data/all/default.json", + target: str = None +): + """ + Run all LLM attacks with specified target and evaluation models. + + Returns a dict: + - success: bool + - results: list (if success) + - error: str (if failure) + """ + if not target: + return {"success": False, "error": "Target parameter is required"} + default_spec_path = Path(spec_path) + if not default_spec_path.exists(): + return {"success": False, "error": f"File not found: {spec_path}"} + + try: + with default_spec_path.open("r") as f: + spec = json.load(f) + except json.JSONDecodeError as e: + return {"success": False, "error": f"Invalid JSON format: {e}"} + except PermissionError: + return { + "success": False, + "error": f"Permission denied reading file: {spec_path}" + } + try: + if "attacks" in spec: + suite = AttackSuite.from_dict(spec) + if target: + suite.set_target(target) + results = suite.run() + return {"success": True, "results": results} + else: + return { + "success": False, + "error": ( + "JSON is invalid. No attacks run." + ) + } + except Exception as e: + return {"success": False, "error": f"Failed to run attacks: {str(e)}"} diff --git a/backend-agent/uv.lock b/backend-agent/uv.lock index 113ed1d..5ab8fe2 100644 --- a/backend-agent/uv.lock +++ b/backend-agent/uv.lock @@ -5375,7 +5375,7 @@ wheels = [ [[package]] name = "stars" -version = "0.6.1" +version = "0.6.2" source = { editable = "." } dependencies = [ { name = "art" }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 491236d..0ec91cc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,23 +1,23 @@ { "name": "stars", - "version": "0.0.6", + "version": "0.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stars", - "version": "0.0.6", - "dependencies": { - "@angular/animations": "^20.3.4", - "@angular/cdk": "^20.2.8", - "@angular/common": "^20.3.4", - "@angular/compiler": "^20.3.4", - "@angular/core": "^20.3.4", - "@angular/forms": "^20.3.4", - "@angular/material": "^20.2.8", - "@angular/platform-browser": "^20.3.4", - "@angular/platform-browser-dynamic": "^20.3.4", - "@angular/router": "^20.3.4", + "version": "0.0.8", + "dependencies": { + "@angular/animations": "^20.3.6", + "@angular/cdk": "^20.2.9", + "@angular/common": "^20.3.6", + "@angular/compiler": "^20.3.6", + "@angular/core": "^20.3.6", + "@angular/forms": "^20.3.6", + "@angular/material": "^20.2.9", + "@angular/platform-browser": "^20.3.6", + "@angular/platform-browser-dynamic": "^20.3.6", + "@angular/router": "^20.3.6", "@humanfs/core": "^0.19.1", "apexcharts": "^5.3.5", "ng-apexcharts": "^2.0.3", @@ -27,18 +27,18 @@ "zone.js": "^0.15.1" }, "devDependencies": { - "@angular-devkit/build-angular": "^20.3.5", + "@angular-devkit/build-angular": "^20.3.6", "@angular-eslint/builder": "^20.4.0", "@angular-eslint/eslint-plugin": "^20.4.0", "@angular-eslint/eslint-plugin-template": "^20.4.0", "@angular-eslint/schematics": "^20.4.0", "@angular-eslint/template-parser": "^20.1.1", - "@angular/cli": "^20.3.5", - "@angular/compiler-cli": "^20.3.4", - "@types/jasmine": "^5.1.9", - "@typescript-eslint/eslint-plugin": "^8.46.0", + "@angular/cli": "^20.3.6", + "@angular/compiler-cli": "^20.3.6", + "@types/jasmine": "^5.1.12", + "@typescript-eslint/eslint-plugin": "^8.46.1", "@typescript-eslint/parser": "^8.39.0", - "eslint": "^9.37.0", + "eslint": "^9.38.0", "eslint-formatter-rdjson": "^1.0.6", "jasmine-core": "^5.12.0", "karma": "^6.4.4", @@ -274,13 +274,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2003.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2003.5.tgz", - "integrity": "sha512-KtA//ucTIdnKp1+vTYnqBallEbiZHLx3Gs7XgYm+p4VJfVjbMZHWY2vrbJoyCUp05goiv2XnDy0bKQ9VYHePWg==", + "version": "0.2003.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2003.6.tgz", + "integrity": "sha512-VtXxfJzrBZ8MQN83shXNaTUaLSOIwa+4/3LD5drxSnHuYJrz+d3FIApWAxcA9QzucsTDZwXyFxaWZN/e5XVm6g==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.3.5", + "@angular-devkit/core": "20.3.6", "rxjs": "7.8.2" }, "engines": { @@ -290,17 +290,17 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "20.3.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-20.3.5.tgz", - "integrity": "sha512-33bG6ic/GC9OrqPiR6ynTpnw9vKfebZtWQzFO9ovjkUoZt4lUFWUgo/F0zeoaJgj0N35Ihn3Dvtxz/x2rwr9lw==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-20.3.6.tgz", + "integrity": "sha512-yCybq8Lh6PnuN5oa81qFDmHjV/MMB1tOY99NU6N/DM4IcbGdyS8IFEeVvM3ohz6bTnqvkmi3rSxWs1jDWvm5/Q==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2003.5", - "@angular-devkit/build-webpack": "0.2003.5", - "@angular-devkit/core": "20.3.5", - "@angular/build": "20.3.5", + "@angular-devkit/architect": "0.2003.6", + "@angular-devkit/build-webpack": "0.2003.6", + "@angular-devkit/core": "20.3.6", + "@angular/build": "20.3.6", "@babel/core": "7.28.3", "@babel/generator": "7.28.3", "@babel/helper-annotate-as-pure": "7.27.3", @@ -311,7 +311,7 @@ "@babel/preset-env": "7.28.3", "@babel/runtime": "7.28.3", "@discoveryjs/json-ext": "0.6.3", - "@ngtools/webpack": "20.3.5", + "@ngtools/webpack": "20.3.6", "ansi-colors": "4.1.3", "autoprefixer": "10.4.21", "babel-loader": "10.0.0", @@ -366,7 +366,7 @@ "@angular/platform-browser": "^20.0.0", "@angular/platform-server": "^20.0.0", "@angular/service-worker": "^20.0.0", - "@angular/ssr": "^20.3.5", + "@angular/ssr": "^20.3.6", "@web/test-runner": "^0.20.0", "browser-sync": "^3.0.2", "jest": "^29.5.0", @@ -444,13 +444,13 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.2003.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.2003.5.tgz", - "integrity": "sha512-BdcaJ5c/a+SD8ZaUo1Qq0a03zeTvGX4ydIaM4li5JYuA8bPCibU9tnb/1FcOWgIZVL1RdV6oIIqbAW6ufq7e1g==", + "version": "0.2003.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.2003.6.tgz", + "integrity": "sha512-KcPIZChvJH2+MscD66Ef6+Od8bVjZXnRHpCCxgcmT+VOC2682cCgBVeZFXXlC7+SI8MfFLashIIY3RN5ORYv2w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2003.5", + "@angular-devkit/architect": "0.2003.6", "rxjs": "7.8.2" }, "engines": { @@ -464,9 +464,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "20.3.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.5.tgz", - "integrity": "sha512-NpAP5j3q/n+SC1s0yAWKDAbc7Y8xUxlmJ5iDRJBGu6qDKM7lMnYA1tn2UEy/JnXluJ2XZqqiymrtucw7yux2xQ==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.6.tgz", + "integrity": "sha512-uLRk3865Iz/EO9Zm/mrFfdyoZinJBihXE6HVDYRYjAqsgW14LsD8pkpWy9+LYlOwcH96Ndnev+msxaTJaNXtPg==", "dev": true, "license": "MIT", "dependencies": { @@ -492,13 +492,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "20.3.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.5.tgz", - "integrity": "sha512-BDizJp7QIoCyMZmuGKoryNUH3QgFPnkEIv0gRdpLhZum4+ZN/DYWaf/jSSGnSVGK88oMrgq7420VEjYPlgJ5MA==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.6.tgz", + "integrity": "sha512-QD7QS1oR0XcZ9ZI4D1c4JjKmSn2up/ocOU2FS1mMO7S5RtAZMsPv4J3r+6ywHA2ev2sRySOQ0D8OYBcEuYX9Jw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.3.5", + "@angular-devkit/core": "20.3.6", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "8.2.0", @@ -645,9 +645,9 @@ "license": "MIT" }, "node_modules/@angular/animations": { - "version": "20.3.4", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.4.tgz", - "integrity": "sha512-b+vFsTtMYtOrcZZLXB4BxuErbrLlShFT6khTvkwu/pFK8ri3tasyJGkeKRZJHao5ZsWdZSqV2mRwzg7vphchnA==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.6.tgz", + "integrity": "sha512-qNaVvEOKvigoCQMg0ABnq44HhiHqKD4WN3KoUcXneklcMYCzFE5nuQxKylfWzCRiI5XqiJ9pqiL1m2D7o+Vdiw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -656,18 +656,18 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "20.3.4" + "@angular/core": "20.3.6" } }, "node_modules/@angular/build": { - "version": "20.3.5", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-20.3.5.tgz", - "integrity": "sha512-Nwwwm8U7lolkdHt75PiPkW93689SBFUN9qEQeu02sPfq2Tqyn20PZGifXkV8A/6mlWbQUjfUnGpRTVk/WhW9Eg==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-20.3.6.tgz", + "integrity": "sha512-O5qyxCCe77tu1zy9XudKxqFqi5zih0ZI8J8Anra/ZZdtTKbLMprXMGFzMYzwCqvcIzzbmOumkSJKoXbFazHaaw==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2003.5", + "@angular-devkit/architect": "0.2003.6", "@babel/core": "7.28.3", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", @@ -709,7 +709,7 @@ "@angular/platform-browser": "^20.0.0", "@angular/platform-server": "^20.0.0", "@angular/service-worker": "^20.0.0", - "@angular/ssr": "^20.3.5", + "@angular/ssr": "^20.3.6", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^20.0.0", @@ -780,9 +780,9 @@ } }, "node_modules/@angular/cdk": { - "version": "20.2.8", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.8.tgz", - "integrity": "sha512-r2GzcgwkRETKUmFhGfmT+T0RYRcYb/JrpADTnUG3sOkHY+05r7YGkmmXMjUIB0nVERqVuFBM1mKVcIFp9SJDmQ==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.9.tgz", + "integrity": "sha512-rbY1AMz9389WJI29iAjWp4o0QKRQHCrQQUuP0ctNQzh1tgWpwiRLx8N4yabdVdsCA846vPsyKJtBlSNwKMsjJA==", "license": "MIT", "dependencies": { "parse5": "^8.0.0", @@ -795,19 +795,19 @@ } }, "node_modules/@angular/cli": { - "version": "20.3.5", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.5.tgz", - "integrity": "sha512-UA843Mh5uHIWnrzKUotGmhJmvefyEizFS7X8xJEUJsX5pa1EKUB/145rKHoLHxRRpHGxFcXtvciJCksFz1lSBA==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.6.tgz", + "integrity": "sha512-1RozAub7Gcl5ES3vBYatIgoMDgujlvySwHARoYT+1VhbYvM0RTt4sn2aDhHxqG0GcyiXR5zISkzJvldaY2nQCQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2003.5", - "@angular-devkit/core": "20.3.5", - "@angular-devkit/schematics": "20.3.5", + "@angular-devkit/architect": "0.2003.6", + "@angular-devkit/core": "20.3.6", + "@angular-devkit/schematics": "20.3.6", "@inquirer/prompts": "7.8.2", "@listr2/prompt-adapter-inquirer": "3.0.1", "@modelcontextprotocol/sdk": "1.17.3", - "@schematics/angular": "20.3.5", + "@schematics/angular": "20.3.6", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.35.0", "ini": "5.0.0", @@ -830,9 +830,9 @@ } }, "node_modules/@angular/common": { - "version": "20.3.4", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.4.tgz", - "integrity": "sha512-hBQahyiHEAtc30a8hPOuIWcUL7j8189DC0sX4RiBd/wtvzH4Sd3XhguxyZAL6gHgNZEQ0nKy0uAfvWB/79GChQ==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.6.tgz", + "integrity": "sha512-+gHMuFe0wz4f+vfGZ2q+fSQSYaY7KlN7QdDrFqLnA7H2sythzhXvRbXEtp4DkPjihh9gupXg2MeLh1ROy5AfSw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -841,14 +841,14 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "20.3.4", + "@angular/core": "20.3.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "20.3.4", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.4.tgz", - "integrity": "sha512-ISfEyn5ppWOP2hyZy0/IFEcQOaq5x1mXVZ2CRCxTpWyXYzavj27Ahr3+LQHPVV0uTNovlENyPAUnrzW+gLxXEw==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.6.tgz", + "integrity": "sha512-OdjXBsAsnn7qiW6fSHClwn9XwjVxhtO9+RbDc6Mf+YPCnJq0s8T78H2fc8VdJFp/Rs+tMZcwwjd9VZPm8+2XWA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -858,9 +858,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "20.3.4", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.4.tgz", - "integrity": "sha512-FwEv+QM9tEMXDMd2V+j82VLOjJVUrI7ENz/LJa2p/YEGVJQkT3HVca5qJj+8I+7bfPEY/d46R/cmjjqMqUcYdg==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.6.tgz", + "integrity": "sha512-VOFRBx9fBt2jW9I8qD23fwGeKxBI8JssJBAMqnFPl3k59VJWHQi6LlXZCLCBNdfwflTJdKeRvdgT51Q0k6tnFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -881,7 +881,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "20.3.4", + "@angular/compiler": "20.3.6", "typescript": ">=5.8 <6.0" }, "peerDependenciesMeta": { @@ -891,9 +891,9 @@ } }, "node_modules/@angular/core": { - "version": "20.3.4", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.4.tgz", - "integrity": "sha512-qLYVXcpSBmsA/9fITB1cT2y37V1Yo3ybWEwvafnbAh8izabrMV0hh+J9Ajh9bPk092T7LS8Xt9eTu0OquBXkbw==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.6.tgz", + "integrity": "sha512-sDURQWnjwE4Y750u/5qwkZEYMoI4CrKghnx4aKulxCnohR3//C78wvz6p8MtCuqYfzGkdQZDYFg8tgAz17qgPw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -902,7 +902,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "20.3.4", + "@angular/compiler": "20.3.6", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0" }, @@ -916,9 +916,9 @@ } }, "node_modules/@angular/forms": { - "version": "20.3.4", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.4.tgz", - "integrity": "sha512-gPbI2iIvVA2jhwjTg7e3j/JqCFIpRSPgzW/wi5q7LrGBm7XKHNzY5xlEVDNvdq+gC4HTius9GOIx9I0i8mjrEw==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.6.tgz", + "integrity": "sha512-tBGo/LBtCtSrClMY4DTm/3UiSjqLLMEYXS/4E0nW1mFDv7ulKnaAQB+KbfBmmTHYxlKLs+SxjKv6GoydMPSurA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -927,22 +927,22 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "20.3.4", - "@angular/core": "20.3.4", - "@angular/platform-browser": "20.3.4", + "@angular/common": "20.3.6", + "@angular/core": "20.3.6", + "@angular/platform-browser": "20.3.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/material": { - "version": "20.2.8", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-20.2.8.tgz", - "integrity": "sha512-JexykfKGTM+oepHZVVPRGCJOs1PWVzdvzonSJ3xuchkNeUZPbrGlWb+wZj84RgpjSGj4ktJ1artrVH/yvLPuhw==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-20.2.9.tgz", + "integrity": "sha512-xo/ozyRXCoJMi89XLTJI6fdPKBv2wBngWMiCrtTg23+pHbuyA/kDbk3v62eJkDD1xdhC4auXaIHu4Ddf5zTgSA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/cdk": "20.2.8", + "@angular/cdk": "20.2.9", "@angular/common": "^20.0.0 || ^21.0.0", "@angular/core": "^20.0.0 || ^21.0.0", "@angular/forms": "^20.0.0 || ^21.0.0", @@ -951,9 +951,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "20.3.4", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.4.tgz", - "integrity": "sha512-8eoHC+vk7scb24qjlpsirkh1Q17uFyWdhl+u92XNjvimtZRY96oDZeFEDPoexPqtKUQcCOpEPbL8P/IbpBsqYQ==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.6.tgz", + "integrity": "sha512-gFp1yd+HtRN8XdpMatRLO5w6FLIzsnF31lD2Duo4BUTCoMAMdfaNT6FtcvNdKu7ANo27Ke26fxEEE2bh6FU98A==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -962,9 +962,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/animations": "20.3.4", - "@angular/common": "20.3.4", - "@angular/core": "20.3.4" + "@angular/animations": "20.3.6", + "@angular/common": "20.3.6", + "@angular/core": "20.3.6" }, "peerDependenciesMeta": { "@angular/animations": { @@ -973,9 +973,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "20.3.4", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.4.tgz", - "integrity": "sha512-eeScVJyZLDTNrnEDDBgF/WZpZrjEszFFkuEzNQ43sbPjc5M7Noue38Nd9QZ664ZQ3a4ZpUpritfHvc55a/fl9Q==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.6.tgz", + "integrity": "sha512-teO8tBygk6vD1waiLmHGXtXPF/9a9Bw2XI+s550KtJlQqRpr7IUWOFPPQik/uGkppv5Jrv6fP+8mh9QX9zoWnQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -984,16 +984,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "20.3.4", - "@angular/compiler": "20.3.4", - "@angular/core": "20.3.4", - "@angular/platform-browser": "20.3.4" + "@angular/common": "20.3.6", + "@angular/compiler": "20.3.6", + "@angular/core": "20.3.6", + "@angular/platform-browser": "20.3.6" } }, "node_modules/@angular/router": { - "version": "20.3.4", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.4.tgz", - "integrity": "sha512-qMVurWXVplYeHBKOWtQFtD+MfwOc0i/VJaFPGdiM5mDlfhtsg3OHcZBbSTmQW02l/4YimF5Ee3+pac279p+g1A==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.6.tgz", + "integrity": "sha512-fSAYOR9nKpH5PoBYFNdII3nAFl2maUrYiISU33CnGwb7J7Q0s09k231c/P5tVN4URi+jdADVwiBI8cIYk8SVrg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1002,9 +1002,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "20.3.4", - "@angular/core": "20.3.4", - "@angular/platform-browser": "20.3.4", + "@angular/common": "20.3.6", + "@angular/core": "20.3.6", + "@angular/platform-browser": "20.3.6", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -3200,13 +3200,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -3239,9 +3239,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", - "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", + "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3347,9 +3347,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", - "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", "dev": true, "license": "MIT", "engines": { @@ -3360,9 +3360,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4009,9 +4009,9 @@ } }, "node_modules/@jsonjoy.com/buffers": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.0.tgz", - "integrity": "sha512-6RX+W5a+ZUY/c/7J5s5jK9UinLfJo5oWKh84fb4X0yK2q4WXEWUWZWuEMjvCb1YNUQhEAhUfr5scEGOH7jC4YQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4043,9 +4043,9 @@ } }, "node_modules/@jsonjoy.com/json-pack": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.20.0.tgz", - "integrity": "sha512-adcXFVorSQULtT4XDL0giRLr2EVGIcyWm6eQKZWTrRA4EEydGOY8QVQtL0PaITQpUyu+lOd/QOicw6vdy1v8QQ==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4055,7 +4055,8 @@ "@jsonjoy.com/json-pointer": "^1.0.2", "@jsonjoy.com/util": "^1.9.0", "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0" + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" }, "engines": { "node": ">=10.0" @@ -4698,9 +4699,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "20.3.5", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-20.3.5.tgz", - "integrity": "sha512-LThQWzgEw6a1eTRWn2hUxwVe8WalL75yola4AaI8gJVqlRhfTcjqpleULihCm9cynn3HnVsaHFElaoLdxhugCA==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-20.3.6.tgz", + "integrity": "sha512-PM3ODWdiYmLfUueJR+jpffuX1qwM6kyEOg/SE9+kfSSyu9dRFt3k5LoAHAzH+gbs1JsvztmG/wfkE/ZlexteKQ==", "dev": true, "license": "MIT", "engines": { @@ -5674,14 +5675,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "20.3.5", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.5.tgz", - "integrity": "sha512-mrVWO64psqah8E8HgpF30NMizVZyX6aH3k6hqf2tDgU3+giKX7xvTG9UQCaXA4MLBsQbpcWAmwPLipwLnPm8wA==", + "version": "20.3.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.6.tgz", + "integrity": "sha512-YPIEyKPBOyJYlda5fA49kMThzZ4WidomEMDghshux8xidbjDaPWBZdyVPQj3IXyW0teGlUM/TH0TH2weumMZrg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.3.5", - "@angular-devkit/schematics": "20.3.5", + "@angular-devkit/core": "20.3.6", + "@angular-devkit/schematics": "20.3.6", "jsonc-parser": "3.3.1" }, "engines": { @@ -6273,9 +6274,9 @@ } }, "node_modules/@types/jasmine": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.9.tgz", - "integrity": "sha512-8t4HtkW4wxiPVedMpeZ63n3vlWxEIquo/zc1Tm8ElU+SqVV7+D3Na2PWaJUp179AzTragMWVwkMv7mvty0NfyQ==", + "version": "5.1.12", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.12.tgz", + "integrity": "sha512-1BzPxNsFDLDfj9InVR3IeY0ZVf4o9XV+4mDqoCfyPkbsA7dYyKAPAb2co6wLFlHcvxPlt1wShm7zQdV7uTfLGA==", "dev": true, "license": "MIT" }, @@ -6405,17 +6406,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz", - "integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", + "integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.0", - "@typescript-eslint/type-utils": "8.46.0", - "@typescript-eslint/utils": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/type-utils": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -6429,22 +6430,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.0", + "@typescript-eslint/parser": "^8.46.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz", - "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", + "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.46.0", - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/typescript-estree": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4" }, "engines": { @@ -6460,14 +6461,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz", - "integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz", + "integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.0", - "@typescript-eslint/types": "^8.46.0", + "@typescript-eslint/tsconfig-utils": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", "debug": "^4.3.4" }, "engines": { @@ -6482,14 +6483,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz", - "integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", + "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0" + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6500,9 +6501,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz", - "integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz", + "integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", "dev": true, "license": "MIT", "engines": { @@ -6517,15 +6518,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz", - "integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz", + "integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/typescript-estree": "8.46.0", - "@typescript-eslint/utils": "8.46.0", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/utils": "8.46.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -6542,9 +6543,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz", - "integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", + "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", "dev": true, "license": "MIT", "engines": { @@ -6556,16 +6557,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz", - "integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", + "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.0", - "@typescript-eslint/tsconfig-utils": "8.46.0", - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0", + "@typescript-eslint/project-service": "8.46.1", + "@typescript-eslint/tsconfig-utils": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6585,16 +6586,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz", - "integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", + "integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.0", - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/typescript-estree": "8.46.0" + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6609,13 +6610,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz", - "integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", + "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/types": "8.46.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -9569,25 +9570,24 @@ } }, "node_modules/eslint": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", - "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.4.0", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.1", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.37.0", + "@eslint/js": "9.38.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", diff --git a/frontend/package.json b/frontend/package.json index e84b834..19a41f4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "stars", - "version": "0.0.7", + "version": "0.0.8", "scripts": { "ng": "ng", "start": "ng serve", @@ -11,16 +11,16 @@ }, "private": true, "dependencies": { - "@angular/animations": "^20.3.4", - "@angular/cdk": "^20.2.8", - "@angular/common": "^20.3.4", - "@angular/compiler": "^20.3.4", - "@angular/core": "^20.3.4", - "@angular/forms": "^20.3.4", - "@angular/material": "^20.2.8", - "@angular/platform-browser": "^20.3.4", - "@angular/platform-browser-dynamic": "^20.3.4", - "@angular/router": "^20.3.4", + "@angular/animations": "^20.3.6", + "@angular/cdk": "^20.2.9", + "@angular/common": "^20.3.6", + "@angular/compiler": "^20.3.6", + "@angular/core": "^20.3.6", + "@angular/forms": "^20.3.6", + "@angular/material": "^20.2.9", + "@angular/platform-browser": "^20.3.6", + "@angular/platform-browser-dynamic": "^20.3.6", + "@angular/router": "^20.3.6", "@humanfs/core": "^0.19.1", "apexcharts": "^5.3.5", "ng-apexcharts": "^2.0.3", @@ -30,18 +30,18 @@ "zone.js": "^0.15.1" }, "devDependencies": { - "@angular-devkit/build-angular": "^20.3.5", + "@angular-devkit/build-angular": "^20.3.6", "@angular-eslint/builder": "^20.4.0", "@angular-eslint/eslint-plugin": "^20.4.0", "@angular-eslint/eslint-plugin-template": "^20.4.0", "@angular-eslint/schematics": "^20.4.0", "@angular-eslint/template-parser": "^20.1.1", - "@angular/cli": "^20.3.5", - "@angular/compiler-cli": "^20.3.4", - "@types/jasmine": "^5.1.9", - "@typescript-eslint/eslint-plugin": "^8.46.0", + "@angular/cli": "^20.3.6", + "@angular/compiler-cli": "^20.3.6", + "@types/jasmine": "^5.1.12", + "@typescript-eslint/eslint-plugin": "^8.46.1", "@typescript-eslint/parser": "^8.39.0", - "eslint": "^9.37.0", + "eslint": "^9.38.0", "eslint-formatter-rdjson": "^1.0.6", "jasmine-core": "^5.12.0", "karma": "^6.4.4",