From 9958a8a60045045972b35def4c9c101c81c5fba7 Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 5 Sep 2026 17:33:58 -0400 Subject: [PATCH] feat: add real motorcycle routing provider foundation --- lib/services/routes/route_provider.dart | 138 +- .../routes/valhalla_polyline_decoder.dart | 71 + .../routes/valhalla_route_provider.dart | 284 ++ server/waze_relay/middleware/app_check.js | 37 + server/waze_relay/middleware/firebase_auth.js | 42 + server/waze_relay/package-lock.json | 2703 +++++++++++++++++ server/waze_relay/package.json | 13 +- server/waze_relay/routes/places.js | 216 ++ server/waze_relay/routes/routing.js | 173 ++ server/waze_relay/routes/waze.js | 251 ++ server/waze_relay/server.js | 321 +- .../waze_relay/test/auth_and_appcheck.test.js | 206 ++ .../test/routing_and_places.test.js | 386 +++ .../test/waze_compatibility.test.js | 42 + test/fake_route_provider.dart | 86 + test/fake_route_provider_test.dart | 63 + test/valhalla_polyline_decoder_test.dart | 62 + test/valhalla_route_provider_test.dart | 261 ++ 18 files changed, 5045 insertions(+), 310 deletions(-) create mode 100644 lib/services/routes/valhalla_polyline_decoder.dart create mode 100644 lib/services/routes/valhalla_route_provider.dart create mode 100644 server/waze_relay/middleware/app_check.js create mode 100644 server/waze_relay/middleware/firebase_auth.js create mode 100644 server/waze_relay/routes/places.js create mode 100644 server/waze_relay/routes/routing.js create mode 100644 server/waze_relay/routes/waze.js create mode 100644 server/waze_relay/test/auth_and_appcheck.test.js create mode 100644 server/waze_relay/test/routing_and_places.test.js create mode 100644 server/waze_relay/test/waze_compatibility.test.js create mode 100644 test/fake_route_provider.dart create mode 100644 test/fake_route_provider_test.dart create mode 100644 test/valhalla_polyline_decoder_test.dart create mode 100644 test/valhalla_route_provider_test.dart diff --git a/lib/services/routes/route_provider.dart b/lib/services/routes/route_provider.dart index 6759363..dfd1eb6 100644 --- a/lib/services/routes/route_provider.dart +++ b/lib/services/routes/route_provider.dart @@ -1,20 +1,75 @@ import '../map/map_provider_interface.dart'; import 'route_models.dart'; +/// Exceptions domain mapped from standard provider API. +class RouteProviderException implements Exception { + final String code; + final String message; + + RouteProviderException(this.code, this.message); + + @override + String toString() => 'RouteProviderException: $message ($code)'; +} + /// Result of a place/location search query. class PlaceSearchResult { - final String name; - final String address; - final double latitude; - final double longitude; - final String? placeId; + final String providerId; + final String primaryText; + final String secondaryText; + final double? latitude; + final double? longitude; + final String? formattedAddress; + final String? attribution; const PlaceSearchResult({ - required this.name, - this.address = '', + required this.providerId, + required this.primaryText, + required this.secondaryText, + this.latitude, + this.longitude, + this.formattedAddress, + this.attribution, + }); + + bool get hasCoordinates => latitude != null && longitude != null; + + AppLatLng? get latLng => + hasCoordinates ? AppLatLng(latitude!, longitude!) : null; + + /// Converts directly into a [ResolvedPlace] when coordinates are already present. + ResolvedPlace? toResolvedPlace() { + if (!hasCoordinates) return null; + return ResolvedPlace( + providerId: providerId, + displayName: primaryText, + formattedAddress: formattedAddress ?? + (secondaryText.isNotEmpty + ? '$primaryText, $secondaryText' + : primaryText), + latitude: latitude!, + longitude: longitude!, + attribution: attribution, + ); + } +} + +/// Resolved place with authoritative coordinates. +class ResolvedPlace { + final String providerId; + final String displayName; + final String? formattedAddress; + final double latitude; + final double longitude; + final String? attribution; + + const ResolvedPlace({ + required this.providerId, + required this.displayName, + this.formattedAddress, required this.latitude, required this.longitude, - this.placeId, + this.attribution, }); AppLatLng get latLng => AppLatLng(latitude, longitude); @@ -28,6 +83,7 @@ class RouteCalculationResult { final List geometry; final bool isStraightLine; final String providerName; + final List warnings; const RouteCalculationResult({ required this.straightLineDistanceMeters, @@ -36,6 +92,7 @@ class RouteCalculationResult { required this.geometry, required this.isStraightLine, required this.providerName, + this.warnings = const [], }); /// Alias for geometry points. @@ -67,13 +124,21 @@ abstract class RouteProvider { String get name; /// Calculates route geometry and metrics between ordered stops. - Future calculateRoute(List stops); + Future calculateRoute( + List stops, { + bool avoidHighways = false, + bool avoidTolls = false, + bool avoidFerries = false, + }); /// Searches for places by textual query. Future> searchPlaces( String query, { AppLatLng? nearLocation, }); + + /// Resolves a place ID into full coordinates. + Future resolvePlace(String placeId); } /// Default geodesic straight-line preview implementation. @@ -87,11 +152,17 @@ class StraightLineRouteProvider implements RouteProvider { String get name => 'Straight-line Preview'; @override - Future calculateRoute(List stops) async { + Future calculateRoute( + List stops, { + bool avoidHighways = false, + bool avoidTolls = false, + bool avoidFerries = false, + }) async { final ordered = List.from(stops); ordered.sort((a, b) => a.order.compareTo(b.order)); - final distanceMeters = MotorSignalRoute.calculateTotalDistanceMeters(ordered); + final distanceMeters = + MotorSignalRoute.calculateTotalDistanceMeters(ordered); final points = ordered.map((s) => s.latLng).toList(); return RouteCalculationResult( @@ -112,35 +183,48 @@ class StraightLineRouteProvider implements RouteProvider { final clean = query.trim(); if (clean.isEmpty) return const []; - // Support direct coordinate input (e.g. "34.5378, -83.9849") + // Check if query is direct coordinates (lat, lng) final parts = clean.split(RegExp(r'[,\s]+')); if (parts.length == 2) { final lat = double.tryParse(parts[0]); final lng = double.tryParse(parts[1]); - if (lat != null && lng != null && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) { + if (lat != null && + lng != null && + lat >= -90 && + lat <= 90 && + lng >= -180 && + lng <= 180) { return [ PlaceSearchResult( - name: 'Coordinate Pin', - address: '${lat.toStringAsFixed(5)}, ${lng.toStringAsFixed(5)}', + providerId: 'coord_${lat.toStringAsFixed(4)}_${lng.toStringAsFixed(4)}', + primaryText: 'Coordinate Pin', + secondaryText: '${lat.toStringAsFixed(4)}, ${lng.toStringAsFixed(4)}', latitude: lat, longitude: lng, + formattedAddress: '${lat.toStringAsFixed(5)}, ${lng.toStringAsFixed(5)}', ), ]; } } - // Default fallback: return query name centered at reference location if provided - if (nearLocation != null) { - return [ - PlaceSearchResult( - name: clean, - address: 'Near Current Map Location', - latitude: nearLocation.latitude, - longitude: nearLocation.longitude, - ), - ]; - } + return [ + PlaceSearchResult( + providerId: 'manual', + primaryText: clean, + secondaryText: 'Near Current Map Location', + latitude: nearLocation?.latitude, + longitude: nearLocation?.longitude, + ), + ]; + } - return const []; + @override + Future resolvePlace(String placeId) async { + return ResolvedPlace( + providerId: placeId, + displayName: 'Unknown Place', + latitude: 0, + longitude: 0, + ); } } diff --git a/lib/services/routes/valhalla_polyline_decoder.dart b/lib/services/routes/valhalla_polyline_decoder.dart new file mode 100644 index 0000000..a369765 --- /dev/null +++ b/lib/services/routes/valhalla_polyline_decoder.dart @@ -0,0 +1,71 @@ +import '../map/map_provider_interface.dart'; + +/// Decodes Valhalla's precision-6 encoded polyline into standard AppLatLng coordinates. +class ValhallaPolylineDecoder { + /// Decodes precision-6 polyline string into a list of [AppLatLng]. + /// + /// Throws [FormatException] if the encoded string is truncated, contains + /// invalid character codes (< 63), or resolves to coordinates outside valid + /// geographic bounds (lat: [-90, 90], lng: [-180, 180]). + static List decode(String encoded) { + if (encoded.isEmpty) return const []; + + final List polyline = []; + int index = 0; + int lat = 0; + int lng = 0; + final int len = encoded.length; + + while (index < len) { + // Decode latitude delta + int b; + int shift = 0; + int result = 0; + do { + if (index >= len) { + throw const FormatException('Truncated polyline data while reading latitude'); + } + final code = encoded.codeUnitAt(index++); + if (code < 63) { + throw FormatException('Invalid polyline character at index ${index - 1}: code $code'); + } + b = code - 63; + result |= (b & 0x1f) << shift; + shift += 5; + } while (b >= 0x20); + + final int dlat = (result & 1) != 0 ? ~(result >> 1) : (result >> 1); + lat += dlat; + + // Decode longitude delta + shift = 0; + result = 0; + do { + if (index >= len) { + throw const FormatException('Truncated polyline data while reading longitude'); + } + final code = encoded.codeUnitAt(index++); + if (code < 63) { + throw FormatException('Invalid polyline character at index ${index - 1}: code $code'); + } + b = code - 63; + result |= (b & 0x1f) << shift; + shift += 5; + } while (b >= 0x20); + + final int dlng = (result & 1) != 0 ? ~(result >> 1) : (result >> 1); + lng += dlng; + + final latDeg = lat / 1e6; + final lngDeg = lng / 1e6; + + if (latDeg < -90.0 || latDeg > 90.0 || lngDeg < -180.0 || lngDeg > 180.0) { + throw FormatException('Decoded coordinate out of geographic bounds: ($latDeg, $lngDeg)'); + } + + polyline.add(AppLatLng(latDeg, lngDeg)); + } + + return polyline; + } +} diff --git a/lib/services/routes/valhalla_route_provider.dart b/lib/services/routes/valhalla_route_provider.dart new file mode 100644 index 0000000..eb1f679 --- /dev/null +++ b/lib/services/routes/valhalla_route_provider.dart @@ -0,0 +1,284 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:firebase_auth/firebase_auth.dart'; +import '../map/map_provider_interface.dart'; +import 'route_models.dart'; +import 'route_provider.dart'; +import 'valhalla_polyline_decoder.dart'; + +/// Provider integrating with the MotorSignal Node.js routing and places relay. +class ValhallaRouteProvider implements RouteProvider { + final String backendUrl; + final http.Client _client; + final Future Function()? _tokenProvider; + + ValhallaRouteProvider({ + this.backendUrl = 'http://10.0.2.2:3000', + http.Client? client, + this._tokenProvider, + }) : _client = client ?? http.Client(); + + @override + String get name => 'Stadia Maps (Valhalla)'; + + Future> _getAuthHeaders() async { + String? token; + final customTokenProvider = _tokenProvider; + if (customTokenProvider != null) { + token = await customTokenProvider(); + } else { + final user = FirebaseAuth.instance.currentUser; + if (user == null) { + throw RouteProviderException( + 'authenticationRequired', + 'User must be authenticated (or signed in as guest) to use cloud routing.', + ); + } + try { + token = await user.getIdToken(); + } catch (e) { + throw RouteProviderException( + 'authenticationRequired', + 'Failed to acquire authentication token: $e', + ); + } + } + + if (token == null || token.isEmpty) { + throw RouteProviderException( + 'authenticationRequired', + 'User must be authenticated to use cloud routing.', + ); + } + + return { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }; + } + + void _handleHttpError(http.Response response) { + if (response.statusCode >= 200 && response.statusCode < 300) return; + + String code = 'providerError'; + String msg = 'An unexpected provider error occurred.'; + + try { + final body = jsonDecode(response.body); + if (body is Map) { + code = body['error']?.toString() ?? code; + msg = body['message']?.toString() ?? msg; + } + } catch (_) {} + + throw RouteProviderException(code, msg); + } + + @override + Future> searchPlaces( + String query, { + AppLatLng? nearLocation, + }) async { + final clean = query.trim(); + if (clean.length < 2) return const []; + + final headers = await _getAuthHeaders(); + + try { + final queryParams = { + 'query': clean, + }; + if (nearLocation != null) { + queryParams['lat'] = nearLocation.latitude.toString(); + queryParams['lng'] = nearLocation.longitude.toString(); + } + + final uri = Uri.parse('$backendUrl/api/places/search') + .replace(queryParameters: queryParams); + + final response = await _client + .get(uri, headers: headers) + .timeout(const Duration(seconds: 8)); + + _handleHttpError(response); + + final data = jsonDecode(response.body); + if (data is! Map) return const []; + + final results = data['results'] as List? ?? []; + final attribution = data['attribution'] as String?; + + return results.whereType>().map((r) { + final lat = (r['latitude'] as num?)?.toDouble(); + final lng = (r['longitude'] as num?)?.toDouble(); + return PlaceSearchResult( + providerId: r['providerId']?.toString() ?? '', + primaryText: r['primaryText']?.toString() ?? '', + secondaryText: r['secondaryText']?.toString() ?? '', + latitude: lat, + longitude: lng, + formattedAddress: r['formattedAddress']?.toString(), + attribution: attribution, + ); + }).toList(); + } on TimeoutException { + throw RouteProviderException( + 'providerTimeout', + 'Place search timed out.', + ); + } catch (e) { + if (e is RouteProviderException) rethrow; + throw RouteProviderException( + 'placeSearchUnavailable', + 'Failed to reach places service: $e', + ); + } + } + + @override + Future resolvePlace(String placeId) async { + final cleanId = placeId.trim(); + if (cleanId.isEmpty) { + throw RouteProviderException( + 'invalidRequest', + 'Place ID cannot be empty.', + ); + } + + final headers = await _getAuthHeaders(); + + try { + final uri = Uri.parse('$backendUrl/api/places/resolve') + .replace(queryParameters: {'placeId': cleanId}); + + final response = await _client + .get(uri, headers: headers) + .timeout(const Duration(seconds: 8)); + + _handleHttpError(response); + + final data = jsonDecode(response.body); + if (data is! Map) { + throw RouteProviderException( + 'placeNotFound', + 'Invalid response from place resolution.', + ); + } + + return ResolvedPlace( + providerId: data['providerId']?.toString() ?? cleanId, + displayName: data['displayName']?.toString() ?? 'Unknown Place', + formattedAddress: data['formattedAddress']?.toString(), + latitude: (data['latitude'] as num).toDouble(), + longitude: (data['longitude'] as num).toDouble(), + attribution: data['attribution']?.toString(), + ); + } on TimeoutException { + throw RouteProviderException( + 'providerTimeout', + 'Place resolution timed out.', + ); + } catch (e) { + if (e is RouteProviderException) rethrow; + throw RouteProviderException( + 'placeSearchUnavailable', + 'Failed to resolve place: $e', + ); + } + } + + @override + Future calculateRoute( + List stops, { + bool avoidHighways = false, + bool avoidTolls = false, + bool avoidFerries = false, + }) async { + final ordered = List.from(stops); + ordered.sort((a, b) => a.order.compareTo(b.order)); + + if (ordered.length < 2) { + throw RouteProviderException( + 'invalidRequest', + 'At least 2 stops are required to calculate a route.', + ); + } + if (ordered.length > 25) { + throw RouteProviderException( + 'invalidRequest', + 'A maximum of 25 stops is allowed.', + ); + } + + final headers = await _getAuthHeaders(); + + final locations = ordered + .map((s) => { + 'lat': s.latitude, + 'lon': s.longitude, + 'type': 'break', + }) + .toList(); + + try { + final uri = Uri.parse('$backendUrl/api/routes/calculate'); + final response = await _client + .post( + uri, + headers: headers, + body: jsonEncode({ + 'locations': locations, + 'options': { + 'avoid_highways': avoidHighways, + 'avoid_tolls': avoidTolls, + 'avoid_ferries': avoidFerries, + } + }), + ) + .timeout(const Duration(seconds: 15)); + + _handleHttpError(response); + + final data = jsonDecode(response.body); + if (data is! Map) { + throw RouteProviderException( + 'routeUnavailable', + 'Malformed response from route calculation.', + ); + } + + final encodedPolyline = data['encodedPolyline'] as String? ?? ''; + final distanceMeters = + (data['distanceMeters'] as num?)?.toDouble() ?? 0.0; + final durationSeconds = + (data['durationSeconds'] as num?)?.toInt(); + final warningsRaw = data['warnings'] as List? ?? []; + + final geometry = ValhallaPolylineDecoder.decode(encodedPolyline); + final straightLineDist = + MotorSignalRoute.calculateTotalDistanceMeters(ordered); + + return RouteCalculationResult( + straightLineDistanceMeters: straightLineDist, + estimatedDistanceMeters: distanceMeters, + estimatedDurationSeconds: durationSeconds, + geometry: geometry, + isStraightLine: false, + providerName: name, + warnings: warningsRaw.map((e) => e.toString()).toList(), + ); + } on TimeoutException { + throw RouteProviderException( + 'providerTimeout', + 'Route calculation timed out.', + ); + } catch (e) { + if (e is RouteProviderException) rethrow; + throw RouteProviderException( + 'routeUnavailable', + 'Failed to calculate route: $e', + ); + } + } +} diff --git a/server/waze_relay/middleware/app_check.js b/server/waze_relay/middleware/app_check.js new file mode 100644 index 0000000..cdfaf2c --- /dev/null +++ b/server/waze_relay/middleware/app_check.js @@ -0,0 +1,37 @@ +const { admin } = require('./firebase_auth'); + +/** + * Express middleware to verify Firebase App Check tokens. + */ +async function requireAppCheck(req, res, next) { + const isEnforced = process.env.ENFORCE_APP_CHECK === 'true'; + const appCheckToken = req.header ? req.header('X-Firebase-AppCheck') : req.headers?.['x-firebase-appcheck']; + + if (!appCheckToken) { + if (isEnforced) { + return res.status(403).json({ + error: 'appCheckFailed', + message: 'Missing X-Firebase-AppCheck header' + }); + } else { + return next(); + } + } + + try { + const appCheckClaims = await admin.appCheck().verifyToken(appCheckToken); + req.appCheck = appCheckClaims; + next(); + } catch (err) { + if (isEnforced) { + return res.status(403).json({ + error: 'appCheckFailed', + message: 'Invalid App Check token' + }); + } else { + return next(); + } + } +} + +module.exports = { requireAppCheck }; diff --git a/server/waze_relay/middleware/firebase_auth.js b/server/waze_relay/middleware/firebase_auth.js new file mode 100644 index 0000000..f969109 --- /dev/null +++ b/server/waze_relay/middleware/firebase_auth.js @@ -0,0 +1,42 @@ +const admin = require('firebase-admin'); + +// Initialize Firebase Admin if not already initialized +try { + if (admin.apps && admin.apps.length === 0) { + admin.initializeApp(); + } +} catch (e) { + console.warn('Firebase admin initialization failed:', e.message); +} + +/** + * Express middleware to verify Firebase ID tokens. + */ +async function requireAuth(req, res, next) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + error: 'authenticationRequired', + message: 'Missing or malformed Authorization header' + }); + } + + const idToken = authHeader.split('Bearer ')[1]; + + try { + const decodedToken = await admin.auth().verifyIdToken(idToken); + req.user = decodedToken; + next(); + } catch (error) { + console.error('Error verifying Firebase ID token:', error.message); + let errorCode = 'invalidToken'; + if (error.code === 'auth/id-token-expired') errorCode = 'expiredToken'; + + return res.status(401).json({ + error: errorCode, + message: 'Invalid or expired Firebase ID token' + }); + } +} + +module.exports = { requireAuth, admin }; diff --git a/server/waze_relay/package-lock.json b/server/waze_relay/package-lock.json index b490db4..244b525 100644 --- a/server/waze_relay/package-lock.json +++ b/server/waze_relay/package-lock.json @@ -9,11 +9,596 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "axios": "^1.20.0", "cors": "^2.8.5", "express": "^4.19.2", + "express-rate-limit": "^8.7.0", + "firebase-admin": "^14.3.0", "puppeteer-core": "^25.9.0" } }, + "node_modules/@fastify/busboy": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.2.tgz", + "integrity": "sha512-yXSS27qPExaXeuLvMRMXOLtpipzfQYNjG3FkunDWKGfMYjKuhFXko9CVzqxm8jcF+lmtS9Fd89QNdh9XDjnbNg==", + "license": "MIT" + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.5.tgz", + "integrity": "sha512-qId34pVZ2CXTmtu4ofW5leiI93DvnOvIcr/5GT7MZPw5WXAi6nZoU+g9cNRgl7K90UJSbK0cXxten4meYMPyZg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-types": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.6.tgz", + "integrity": "sha512-yPLahy7Esfu2w/yme3msVK4xTkDXQqq6szfQn8yVOQpCKiT5GVFjqNpLbuz6NkX0WuTwUidkmPewW3r4xkvpeg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/logger": "0.5.2" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.6.tgz", + "integrity": "sha512-FgwZqDrBqgK0BHI70QTv1v/5wmuDF9f8fsJFvKSxoRlK2PPfSQtZAk2jhXu4pe9BZzFi/e4UYusU/bEQHdf6Qg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/component": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.5.tgz", + "integrity": "sha512-vuFDcL91Q+2ZuBJkyOh86T4q0B4ffNTDjc/A38tybO56odQABxRTFLTIowCWqAKeIcgo37GowWMVgFF73gD8Qw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.5.tgz", + "integrity": "sha512-/JGpvszLoNXNgzilRXocigGfFF4hbcPA9wN1i1kjJx6oKkXgkHteZYl3lQs1lJX3ETDf6bv7zI15lPMVUp4sAQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.5", + "@firebase/auth-interop-types": "0.2.6", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.7.tgz", + "integrity": "sha512-lBq9sJm8MnJINKJnkAKSOj2MbC66xGoSVCcGkPtstl+lkoKEBtD46qHLbuDgW/WbLPoKVm17RIN8BxHj+Z2F2w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/database": "1.1.5", + "@firebase/database-types": "1.0.22", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + }, + "peerDependenciesMeta": { + "@firebase/app": { + "optional": true + }, + "@firebase/app-compat": { + "optional": true + } + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.22.tgz", + "integrity": "sha512-YAZNXsjY9EQQ+pKw/3ax8n5FgolHC7Qew7EY5RceYdl0R2ZP+kCv1O0DkKlcceue8uQCOreHCNN7PWVFpY1Nug==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.6", + "@firebase/util": "1.15.3" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.2.tgz", + "integrity": "sha512-J2VO4NFTc0xQFrxV1B/lm5balicm9cwuX2acR9Yn41fN8KgUeQFo+VJV222IqW2FPSXKDu9uo5WdQFWf9TPbYg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.3.tgz", + "integrity": "sha512-c/z/gaIlaaLZEuGbE6sLUuJ61tskg1JghvhcNQzW948ASBinbVBBRnZTC4b4yt4LaEtJQkYlyzqLHcutFwEIvA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-8.7.1.tgz", + "integrity": "sha512-Hp/WI8sH569ANitsko6RNvCUkzTCYudHoINOkQjiJq2FBG6kKnofBAW7PtS823cu6RMxHqK2RxRcspdjrRpUFA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "fast-deep-equal": "^3.1.3", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^5.0.1", + "protobufjs": "^7.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.22.0.tgz", + "integrity": "sha512-W98gTQOAntEeEQ7/pZxSQXxfUO5CiQcXJRsxBpUa6UU25n8YN/VtogPBi0H+MAmUrn/6bY/sBhFansoWEsr2/g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "<4.1.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^5.3.4", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/storage/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/@grpc/proto-loader/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "optional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT", + "optional": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@nodable/entities": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/@puppeteer/browsers": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.1.tgz", @@ -42,6 +627,99 @@ } } }, + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz", + "integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/request": { + "version": "2.48.13", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz", + "integrity": "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT", + "optional": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -55,6 +733,41 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/ansi-regex": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", @@ -79,12 +792,99 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/body-parser": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", @@ -109,6 +909,22 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -194,6 +1010,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "optional": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -247,6 +1095,30 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "optional": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -256,6 +1128,15 @@ "ms": "2.0.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -295,6 +1176,35 @@ "node": ">= 0.4" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT", + "optional": true + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -316,6 +1226,16 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -346,6 +1266,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -370,6 +1305,16 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.22.2", "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", @@ -416,6 +1361,136 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-rate-limit/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express-rate-limit/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-xml-builder": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", + "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.1.tgz", + "integrity": "sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@nodable/entities": "^3.0.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.2", + "xml-naming": "^0.3.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -434,6 +1509,93 @@ "node": ">= 0.8" } }, + "node_modules/firebase-admin": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-14.3.0.tgz", + "integrity": "sha512-FUxr5HI9C0RNJW3B+CKyJOnIt5uXn/XHheyCECVTLQGSJ/MaE1Zcwf+dCaaG+xx0+VX1A4sZ1t+hc35bu73dVw==", + "license": "Apache-2.0", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@firebase/database-compat": "^2.1.4", + "@firebase/database-types": "^1.0.20", + "fast-deep-equal": "^3.1.1", + "google-auth-library": "^10.6.2", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^4.0.1" + }, + "engines": { + "node": ">=22" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^8.7.1", + "@google-cloud/storage": "^7.22.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "optional": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -461,6 +1623,170 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "license": "MIT", + "optional": true + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/gcp-metadata/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gcp-metadata/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/gcp-metadata/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -519,6 +1845,314 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "optional": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.1.tgz", + "integrity": "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/google-auth-library/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/google-auth-library/node_modules/gaxios": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/google-auth-library/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/google-auth-library/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/google-gax": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.8.tgz", + "integrity": "sha512-M4vpZcXQIC1gqIVGQ7eaU3jXQA6zecStyTXu514TYfThlgSurYJOxHZo9fzU6hAgwPWvuEynAVHWyaIk80VeEA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "duplexify": "^4.1.3", + "google-auth-library": "10.5.0", + "google-logging-utils": "1.1.3", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "3.0.4", + "protobufjs": "^7.5.4", + "retry-request": "^8.0.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/google-gax/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/google-gax/node_modules/gaxios": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "optional": true, + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/google-gax/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/google-gax/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/google-gax/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "optional": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/google-gax/node_modules/retry-request": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.4.tgz", + "integrity": "sha512-pI6/7eabUYkZxamkOq0g0uMxKLLGnjzhefY+vL8bVXag5rto4OU2YBTPytWLuHH7aEKD6fn7kJycQfid0Mwnkw==", + "license": "MIT", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/teeny-request": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.4.tgz", + "integrity": "sha512-R1Cg4Vu0UULeDfHL/kjABLaTW++9yD/B6n2g48y5dJ04hsEaxcfmAqbNDzNsbqAYJyIpZafjklLG9YxRu9uzOg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -531,6 +2165,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -543,6 +2191,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -555,6 +2218,23 @@ "node": ">= 0.4" } }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -575,6 +2255,88 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -593,6 +2355,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", + "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -602,6 +2373,258 @@ "node": ">= 0.10" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unsafe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz", + "integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-4.1.0.tgz", + "integrity": "sha512-sbkByqyATKYJP5F4RXj03N5TUNC0QLTjCAZvwTzC4BwJZ8e0/cWxN8YROnyUth2g1/ONWi4eSFHeu6oYalrc3Q==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.4", + "debug": "^4.3.4", + "jose": "^6.1.3", + "limiter": "^1.1.5", + "lru-cache": "^11.0.0", + "lru-memoizer": "^3.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >= 23.0.0" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lru-memoizer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-3.0.0.tgz", + "integrity": "sha512-m83w/cYXLdUIboKSPxzPAGfYnk+vqeDYXuoSrQRw1q+yVEd8IXhvMufN8Q5TIPe7e2jyX4SRNrDJI2Skw1yznQ==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^11.0.1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -671,6 +2694,32 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -701,6 +2750,47 @@ "node": ">= 0.6" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -710,6 +2800,16 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -734,6 +2834,39 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0", + "optional": true + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -743,12 +2876,99 @@ "node": ">= 0.8" } }, + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC", + "optional": true + }, "node_modules/path-to-regexp": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, + "node_modules/proto3-json-serializer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz", + "integrity": "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "protobufjs": "^7.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -762,6 +2982,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/puppeteer-core": { "version": "25.9.0", "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.9.0.tgz", @@ -819,6 +3048,72 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -845,6 +3140,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", @@ -896,6 +3203,29 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "optional": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", @@ -968,6 +3298,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -977,6 +3320,33 @@ "node": ">= 0.8" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT", + "optional": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", @@ -993,6 +3363,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", @@ -1008,6 +3424,70 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.2.tgz", + "integrity": "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT", + "optional": true + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1017,6 +3497,19 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT", + "optional": true + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1036,6 +3529,12 @@ "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", "license": "MIT" }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1045,6 +3544,13 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT", + "optional": true + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1054,6 +3560,21 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "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).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1063,12 +3584,78 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webdriver-bidi-protocol": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==", "license": "Apache-2.0" }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrap-ansi": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", @@ -1086,6 +3673,86 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -1103,6 +3770,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "optional": true + }, "node_modules/ws": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", @@ -1124,6 +3798,22 @@ } } }, + "node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -1159,6 +3849,19 @@ "node": "^20.19.0 || ^22.12.0 || >=23" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/server/waze_relay/package.json b/server/waze_relay/package.json index d790acc..38bfefc 100644 --- a/server/waze_relay/package.json +++ b/server/waze_relay/package.json @@ -1,21 +1,26 @@ { "name": "motorsignal-waze-relay", "version": "1.0.0", - "description": "High-performance headless relay server for live Waze police, speed trap, and hazard alerts", + "description": "High-performance relay server for live Waze alerts, motorcycle routing, and geocoding", "main": "server.js", "scripts": { "start": "node server.js", - "dev": "node --watch server.js" + "dev": "node --watch server.js", + "test": "node --test test/**/*.test.js" }, "dependencies": { + "axios": "^1.20.0", "cors": "^2.8.5", "express": "^4.19.2", + "express-rate-limit": "^8.7.0", + "firebase-admin": "^14.3.0", "puppeteer-core": "^25.9.0" }, "keywords": [ "waze", - "police-alerts", - "speed-traps", + "routing", + "places", + "valhalla", "motorsignal" ], "author": "MotorSignal", diff --git a/server/waze_relay/routes/places.js b/server/waze_relay/routes/places.js new file mode 100644 index 0000000..a48adb1 --- /dev/null +++ b/server/waze_relay/routes/places.js @@ -0,0 +1,216 @@ +const express = require('express'); +const axios = require('axios'); +const rateLimit = require('express-rate-limit'); +const { requireAuth } = require('../middleware/firebase_auth'); +const { requireAppCheck } = require('../middleware/app_check'); + +const router = express.Router(); + +// Configurable rate limit parameters +const RATE_LIMIT_WINDOW_MS = parseInt(process.env.RATE_LIMIT_WINDOW_MS || '3600000', 10); +const PLACE_SEARCH_RATE_LIMIT_MAX = parseInt( + process.env.PLACE_SEARCH_RATE_LIMIT_MAX || process.env.PLACES_RATE_LIMIT || '200', + 10 +); +const PLACE_RESOLVE_RATE_LIMIT_MAX = parseInt( + process.env.PLACE_RESOLVE_RATE_LIMIT_MAX || '200', + 10 +); + +const searchLimiter = rateLimit({ + windowMs: RATE_LIMIT_WINDOW_MS, + max: PLACE_SEARCH_RATE_LIMIT_MAX, + keyGenerator: (req) => req.user?.uid || req.ip, + validate: { keyGeneratorIpFallback: false }, + handler: (req, res) => + res.status(429).json({ + error: 'providerRateLimited', + message: 'Places search rate limit exceeded' + }) +}); + +const resolveLimiter = rateLimit({ + windowMs: RATE_LIMIT_WINDOW_MS, + max: PLACE_RESOLVE_RATE_LIMIT_MAX, + keyGenerator: (req) => req.user?.uid || req.ip, + validate: { keyGeneratorIpFallback: false }, + handler: (req, res) => + res.status(429).json({ + error: 'providerRateLimited', + message: 'Places resolve rate limit exceeded' + }) +}); + +router.get('/search', requireAppCheck, requireAuth, searchLimiter, async (req, res) => { + const stadiaApiKey = process.env.STADIA_API_KEY; + if (!stadiaApiKey) { + return res.status(503).json({ + error: 'providerUnavailable', + message: 'Places provider is not configured' + }); + } + + const query = req.query.query; + if (!query || typeof query !== 'string') { + return res.status(400).json({ + error: 'invalidRequest', + message: 'Valid query parameter is required' + }); + } + + const clean = query.trim(); + if (clean.length < 2) { + return res.status(400).json({ + error: 'invalidRequest', + message: 'Query must be at least 2 characters' + }); + } + + if (clean.length > 200) { + return res.status(400).json({ + error: 'invalidRequest', + message: 'Query cannot exceed 200 characters' + }); + } + + const params = { + text: clean, + api_key: stadiaApiKey + }; + + if (req.query.lat && req.query.lng) { + const lat = parseFloat(req.query.lat); + const lng = parseFloat(req.query.lng); + if (Number.isFinite(lat) && Number.isFinite(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) { + params['focus.point.lat'] = lat; + params['focus.point.lon'] = lng; + } + } + + try { + const response = await axios.get('https://api.stadiamaps.com/geocoding/v1/autocomplete', { + params, + timeout: 5000 + }); + + const features = response.data?.features || []; + const normalized = []; + + for (const f of features) { + const props = f.properties || {}; + const coords = Array.isArray(f.geometry?.coordinates) ? f.geometry.coordinates : null; + const lng = coords && coords.length >= 2 ? coords[0] : null; + const lat = coords && coords.length >= 2 ? coords[1] : null; + + normalized.push({ + providerId: String(props.id || props.gid || ''), + primaryText: props.name || props.label || '', + secondaryText: [props.region, props.country].filter(Boolean).join(', '), + formattedAddress: props.label || props.name || '', + latitude: typeof lat === 'number' && Number.isFinite(lat) ? lat : null, + longitude: typeof lng === 'number' && Number.isFinite(lng) ? lng : null + }); + } + + res.json({ + results: normalized, + attribution: 'Powered by Stadia Maps, © OpenStreetMap contributors' + }); + } catch (error) { + if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) { + return res.status(504).json({ + error: 'providerTimeout', + message: 'Places search timed out' + }); + } + console.error('[Places] Search upstream error:', error.response?.status || error.message); + res.status(502).json({ + error: 'placeSearchUnavailable', + message: 'Places search is temporarily unavailable' + }); + } +}); + +router.get('/resolve', requireAppCheck, requireAuth, resolveLimiter, async (req, res) => { + const stadiaApiKey = process.env.STADIA_API_KEY; + if (!stadiaApiKey) { + return res.status(503).json({ + error: 'providerUnavailable', + message: 'Places provider is not configured' + }); + } + + const placeId = req.query.placeId; + if (!placeId || typeof placeId !== 'string') { + return res.status(400).json({ + error: 'invalidRequest', + message: 'placeId is required' + }); + } + + const cleanId = placeId.trim(); + if (cleanId.length === 0 || cleanId.length > 200) { + return res.status(400).json({ + error: 'invalidRequest', + message: 'placeId must be between 1 and 200 characters' + }); + } + + try { + const response = await axios.get('https://api.stadiamaps.com/geocoding/v1/place', { + params: { + ids: cleanId, + api_key: stadiaApiKey + }, + timeout: 5000 + }); + + const features = response.data?.features || []; + if (features.length === 0) { + return res.status(404).json({ + error: 'placeNotFound', + message: 'Place could not be resolved' + }); + } + + const feature = features[0]; + const props = feature.properties || {}; + const coords = feature.geometry?.coordinates || []; + + if (!Array.isArray(coords) || coords.length < 2) { + return res.status(404).json({ + error: 'placeNotFound', + message: 'Place has no valid coordinates' + }); + } + + res.json({ + providerId: String(props.id || props.gid || cleanId), + displayName: props.name || props.label || 'Unknown', + formattedAddress: props.label || props.name || '', + latitude: coords[1], + longitude: coords[0], + attribution: 'Powered by Stadia Maps, © OpenStreetMap contributors' + }); + } catch (error) { + if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) { + return res.status(504).json({ + error: 'providerTimeout', + message: 'Place resolution timed out' + }); + } + console.error('[Places] Resolve upstream error:', error.response?.status || error.message); + if (error.response?.status === 404) { + return res.status(404).json({ + error: 'placeNotFound', + message: 'Place not found' + }); + } + res.status(502).json({ + error: 'placeSearchUnavailable', + message: 'Places service is temporarily unavailable' + }); + } +}); + +module.exports = router; diff --git a/server/waze_relay/routes/routing.js b/server/waze_relay/routes/routing.js new file mode 100644 index 0000000..6e89c77 --- /dev/null +++ b/server/waze_relay/routes/routing.js @@ -0,0 +1,173 @@ +const express = require('express'); +const axios = require('axios'); +const rateLimit = require('express-rate-limit'); +const { requireAuth } = require('../middleware/firebase_auth'); +const { requireAppCheck } = require('../middleware/app_check'); + +const router = express.Router(); + +// Configurable rate limit parameters +const RATE_LIMIT_WINDOW_MS = parseInt(process.env.RATE_LIMIT_WINDOW_MS || '3600000', 10); +const ROUTE_RATE_LIMIT_MAX = parseInt( + process.env.ROUTE_RATE_LIMIT_MAX || process.env.ROUTE_RATE_LIMIT || '50', + 10 +); + +// Rate limiter: per user UID (with IP fallback) +const routeLimiter = rateLimit({ + windowMs: RATE_LIMIT_WINDOW_MS, + max: ROUTE_RATE_LIMIT_MAX, + keyGenerator: (req) => req.user?.uid || req.ip, + validate: { keyGeneratorIpFallback: false }, + handler: (req, res) => { + res.status(429).json({ + error: 'providerRateLimited', + message: 'Route calculation rate limit exceeded' + }); + } +}); + +router.post('/calculate', requireAppCheck, requireAuth, routeLimiter, async (req, res) => { + const stadiaApiKey = process.env.STADIA_API_KEY; + if (!stadiaApiKey) { + console.error('[Routing] STADIA_API_KEY is not configured'); + return res.status(503).json({ + error: 'providerUnavailable', + message: 'Routing provider is not configured' + }); + } + + const { locations, options } = req.body || {}; + + if (!Array.isArray(locations) || locations.length < 2) { + return res.status(400).json({ + error: 'invalidRequest', + message: 'At least 2 locations are required' + }); + } + + if (locations.length > 25) { + return res.status(400).json({ + error: 'invalidRequest', + message: 'Maximum of 25 locations allowed' + }); + } + + // Validate and sanitize each coordinate location + const valhallaLocations = []; + for (let i = 0; i < locations.length; i++) { + const loc = locations[i]; + if (!loc || typeof loc !== 'object') { + return res.status(400).json({ + error: 'invalidRequest', + message: `Location at index ${i} must be an object` + }); + } + + const lat = loc.lat; + const lon = loc.lon ?? loc.lng; + + if (typeof lat !== 'number' || typeof lon !== 'number' || !Number.isFinite(lat) || !Number.isFinite(lon)) { + return res.status(400).json({ + error: 'invalidRequest', + message: `Invalid or non-finite coordinates at index ${i}` + }); + } + + if (lat < -90 || lat > 90 || lon < -180 || lon > 180) { + return res.status(400).json({ + error: 'invalidRequest', + message: `Coordinates out of geographic bounds at index ${i}` + }); + } + + // Type is 'break' for origin, destination, and physical stops + let type = 'break'; + if (i > 0 && i < locations.length - 1 && loc.type === 'through') { + type = 'through'; + } + + valhallaLocations.push({ lat, lon, type }); + } + + // Strictly enforce motorcycle profile server-side; do not accept arbitrary costing + const payload = { + locations: valhallaLocations, + costing: 'motorcycle', + directions_options: { + units: 'miles' + } + }; + + // Optional avoidance options + if (options && typeof options === 'object') { + const costingOptions = {}; + if (options.avoid_highways === true) costingOptions.use_highways = 0; + if (options.avoid_tolls === true) costingOptions.use_tolls = 0; + if (options.avoid_ferries === true) costingOptions.use_ferry = 0; + + if (Object.keys(costingOptions).length > 0) { + payload.costing_options = { motorcycle: costingOptions }; + } + } + + try { + const response = await axios.post('https://api.stadiamaps.com/route/v1', payload, { + params: { api_key: stadiaApiKey }, + timeout: 10000 + }); + + const data = response.data; + if (!data || !data.trip || !Array.isArray(data.trip.legs) || data.trip.legs.length === 0) { + return res.status(404).json({ + error: 'routeNotFound', + message: 'No route could be calculated' + }); + } + + let totalDistanceMeters = 0; + let totalDurationSeconds = 0; + const polylineParts = []; + + for (const leg of data.trip.legs) { + if (leg.summary) { + totalDistanceMeters += (Number(leg.summary.length) || 0) * 1000; + totalDurationSeconds += Math.round(Number(leg.summary.time) || 0); + } + if (leg.shape) { + polylineParts.push(leg.shape); + } + } + + res.json({ + provider: 'valhalla_stadia', + encodedPolyline: polylineParts.join(''), + distanceMeters: Math.round(totalDistanceMeters * 10) / 10, + durationSeconds: totalDurationSeconds, + warnings: Array.isArray(data.trip.warnings) ? data.trip.warnings : [], + attribution: '© Stadia Maps, © OpenStreetMap contributors' + }); + } catch (error) { + if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) { + return res.status(504).json({ + error: 'providerTimeout', + message: 'Routing provider timed out' + }); + } + + if (error.response?.status === 400 || error.response?.status === 404) { + return res.status(404).json({ + error: 'routeNotFound', + message: 'No road route could be found between the specified locations' + }); + } + + console.error('[Routing] Upstream error occurred:', error.response?.status || error.message); + res.status(502).json({ + error: 'providerUnavailable', + message: 'Routing service is temporarily unavailable' + }); + } +}); + +module.exports = router; diff --git a/server/waze_relay/routes/waze.js b/server/waze_relay/routes/waze.js new file mode 100644 index 0000000..8f626c8 --- /dev/null +++ b/server/waze_relay/routes/waze.js @@ -0,0 +1,251 @@ +const express = require('express'); +const https = require('https'); +const router = express.Router(); + +const cache = new Map(); +const CACHE_TTL_MS = 30 * 1000; + +const BROWSER_USER_AGENTS = [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36' +]; + +function getRandomUserAgent() { + return BROWSER_USER_AGENTS[Math.floor(Math.random() * BROWSER_USER_AGENTS.length)]; +} + +function getWazeEnv(lat, lng) { + if (lat >= 29.0 && lat <= 33.5 && lng >= 34.0 && lng <= 36.0) { + return 'il'; + } + if (lng >= -170.0 && lng <= -50.0) { + return 'na'; + } + return 'row'; +} + +function fetchWazeAlerts(top, bottom, left, right, env = 'na') { + return new Promise((resolve, reject) => { + const query = new URLSearchParams({ + top: top.toString(), + bottom: bottom.toString(), + left: left.toString(), + right: right.toString(), + env: env, + types: 'alerts' + }); + + const req = https.request({ + hostname: 'www.waze.com', + port: 443, + path: `/rtserver/web/TGeoRSS?${query.toString()}`, + method: 'GET', + headers: { + 'User-Agent': getRandomUserAgent(), + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'en-US,en;q=0.9', + 'Referer': 'https://www.waze.com/live-map/', + 'X-Requested-With': 'XMLHttpRequest', + 'Sec-Ch-Ua': '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"', + 'Sec-Ch-Ua-Mobile': '?0', + 'Sec-Ch-Ua-Platform': '"Windows"', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'Connection': 'keep-alive' + }, + timeout: 8000 + }, (res) => { + let body = ''; + res.on('data', chunk => body += chunk); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + try { + const data = JSON.parse(body); + resolve(data); + } catch (e) { + reject(new Error(`Failed to parse Waze JSON: ${e.message}`)); + } + } else { + reject(new Error(`Waze responded with HTTP ${res.statusCode}: ${body.slice(0, 150)}`)); + } + }); + }); + + req.on('timeout', () => { + req.destroy(); + reject(new Error('Waze request timed out')); + }); + + req.on('error', (err) => { + reject(err); + }); + + req.end(); + }); +} + +function normalizeAlerts(wazeData) { + const rawAlerts = wazeData.alerts || []; + const normalized = []; + + for (const item of rawAlerts) { + const lat = item.location?.y ?? item.latitude ?? item.lat; + const lng = item.location?.x ?? item.longitude ?? item.lng; + if (lat == null || lng == null) continue; + + const type = (item.type || 'HAZARD').toUpperCase(); + const subtype = (item.subtype || item.subType || '').toUpperCase(); + const street = item.street || item.road || ''; + const city = item.city || ''; + + let category = 'other'; + let description = item.reportDescription || ''; + + if (type.includes('POLICE') || subtype.includes('POLICE')) { + category = 'police'; + if (subtype.includes('HIDDEN')) { + description = description || 'Hidden police speed trap reported'; + } else if (subtype.includes('VISIBLE')) { + description = description || 'Police speed trap reported'; + } else { + description = description || 'Police presence reported'; + } + } else if (type.includes('SPEED_CAMERA') || subtype.includes('SPEED_CAMERA') || type.includes('CAMERA')) { + category = 'speed_camera'; + description = description || 'Speed enforcement camera'; + } else if (type.includes('RED_LIGHT_CAMERA') || subtype.includes('RED_LIGHT_CAMERA')) { + category = 'speed_camera'; + description = description || 'Red light camera'; + } else if (type.includes('ACCIDENT') || subtype.includes('ACCIDENT')) { + category = 'accident'; + description = description || (subtype.includes('MAJOR') ? 'Major collision' : 'Accident reported'); + } else if (type.includes('HAZARD') || subtype.includes('HAZARD') || subtype.includes('WEATHER') || subtype.includes('DEBRIS')) { + category = 'hazard'; + description = description || (subtype.includes('POT_HOLE') ? 'Pothole on roadway' : 'Road hazard / debris'); + } else if (type.includes('ROAD_CLOSED') || subtype.includes('ROAD_CLOSED')) { + category = 'road_closure'; + description = description || 'Road closed'; + } else if (type.includes('CONSTRUCTION') || subtype.includes('CONSTRUCTION')) { + category = 'construction'; + description = description || 'Road work / Construction'; + } else if (type.includes('JAM') || type.includes('TRAFFIC')) { + category = 'traffic'; + description = description || 'Heavy traffic congestion'; + } + + if (street) { + description += ` on ${street}`; + } + + normalized.push({ + id: item.uuid || item.id || `wz_${lat.toFixed(5)}_${lng.toFixed(5)}`, + providerId: 'community_traffic', + source: 'Waze Community Relay', + type: type, + subtype: subtype, + category: category, + latitude: parseFloat(lat), + longitude: parseFloat(lng), + heading: item.magvar || item.heading || null, + roadName: street, + city: city, + description: description, + confidence: (item.confidence != null ? item.confidence / 10.0 : (item.reportRating != null ? item.reportRating / 5.0 : 0.85)), + severity: item.reportRating >= 4 ? 'high' : (item.reportRating >= 2 ? 'medium' : 'low'), + reliability: item.reliability || 5, + reportRating: item.reportRating || 3, + nThumbsUp: item.nThumbsUp || 0, + pubMillis: item.pubMillis || Date.now(), + reportedAt: new Date(item.pubMillis || Date.now()).toISOString() + }); + } + + return normalized; +} + +router.get('/', async (req, res) => { + try { + let top, bottom, left, right, lat, lng; + + if (req.query.top && req.query.bottom && req.query.left && req.query.right) { + top = parseFloat(req.query.top); + bottom = parseFloat(req.query.bottom); + left = parseFloat(req.query.left); + right = parseFloat(req.query.right); + lat = (top + bottom) / 2; + lng = (left + right) / 2; + } else if (req.query.lat && req.query.lng) { + lat = parseFloat(req.query.lat); + lng = parseFloat(req.query.lng); + const radiusMeters = parseFloat(req.query.radius || 5000); + + const latDelta = radiusMeters / 111000.0; + const lngDelta = radiusMeters / (111000.0 * Math.cos(lat * Math.PI / 180.0)); + + top = lat + latDelta; + bottom = lat - latDelta; + left = lng - lngDelta; + right = lng + lngDelta; + } else { + lat = 37.7749; + lng = -122.4194; + top = 37.82; + bottom = 37.72; + left = -122.48; + right = -122.36; + } + + const env = req.query.env || getWazeEnv(lat, lng); + const cacheKey = `${lat.toFixed(2)}_${lng.toFixed(2)}_${env}`; + const cached = cache.get(cacheKey); + + if (cached && (Date.now() - cached.time < CACHE_TTL_MS)) { + return res.json({ + provider: 'waze_relay', + cached: true, + count: cached.alerts.length, + alerts: cached.alerts + }); + } + + try { + const wazeRaw = await fetchWazeAlerts(top, bottom, left, right, env); + const normalizedAlerts = normalizeAlerts(wazeRaw); + + cache.set(cacheKey, { time: Date.now(), alerts: normalizedAlerts }); + + return res.json({ + provider: 'waze_relay', + cached: false, + count: normalizedAlerts.length, + alerts: normalizedAlerts + }); + } catch (fetchErr) { + console.warn(`[WazeRelay] Warning: ${fetchErr.message}`); + if (cached) { + return res.json({ + provider: 'waze_relay', + cached: true, + stale: true, + count: cached.alerts.length, + alerts: cached.alerts + }); + } + + return res.json({ + provider: 'waze_relay', + status: 'partial', + message: fetchErr.message, + count: 0, + alerts: [] + }); + } + } catch (err) { + console.error('[WazeRelay] Server Error:', err); + res.status(500).json({ error: err.message, alerts: [] }); + } +}); + +module.exports = router; diff --git a/server/waze_relay/server.js b/server/waze_relay/server.js index dd7ae11..f39ad70 100644 --- a/server/waze_relay/server.js +++ b/server/waze_relay/server.js @@ -1,301 +1,64 @@ -const http = require('http'); -const https = require('https'); const express = require('express'); const cors = require('cors'); +const wazeRoutes = require('./routes/waze'); +const routingRoutes = require('./routes/routing'); +const placesRoutes = require('./routes/places'); + const app = express(); const PORT = process.env.PORT || 3000; +// Standard middleware app.use(cors()); app.use(express.json()); +app.set('trust proxy', 1); -// In-memory cache for Waze requests (30s TTL per geo tile) -const cache = new Map(); -const CACHE_TTL_MS = 30 * 1000; - -// User agents and headers mimicking genuine browser requests -const BROWSER_USER_AGENTS = [ - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36' -]; - -function getRandomUserAgent() { - return BROWSER_USER_AGENTS[Math.floor(Math.random() * BROWSER_USER_AGENTS.length)]; -} - -/** - * Determine Waze server environment based on longitude - * na: North America (Western Hemisphere approx) - * il: Israel - * row: Rest of World - */ -function getWazeEnv(lat, lng) { - if (lat >= 29.0 && lat <= 33.5 && lng >= 34.0 && lng <= 36.0) { - return 'il'; - } - if (lng >= -170.0 && lng <= -50.0) { - return 'na'; - } - return 'row'; -} - -/** - * Fetch raw GeoRSS alerts from Waze with browser impersonation - */ -function fetchWazeAlerts(top, bottom, left, right, env = 'na') { - return new Promise((resolve, reject) => { - const query = new URLSearchParams({ - top: top.toString(), - bottom: bottom.toString(), - left: left.toString(), - right: right.toString(), - env: env, - types: 'alerts,traffic,jams' - }); - - const url = `https://www.waze.com/live-map/api/georss?${query.toString()}`; - const parsed = new URL(url); - - const req = https.request({ - hostname: parsed.hostname, - path: parsed.pathname + parsed.search, - method: 'GET', - headers: { - 'User-Agent': getRandomUserAgent(), - 'Accept': 'application/json, text/plain, */*', - 'Accept-Language': 'en-US,en;q=0.9', - 'Referer': 'https://www.waze.com/live-map/', - 'Sec-Ch-Ua': '"Chromium";v="124", "Google Chrome";v="124"', - 'Sec-Ch-Ua-Mobile': '?0', - 'Sec-Ch-Ua-Platform': '"Windows"', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - 'Connection': 'keep-alive' - }, - timeout: 8000 - }, (res) => { - let body = ''; - res.on('data', chunk => body += chunk); - res.on('end', () => { - if (res.statusCode >= 200 && res.statusCode < 300) { - try { - const data = JSON.parse(body); - resolve(data); - } catch (e) { - reject(new Error(`Failed to parse Waze JSON: ${e.message}`)); - } - } else { - reject(new Error(`Waze responded with HTTP ${res.statusCode}: ${body.slice(0, 150)}`)); - } - }); - }); - - req.on('timeout', () => { - req.destroy(); - reject(new Error('Waze request timed out')); - }); - - req.on('error', (err) => { - reject(err); - }); - - req.end(); - }); -} - -/** - * Normalize and format Waze raw alerts into MotorSignal Community Schema - */ -function normalizeAlerts(wazeData) { - const rawAlerts = wazeData.alerts || []; - const normalized = []; - - for (const item of rawAlerts) { - const lat = item.location?.y ?? item.latitude ?? item.lat; - const lng = item.location?.x ?? item.longitude ?? item.lng; - if (lat == null || lng == null) continue; - - const type = (item.type || 'HAZARD').toUpperCase(); - const subtype = (item.subtype || item.subType || '').toUpperCase(); - const street = item.street || item.road || ''; - const city = item.city || ''; - - let category = 'other'; - let description = item.reportDescription || ''; - - if (type.includes('POLICE') || subtype.includes('POLICE')) { - category = 'police'; - if (subtype.includes('HIDDEN')) { - description = description || 'Hidden police speed trap reported'; - } else if (subtype.includes('VISIBLE')) { - description = description || 'Police speed trap reported'; - } else { - description = description || 'Police presence reported'; - } - } else if (type.includes('SPEED_CAMERA') || subtype.includes('SPEED_CAMERA') || type.includes('CAMERA')) { - category = 'speed_camera'; - description = description || 'Speed enforcement camera'; - } else if (type.includes('RED_LIGHT_CAMERA') || subtype.includes('RED_LIGHT_CAMERA')) { - category = 'speed_camera'; - description = description || 'Red light camera'; - } else if (type.includes('ACCIDENT') || subtype.includes('ACCIDENT')) { - category = 'accident'; - description = description || (subtype.includes('MAJOR') ? 'Major collision' : 'Accident reported'); - } else if (type.includes('HAZARD') || subtype.includes('HAZARD') || subtype.includes('WEATHER') || subtype.includes('DEBRIS')) { - category = 'hazard'; - description = description || (subtype.includes('POT_HOLE') ? 'Pothole on roadway' : 'Road hazard / debris'); - } else if (type.includes('ROAD_CLOSED') || subtype.includes('ROAD_CLOSED')) { - category = 'road_closure'; - description = description || 'Road closed'; - } else if (type.includes('CONSTRUCTION') || subtype.includes('CONSTRUCTION')) { - category = 'construction'; - description = description || 'Road work / Construction'; - } else if (type.includes('JAM') || type.includes('TRAFFIC')) { - category = 'traffic'; - description = description || 'Heavy traffic congestion'; - } - - if (street) { - description += ` on ${street}`; - } - - normalized.push({ - id: item.uuid || item.id || `wz_${lat.toFixed(5)}_${lng.toFixed(5)}`, - providerId: 'community_traffic', - source: 'Waze Community Relay', - type: type, - subtype: subtype, - category: category, - latitude: parseFloat(lat), - longitude: parseFloat(lng), - heading: item.magvar || item.heading || null, - roadName: street, - city: city, - description: description, - confidence: (item.confidence != null ? item.confidence / 10.0 : (item.reportRating != null ? item.reportRating / 5.0 : 0.85)), - severity: item.reportRating >= 4 ? 'high' : (item.reportRating >= 2 ? 'medium' : 'low'), - reliability: item.reliability || 5, - reportRating: item.reportRating || 3, - nThumbsUp: item.nThumbsUp || 0, - pubMillis: item.pubMillis || Date.now(), - reportedAt: new Date(item.pubMillis || Date.now()).toISOString() - }); - } - - return normalized; -} - -// ------------------------------------------------------------- -// REST API ROUTES -// ------------------------------------------------------------- - -/** - * Health check & discovery endpoint (Supports MotorSignal TEST CONNECTION button) - */ +// Health check & discovery endpoint (Preserves legacy contract for MotorSignal TEST CONNECTION) app.get(['/', '/health', '/test'], (req, res) => { res.json({ status: 'ok', service: 'MotorSignal Waze Community Relay', - version: '1.0.0', + version: '1.1.0', timestamp: new Date().toISOString(), uptimeSeconds: Math.floor(process.uptime()), - features: ['police', 'speed_cameras', 'hazards', 'accidents', 'traffic'] + features: [ + 'police', + 'speed_cameras', + 'hazards', + 'accidents', + 'traffic', + 'routing', + 'places' + ] }); }); -/** - * Main alerts endpoint - */ -app.get(['/alerts', '/api/alerts'], async (req, res) => { - try { - let top, bottom, left, right, lat, lng; +// Mount Waze routes on legacy and modern endpoints +app.use('/alerts', wazeRoutes); +app.use('/api/alerts', wazeRoutes); +app.use('/api/waze', wazeRoutes); - if (req.query.top && req.query.bottom && req.query.left && req.query.right) { - top = parseFloat(req.query.top); - bottom = parseFloat(req.query.bottom); - left = parseFloat(req.query.left); - right = parseFloat(req.query.right); - lat = (top + bottom) / 2; - lng = (left + right) / 2; - } else if (req.query.lat && req.query.lng) { - lat = parseFloat(req.query.lat); - lng = parseFloat(req.query.lng); - const radiusMeters = parseFloat(req.query.radius || 5000); +// Mount routing and places endpoints +app.use('/api/routes', routingRoutes); +app.use('/api/places', placesRoutes); - const latDelta = radiusMeters / 111000.0; - const lngDelta = radiusMeters / (111000.0 * Math.cos(lat * Math.PI / 180.0)); - - top = lat + latDelta; - bottom = lat - latDelta; - left = lng - lngDelta; - right = lng + lngDelta; - } else { - lat = 37.7749; - lng = -122.4194; - top = 37.82; - bottom = 37.72; - left = -122.48; - right = -122.36; - } - - const env = req.query.env || getWazeEnv(lat, lng); - const cacheKey = `${lat.toFixed(2)}_${lng.toFixed(2)}_${env}`; - const cached = cache.get(cacheKey); - - if (cached && (Date.now() - cached.time < CACHE_TTL_MS)) { - return res.json({ - provider: 'waze_relay', - cached: true, - count: cached.alerts.length, - alerts: cached.alerts - }); - } - - try { - const wazeRaw = await fetchWazeAlerts(top, bottom, left, right, env); - const normalizedAlerts = normalizeAlerts(wazeRaw); - - cache.set(cacheKey, { time: Date.now(), alerts: normalizedAlerts }); - - return res.json({ - provider: 'waze_relay', - cached: false, - count: normalizedAlerts.length, - alerts: normalizedAlerts - }); - } catch (fetchErr) { - console.warn(`[WazeRelay] Warning: ${fetchErr.message}`); - if (cached) { - return res.json({ - provider: 'waze_relay', - cached: true, - stale: true, - count: cached.alerts.length, - alerts: cached.alerts - }); - } - - return res.json({ - provider: 'waze_relay', - status: 'partial', - message: fetchErr.message, - count: 0, - alerts: [] - }); - } - } catch (err) { - console.error('[WazeRelay] Server Error:', err); - res.status(500).json({ error: err.message, alerts: [] }); - } +// Central error handler +app.use((err, req, res, next) => { + console.error('[Relay] Unhandled error:', err); + res.status(500).json({ error: 'internalError', message: 'An internal server error occurred' }); }); -app.listen(PORT, '0.0.0.0', () => { - console.log(`====================================================`); - console.log(` 🏍️ MotorSignal Waze Alert Relay Server running`); - console.log(` 📡 Listening on: http://0.0.0.0:${PORT}`); - console.log(` 🏥 Healthcheck: http://localhost:${PORT}/health`); - console.log(` 🚨 Alerts API: http://localhost:${PORT}/alerts?lat=37.77&lng=-122.42`); - console.log(`====================================================`); -}); +if (require.main === module) { + app.listen(PORT, '0.0.0.0', () => { + console.log(`====================================================`); + console.log(` 🏍️ MotorSignal API & Waze Alert Relay Server running`); + console.log(` 📡 Listening on: http://0.0.0.0:${PORT}`); + console.log(` 🏥 Healthcheck: http://localhost:${PORT}/health`); + console.log(` 🚨 Alerts API: http://localhost:${PORT}/alerts?lat=37.77&lng=-122.42`); + console.log(` 🗺️ Routing API: http://localhost:${PORT}/api/routes/calculate`); + console.log(` 📍 Places API: http://localhost:${PORT}/api/places/search`); + console.log(`====================================================`); + }); +} + +module.exports = app; diff --git a/server/waze_relay/test/auth_and_appcheck.test.js b/server/waze_relay/test/auth_and_appcheck.test.js new file mode 100644 index 0000000..65e2f0c --- /dev/null +++ b/server/waze_relay/test/auth_and_appcheck.test.js @@ -0,0 +1,206 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert'); +const { requireAuth, admin } = require('../middleware/firebase_auth'); +const { requireAppCheck } = require('../middleware/app_check'); + +describe('Auth Middleware', () => { + it('rejects missing Authorization header with 401 authenticationRequired', async () => { + const req = { headers: {} }; + let statusCode = null; + let responseBody = null; + const res = { + status: (c) => { + statusCode = c; + return { + json: (b) => { + responseBody = b; + } + }; + } + }; + let nextCalled = false; + await requireAuth(req, res, () => { + nextCalled = true; + }); + + assert.strictEqual(nextCalled, false); + assert.strictEqual(statusCode, 401); + assert.strictEqual(responseBody?.error, 'authenticationRequired'); + }); + + it('rejects malformed Authorization header with 401 authenticationRequired', async () => { + const req = { headers: { authorization: 'Basic 12345' } }; + let statusCode = null; + let responseBody = null; + const res = { + status: (c) => { + statusCode = c; + return { + json: (b) => { + responseBody = b; + } + }; + } + }; + let nextCalled = false; + await requireAuth(req, res, () => { + nextCalled = true; + }); + + assert.strictEqual(nextCalled, false); + assert.strictEqual(statusCode, 401); + assert.strictEqual(responseBody?.error, 'authenticationRequired'); + }); + + it('rejects empty Bearer token with 401 invalidToken', async () => { + const req = { headers: { authorization: 'Bearer ' } }; + let statusCode = null; + let responseBody = null; + const res = { + status: (c) => { + statusCode = c; + return { + json: (b) => { + responseBody = b; + } + }; + } + }; + let nextCalled = false; + await requireAuth(req, res, () => { + nextCalled = true; + }); + + assert.strictEqual(nextCalled, false); + assert.strictEqual(statusCode, 401); + assert.strictEqual(responseBody?.error, 'invalidToken'); + }); + + it('successfully attaches verified UID from decoded ID token', async () => { + const originalAuth = admin.auth; + try { + // Mock verifyIdToken + admin.auth = () => ({ + verifyIdToken: async (token) => { + if (token === 'valid-token') { + return { uid: 'rider_alice_123', email: 'alice@example.com' }; + } + throw new Error('Invalid token'); + } + }); + + const req = { headers: { authorization: 'Bearer valid-token' } }; + const res = { status: () => ({ json: () => {} }) }; + let nextCalled = false; + + await requireAuth(req, res, () => { + nextCalled = true; + }); + + assert.strictEqual(nextCalled, true); + assert.strictEqual(req.user?.uid, 'rider_alice_123'); + assert.strictEqual(req.user?.email, 'alice@example.com'); + } finally { + admin.auth = originalAuth; + } + }); +}); + +describe('App Check Middleware', () => { + it('allows request when enforcement is disabled and token is missing', async () => { + const req = { header: () => undefined }; + let nextCalled = false; + const res = { + status: () => ({ json: () => {} }) + }; + await requireAppCheck(req, res, () => { + nextCalled = true; + }); + assert.strictEqual(nextCalled, true); + }); + + it('fails closed when enforcement is enabled and token is missing', async () => { + process.env.ENFORCE_APP_CHECK = 'true'; + try { + const req = { header: () => undefined }; + let statusCode = null; + let responseBody = null; + const res = { + status: (c) => { + statusCode = c; + return { + json: (b) => { + responseBody = b; + } + }; + } + }; + let nextCalled = false; + await requireAppCheck(req, res, () => { + nextCalled = true; + }); + assert.strictEqual(nextCalled, false); + assert.strictEqual(statusCode, 403); + assert.strictEqual(responseBody?.error, 'appCheckFailed'); + } finally { + delete process.env.ENFORCE_APP_CHECK; + } + }); + + it('fails closed when enforcement is enabled and token is invalid', async () => { + process.env.ENFORCE_APP_CHECK = 'true'; + try { + const req = { header: (h) => (h === 'X-Firebase-AppCheck' ? 'invalid-token' : undefined) }; + let statusCode = null; + let responseBody = null; + const res = { + status: (c) => { + statusCode = c; + return { + json: (b) => { + responseBody = b; + } + }; + } + }; + let nextCalled = false; + await requireAppCheck(req, res, () => { + nextCalled = true; + }); + assert.strictEqual(nextCalled, false); + assert.strictEqual(statusCode, 403); + assert.strictEqual(responseBody?.error, 'appCheckFailed'); + } finally { + delete process.env.ENFORCE_APP_CHECK; + } + }); + + it('allows valid App Check token when enforcement is enabled', async () => { + process.env.ENFORCE_APP_CHECK = 'true'; + const originalAppCheck = admin.appCheck; + try { + admin.appCheck = () => ({ + verifyToken: async (token) => { + if (token === 'valid-app-check-token') { + return { appId: 'motorsignal-app-id', token: 'valid-app-check-token' }; + } + throw new Error('Bad token'); + } + }); + + const req = { header: (h) => (h === 'X-Firebase-AppCheck' ? 'valid-app-check-token' : undefined) }; + const res = { status: () => ({ json: () => {} }) }; + let nextCalled = false; + + await requireAppCheck(req, res, () => { + nextCalled = true; + }); + + assert.strictEqual(nextCalled, true); + assert.strictEqual(req.appCheck?.appId, 'motorsignal-app-id'); + } finally { + admin.appCheck = originalAppCheck; + delete process.env.ENFORCE_APP_CHECK; + } + }); +}); diff --git a/server/waze_relay/test/routing_and_places.test.js b/server/waze_relay/test/routing_and_places.test.js new file mode 100644 index 0000000..4da4c21 --- /dev/null +++ b/server/waze_relay/test/routing_and_places.test.js @@ -0,0 +1,386 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert'); +const axios = require('axios'); +const routingRouter = require('../routes/routing'); +const placesRouter = require('../routes/places'); + +describe('Routing Input Hardening & Validations', () => { + const calcLayer = routingRouter.stack.find((l) => l.route && l.route.path === '/calculate'); + assert.ok(calcLayer, '/calculate endpoint must exist'); + const calcHandler = calcLayer.route.stack[calcLayer.route.stack.length - 1].handle; + + it('rejects less than 2 stops with 400 invalidRequest', async () => { + process.env.STADIA_API_KEY = 'test-key'; + try { + const req = { body: { locations: [{ lat: 37.77, lon: -122.42 }] } }; + let statusCode = null; + let body = null; + const res = { + status: (c) => { + statusCode = c; + return { json: (b) => { body = b; } }; + } + }; + await calcHandler(req, res); + assert.strictEqual(statusCode, 400); + assert.strictEqual(body?.error, 'invalidRequest'); + assert.match(body?.message, /At least 2 locations/); + } finally { + delete process.env.STADIA_API_KEY; + } + }); + + it('rejects more than 25 stops with 400 invalidRequest', async () => { + process.env.STADIA_API_KEY = 'test-key'; + try { + const stops = Array.from({ length: 26 }, (_, i) => ({ + lat: 37.0 + i * 0.01, + lon: -122.0 + })); + const req = { body: { locations: stops } }; + let statusCode = null; + let body = null; + const res = { + status: (c) => { + statusCode = c; + return { json: (b) => { body = b; } }; + } + }; + await calcHandler(req, res); + assert.strictEqual(statusCode, 400); + assert.strictEqual(body?.error, 'invalidRequest'); + assert.match(body?.message, /Maximum of 25/); + } finally { + delete process.env.STADIA_API_KEY; + } + }); + + it('accepts exactly 25 stops and passes motorcycle costing to provider', async () => { + process.env.STADIA_API_KEY = 'test-secret-key-123'; + const originalPost = axios.post; + try { + let interceptedUrl = null; + let interceptedPayload = null; + let interceptedParams = null; + + axios.post = async (url, payload, config) => { + interceptedUrl = url; + interceptedPayload = payload; + interceptedParams = config?.params; + return { + data: { + trip: { + legs: [ + { + summary: { length: 15.5, time: 1200 }, + shape: 'test_encoded_polyline' + } + ], + warnings: [] + } + } + }; + }; + + const stops = Array.from({ length: 25 }, (_, i) => ({ + lat: 37.0 + i * 0.01, + lon: -122.0 + })); + + const req = { body: { locations: stops } }; + let responseData = null; + const res = { + json: (b) => { responseData = b; } + }; + + await calcHandler(req, res); + + assert.strictEqual(interceptedUrl, 'https://api.stadiamaps.com/route/v1'); + assert.strictEqual(interceptedParams?.api_key, 'test-secret-key-123'); + assert.strictEqual(interceptedPayload?.costing, 'motorcycle'); + assert.strictEqual(interceptedPayload?.locations?.length, 25); + assert.strictEqual(responseData?.distanceMeters, 15500); + assert.strictEqual(responseData?.durationSeconds, 1200); + assert.strictEqual(responseData?.encodedPolyline, 'test_encoded_polyline'); + // Verify API key is NOT leaked in client response + assert.strictEqual(JSON.stringify(responseData).includes('test-secret-key-123'), false); + } finally { + axios.post = originalPost; + delete process.env.STADIA_API_KEY; + } + }); + + it('sanitizes upstream timeout error to 504 providerTimeout without leaking internals', async () => { + process.env.STADIA_API_KEY = 'test-key'; + const originalPost = axios.post; + try { + axios.post = async () => { + const err = new Error('connect ECONNABORTED'); + err.code = 'ECONNABORTED'; + throw err; + }; + + const req = { + body: { + locations: [ + { lat: 37.7, lon: -122.4 }, + { lat: 37.8, lon: -122.4 } + ] + } + }; + let statusCode = null; + let body = null; + const res = { + status: (c) => { + statusCode = c; + return { json: (b) => { body = b; } }; + } + }; + + await calcHandler(req, res); + assert.strictEqual(statusCode, 504); + assert.strictEqual(body?.error, 'providerTimeout'); + assert.strictEqual(JSON.stringify(body).includes('ECONNABORTED'), false); + } finally { + axios.post = originalPost; + delete process.env.STADIA_API_KEY; + } + }); + + it('rejects invalid latitude bounds (< -90 or > 90)', async () => { + process.env.STADIA_API_KEY = 'test-key'; + try { + const req = { + body: { + locations: [ + { lat: 95.0, lon: -122.4 }, + { lat: 37.7, lon: -122.4 } + ] + } + }; + let statusCode = null; + let body = null; + const res = { + status: (c) => { + statusCode = c; + return { json: (b) => { body = b; } }; + } + }; + await calcHandler(req, res); + assert.strictEqual(statusCode, 400); + assert.strictEqual(body?.error, 'invalidRequest'); + assert.match(body?.message, /out of geographic bounds/); + } finally { + delete process.env.STADIA_API_KEY; + } + }); + + it('rejects invalid longitude bounds (< -180 or > 180)', async () => { + process.env.STADIA_API_KEY = 'test-key'; + try { + const req = { + body: { + locations: [ + { lat: 37.7, lon: -185.0 }, + { lat: 37.7, lon: -122.4 } + ] + } + }; + let statusCode = null; + let body = null; + const res = { + status: (c) => { + statusCode = c; + return { json: (b) => { body = b; } }; + } + }; + await calcHandler(req, res); + assert.strictEqual(statusCode, 400); + assert.strictEqual(body?.error, 'invalidRequest'); + assert.match(body?.message, /out of geographic bounds/); + } finally { + delete process.env.STADIA_API_KEY; + } + }); + + it('rejects non-finite coordinates (NaN/Infinity)', async () => { + process.env.STADIA_API_KEY = 'test-key'; + try { + const req = { + body: { + locations: [ + { lat: NaN, lon: -122.4 }, + { lat: 37.7, lon: -122.4 } + ] + } + }; + let statusCode = null; + let body = null; + const res = { + status: (c) => { + statusCode = c; + return { json: (b) => { body = b; } }; + } + }; + await calcHandler(req, res); + assert.strictEqual(statusCode, 400); + assert.strictEqual(body?.error, 'invalidRequest'); + assert.match(body?.message, /Invalid or non-finite/); + } finally { + delete process.env.STADIA_API_KEY; + } + }); + + it('fails with 503 providerUnavailable if STADIA_API_KEY is not configured', async () => { + delete process.env.STADIA_API_KEY; + const req = { body: { locations: [{ lat: 37.7, lon: -122.4 }, { lat: 37.8, lon: -122.4 }] } }; + let statusCode = null; + let body = null; + const res = { + status: (c) => { + statusCode = c; + return { json: (b) => { body = b; } }; + } + }; + await calcHandler(req, res); + assert.strictEqual(statusCode, 503); + assert.strictEqual(body?.error, 'providerUnavailable'); + }); +}); + +describe('Places Input Hardening & Validations', () => { + const searchLayer = placesRouter.stack.find((l) => l.route && l.route.path === '/search'); + assert.ok(searchLayer, '/search endpoint must exist'); + const searchHandler = searchLayer.route.stack[searchLayer.route.stack.length - 1].handle; + + const resolveLayer = placesRouter.stack.find((l) => l.route && l.route.path === '/resolve'); + assert.ok(resolveLayer, '/resolve endpoint must exist'); + const resolveHandler = resolveLayer.route.stack[resolveLayer.route.stack.length - 1].handle; + + it('search rejects empty or short query with 400', async () => { + process.env.STADIA_API_KEY = 'test-key'; + try { + const req = { query: { query: 'a' } }; + let statusCode = null; + let body = null; + const res = { + status: (c) => { + statusCode = c; + return { json: (b) => { body = b; } }; + } + }; + await searchHandler(req, res); + assert.strictEqual(statusCode, 400); + assert.strictEqual(body?.error, 'invalidRequest'); + } finally { + delete process.env.STADIA_API_KEY; + } + }); + + it('search rejects excessive query (>200 chars) with 400', async () => { + process.env.STADIA_API_KEY = 'test-key'; + try { + const req = { query: { query: 'A'.repeat(201) } }; + let statusCode = null; + let body = null; + const res = { + status: (c) => { + statusCode = c; + return { json: (b) => { body = b; } }; + } + }; + await searchHandler(req, res); + assert.strictEqual(statusCode, 400); + assert.strictEqual(body?.error, 'invalidRequest'); + } finally { + delete process.env.STADIA_API_KEY; + } + }); + + it('search normalizes Pelias GeoJSON features into place results with coordinates', async () => { + process.env.STADIA_API_KEY = 'test-places-key'; + const originalGet = axios.get; + try { + axios.get = async (url, config) => { + assert.strictEqual(url, 'https://api.stadiamaps.com/geocoding/v1/autocomplete'); + assert.strictEqual(config?.params?.api_key, 'test-places-key'); + return { + data: { + features: [ + { + properties: { + id: 'venue_123', + name: 'Skyline Boulevard', + label: 'Skyline Boulevard, Oakland, CA', + region: 'California', + country: 'USA' + }, + geometry: { + coordinates: [-122.18, 37.81] + } + } + ] + } + }; + }; + + const req = { query: { query: 'Skyline' } }; + let responseData = null; + const res = { + json: (d) => { responseData = d; } + }; + + await searchHandler(req, res); + assert.ok(Array.isArray(responseData?.results)); + assert.strictEqual(responseData.results.length, 1); + const place = responseData.results[0]; + assert.strictEqual(place.providerId, 'venue_123'); + assert.strictEqual(place.primaryText, 'Skyline Boulevard'); + assert.strictEqual(place.latitude, 37.81); + assert.strictEqual(place.longitude, -122.18); + } finally { + axios.get = originalGet; + delete process.env.STADIA_API_KEY; + } + }); + + it('resolve rejects missing or empty placeId with 400', async () => { + process.env.STADIA_API_KEY = 'test-key'; + try { + const req = { query: { placeId: ' ' } }; + let statusCode = null; + let body = null; + const res = { + status: (c) => { + statusCode = c; + return { json: (b) => { body = b; } }; + } + }; + await resolveHandler(req, res); + assert.strictEqual(statusCode, 400); + assert.strictEqual(body?.error, 'invalidRequest'); + } finally { + delete process.env.STADIA_API_KEY; + } + }); + + it('resolve rejects excessive placeId (>200 chars) with 400', async () => { + process.env.STADIA_API_KEY = 'test-key'; + try { + const req = { query: { placeId: 'x'.repeat(201) } }; + let statusCode = null; + let body = null; + const res = { + status: (c) => { + statusCode = c; + return { json: (b) => { body = b; } }; + } + }; + await resolveHandler(req, res); + assert.strictEqual(statusCode, 400); + assert.strictEqual(body?.error, 'invalidRequest'); + } finally { + delete process.env.STADIA_API_KEY; + } + }); +}); diff --git a/server/waze_relay/test/waze_compatibility.test.js b/server/waze_relay/test/waze_compatibility.test.js new file mode 100644 index 0000000..8f586b7 --- /dev/null +++ b/server/waze_relay/test/waze_compatibility.test.js @@ -0,0 +1,42 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert'); +const app = require('../server'); + +describe('Waze Compatibility and Health Endpoints', () => { + it('health endpoint returns legacy service name and status ok', async () => { + // Test the health route handler directly + const route = app._router.stack.find( + (layer) => layer.route && layer.route.path && (layer.route.path === '/health' || layer.route.path.includes('/health')) + ); + assert.ok(route, 'Health route should be registered'); + + let responseData = null; + const req = {}; + const res = { + json: (data) => { + responseData = data; + } + }; + + route.route.stack[0].handle(req, res); + + assert.strictEqual(responseData?.status, 'ok'); + assert.strictEqual(responseData?.service, 'MotorSignal Waze Community Relay'); + assert.ok(Array.isArray(responseData?.features)); + assert.ok(responseData.features.includes('police')); + assert.ok(responseData.features.includes('traffic')); + }); + + it('mounts legacy /alerts, /api/alerts, and /api/waze routes', () => { + const mountedPaths = app._router.stack + .filter((layer) => layer.name === 'router') + .map((layer) => layer.regexp.toString()); + + // Express regex representations for paths + const hasAlerts = mountedPaths.some((p) => p.includes('alerts')); + const hasApiAlerts = mountedPaths.some((p) => p.includes('api')); + + assert.ok(hasAlerts, '/alerts router should be mounted'); + assert.ok(hasApiAlerts, '/api/* router should be mounted'); + }); +}); diff --git a/test/fake_route_provider.dart b/test/fake_route_provider.dart new file mode 100644 index 0000000..4c5c4e6 --- /dev/null +++ b/test/fake_route_provider.dart @@ -0,0 +1,86 @@ +import 'package:motorcycle_dash/services/map/map_provider_interface.dart'; +import 'package:motorcycle_dash/services/routes/route_models.dart'; +import 'package:motorcycle_dash/services/routes/route_provider.dart'; + +class FakeRouteProvider implements RouteProvider { + final bool shouldFail; + final bool shouldTimeout; + + FakeRouteProvider({ + this.shouldFail = false, + this.shouldTimeout = false, + }); + + @override + String get name => 'Fake Provider'; + + @override + Future calculateRoute( + List stops, { + bool avoidHighways = false, + bool avoidTolls = false, + bool avoidFerries = false, + }) async { + if (shouldTimeout) { + await Future.delayed(const Duration(seconds: 2)); + throw RouteProviderException('providerTimeout', 'Fake timeout'); + } + + if (shouldFail) { + throw RouteProviderException('routeNotFound', 'Fake route not found'); + } + + if (stops.length < 2) { + throw RouteProviderException('invalidRequest', 'Too few stops'); + } + if (stops.length > 25) { + throw RouteProviderException('invalidRequest', 'Too many stops'); + } + + final ordered = List.from(stops); + ordered.sort((a, b) => a.order.compareTo(b.order)); + + final dist = MotorSignalRoute.calculateTotalDistanceMeters(ordered); + final geometry = ordered.map((s) => s.latLng).toList(); + + return RouteCalculationResult( + straightLineDistanceMeters: dist, + estimatedDistanceMeters: dist * 1.2, + estimatedDurationSeconds: (dist / 20).toInt(), + geometry: geometry, + isStraightLine: false, + providerName: name, + warnings: ['This is a fake route.'], + ); + } + + @override + Future resolvePlace(String placeId) async { + if (shouldFail) throw RouteProviderException('placeNotFound', 'Fake not found'); + return ResolvedPlace( + providerId: placeId, + displayName: 'Fake Place', + latitude: 0.0, + longitude: 0.0, + attribution: 'Fake Attribution', + ); + } + + @override + Future> searchPlaces( + String query, { + AppLatLng? nearLocation, + }) async { + if (shouldFail) throw RouteProviderException('placeSearchUnavailable', 'Fake search error'); + if (query.length < 2) return []; + + return [ + PlaceSearchResult( + providerId: 'fake_1', + primaryText: 'Fake Result 1 for $query', + secondaryText: 'Fake City', + attribution: 'Fake Attribution', + ), + ]; + } +} diff --git a/test/fake_route_provider_test.dart b/test/fake_route_provider_test.dart new file mode 100644 index 0000000..57094fc --- /dev/null +++ b/test/fake_route_provider_test.dart @@ -0,0 +1,63 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:motorcycle_dash/services/routes/route_models.dart'; +import 'fake_route_provider.dart'; +import 'package:motorcycle_dash/services/routes/route_provider.dart'; + +void main() { + group('FakeRouteProvider', () { + test('enforces 25 stop maximum', () async { + final provider = FakeRouteProvider(); + + final stops = List.generate(26, (i) => RouteStop( + id: 'stop_$i', + displayName: 'Stop $i', + latitude: 0.0, + longitude: 0.0, + order: i, + stopType: RouteStopType.waypoint, + )); + + expect( + () => provider.calculateRoute(stops), + throwsA(isA()), + ); + }); + + test('enforces 2 stop minimum', () async { + final provider = FakeRouteProvider(); + + final stops = [ + const RouteStop( + id: 'stop_1', + displayName: 'Stop 1', + latitude: 0.0, + longitude: 0.0, + order: 0, + stopType: RouteStopType.waypoint, + ) + ]; + + expect( + () => provider.calculateRoute(stops), + throwsA(isA()), + ); + }); + + test('calculates successfully', () async { + final provider = FakeRouteProvider(); + + final stops = List.generate(3, (i) => RouteStop( + id: 'stop_$i', + displayName: 'Stop $i', + latitude: i.toDouble(), + longitude: i.toDouble(), + order: i, + stopType: RouteStopType.waypoint, + )); + + final result = await provider.calculateRoute(stops); + expect(result.geometry.length, 3); + expect(result.isStraightLine, false); + }); + }); +} diff --git a/test/valhalla_polyline_decoder_test.dart b/test/valhalla_polyline_decoder_test.dart new file mode 100644 index 0000000..2db4996 --- /dev/null +++ b/test/valhalla_polyline_decoder_test.dart @@ -0,0 +1,62 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:motorcycle_dash/services/routes/valhalla_polyline_decoder.dart'; + +void main() { + group('ValhallaPolylineDecoder', () { + test('handles empty string', () { + final points = ValhallaPolylineDecoder.decode(''); + expect(points, isEmpty); + }); + + test('decodes single point with precision 6', () { + // Deterministic fixture for (37.774900, -122.419400) + final points = ValhallaPolylineDecoder.decode('gbr`gAnk{nhF'); + expect(points.length, 1); + expect(points[0].latitude, closeTo(37.7749, 1e-5)); + expect(points[0].longitude, closeTo(-122.4194, 1e-5)); + }); + + test('decodes multiple points with positive deltas (SF to Oakland)', () { + // SF (37.774900, -122.419400) -> Oakland (37.804400, -122.271200) + final points = ValhallaPolylineDecoder.decode('gbr`gAnk{nhFwrx@om`H'); + expect(points.length, 2); + expect(points[0].latitude, closeTo(37.7749, 1e-5)); + expect(points[0].longitude, closeTo(-122.4194, 1e-5)); + expect(points[1].latitude, closeTo(37.8044, 1e-5)); + expect(points[1].longitude, closeTo(-122.2712, 1e-5)); + }); + + test('decodes multiple points with negative lat delta (SF to San Jose)', () { + // SF (37.774900, -122.419400) -> San Jose (37.338200, -121.886300) + final points = ValhallaPolylineDecoder.decode('gbr`gAnk{nhFv|sYwep_@'); + expect(points.length, 2); + expect(points[0].latitude, closeTo(37.7749, 1e-5)); + expect(points[0].longitude, closeTo(-122.4194, 1e-5)); + expect(points[1].latitude, closeTo(37.3382, 1e-5)); + expect(points[1].longitude, closeTo(-121.8863, 1e-5)); + }); + + test('throws FormatException on truncated polyline', () { + expect( + () => ValhallaPolylineDecoder.decode('gbr`gA'), + throwsA(isA()), + ); + }); + + test('throws FormatException on invalid characters (ascii < 63)', () { + expect( + () => ValhallaPolylineDecoder.decode('gbr\x00gAnk'), + throwsA(isA()), + ); + }); + + test('throws FormatException when coordinates exceed geographic bounds', () { + // Encoded value that unpacks to latitude > 90 + // 95.0 * 1e6 = 95000000 + expect( + () => ValhallaPolylineDecoder.decode('____~_~_~_~_~_'), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/valhalla_route_provider_test.dart b/test/valhalla_route_provider_test.dart new file mode 100644 index 0000000..0f3c81a --- /dev/null +++ b/test/valhalla_route_provider_test.dart @@ -0,0 +1,261 @@ +import 'dart:convert'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:motorcycle_dash/services/routes/route_models.dart'; +import 'package:motorcycle_dash/services/routes/route_provider.dart'; +import 'package:motorcycle_dash/services/routes/valhalla_route_provider.dart'; + +void main() { + group('ValhallaRouteProvider Tests', () { + test('throws authenticationRequired when token provider returns null', () async { + final provider = ValhallaRouteProvider( + backendUrl: 'http://localhost:3000', + tokenProvider: () async => null, + ); + + final stops = [ + const RouteStop( + id: 's1', + displayName: 'Start', + latitude: 37.77, + longitude: -122.42, + order: 0, + ), + const RouteStop( + id: 's2', + displayName: 'End', + latitude: 37.80, + longitude: -122.27, + order: 1, + ), + ]; + + expect( + () => provider.calculateRoute(stops), + throwsA(isA().having( + (e) => e.code, + 'code', + 'authenticationRequired', + )), + ); + }); + + test('validates minimum 2 stops', () async { + final provider = ValhallaRouteProvider( + tokenProvider: () async => 'valid-mock-token', + ); + + final stops = [ + const RouteStop( + id: 's1', + displayName: 'Single Stop', + latitude: 37.77, + longitude: -122.42, + order: 0, + ), + ]; + + expect( + () => provider.calculateRoute(stops), + throwsA(isA().having( + (e) => e.code, + 'code', + 'invalidRequest', + )), + ); + }); + + test('validates maximum 25 stops', () async { + final provider = ValhallaRouteProvider( + tokenProvider: () async => 'valid-mock-token', + ); + + final stops = List.generate( + 26, + (i) => RouteStop( + id: 's$i', + displayName: 'Stop $i', + latitude: 37.0 + i * 0.01, + longitude: -122.0, + order: i, + ), + ); + + expect( + () => provider.calculateRoute(stops), + throwsA(isA().having( + (e) => e.code, + 'code', + 'invalidRequest', + )), + ); + }); + + test('calculates 2-stop route and parses normalized result with precision 6 geometry', () async { + final mockClient = MockClient((request) async { + expect(request.url.path, '/api/routes/calculate'); + expect(request.headers['authorization'], 'Bearer valid-test-token'); + + final payload = jsonDecode(request.body) as Map; + expect((payload['locations'] as List).length, 2); + + // Return fixture for SF to Oakland + return http.Response( + jsonEncode({ + 'provider': 'valhalla_stadia', + 'encodedPolyline': 'gbr`gAnk{nhFwrx@om`H', + 'distanceMeters': 18500.5, + 'durationSeconds': 1320, + 'warnings': ['Motorcycle route preview'], + 'attribution': '© Stadia Maps', + }), + 200, + headers: {'content-type': 'application/json'}, + ); + }); + + final provider = ValhallaRouteProvider( + client: mockClient, + tokenProvider: () async => 'valid-test-token', + ); + + final stops = [ + const RouteStop( + id: 's1', + displayName: 'SF', + latitude: 37.7749, + longitude: -122.4194, + order: 0, + ), + const RouteStop( + id: 's2', + displayName: 'Oakland', + latitude: 37.8044, + longitude: -122.2712, + order: 1, + ), + ]; + + final result = await provider.calculateRoute(stops); + + expect(result.isStraightLine, false); + expect(result.estimatedDistanceMeters, 18500.5); + expect(result.estimatedDurationSeconds, 1320); + expect(result.geometry.length, 2); + expect(result.geometry[0].latitude, closeTo(37.7749, 1e-4)); + expect(result.geometry[1].latitude, closeTo(37.8044, 1e-4)); + expect(result.warnings, contains('Motorcycle route preview')); + }); + + test('searches places and parses coordinates from search results', () async { + final mockClient = MockClient((request) async { + expect(request.url.path, '/api/places/search'); + expect(request.url.queryParameters['query'], 'Alice Restaurant'); + + return http.Response( + jsonEncode({ + 'results': [ + { + 'providerId': 'place_123', + 'primaryText': "Alice's Restaurant", + 'secondaryText': 'Woodside, CA', + 'formattedAddress': "Alice's Restaurant, Skyline Blvd, Woodside, CA", + 'latitude': 37.3868, + 'longitude': -122.2644, + } + ], + 'attribution': 'Powered by Stadia Maps', + }), + 200, + headers: {'content-type': 'application/json'}, + ); + }); + + final provider = ValhallaRouteProvider( + client: mockClient, + tokenProvider: () async => 'valid-test-token', + ); + + final results = await provider.searchPlaces('Alice Restaurant'); + + expect(results.length, 1); + final place = results.first; + expect(place.providerId, 'place_123'); + expect(place.primaryText, "Alice's Restaurant"); + expect(place.hasCoordinates, true); + expect(place.latitude, 37.3868); + expect(place.longitude, -122.2644); + + // Verify immediate resolution without second network call + final resolved = place.toResolvedPlace(); + expect(resolved, isNotNull); + expect(resolved!.latitude, 37.3868); + expect(resolved.displayName, "Alice's Restaurant"); + }); + + test('resolves place details by placeId', () async { + final mockClient = MockClient((request) async { + expect(request.url.path, '/api/places/resolve'); + expect(request.url.queryParameters['placeId'], 'gid_999'); + + return http.Response( + jsonEncode({ + 'providerId': 'gid_999', + 'displayName': 'Mount Hamilton Observatory', + 'formattedAddress': 'Mount Hamilton Rd, San Jose, CA', + 'latitude': 37.3414, + 'longitude': -121.6429, + 'attribution': 'Powered by Stadia Maps', + }), + 200, + headers: {'content-type': 'application/json'}, + ); + }); + + final provider = ValhallaRouteProvider( + client: mockClient, + tokenProvider: () async => 'valid-test-token', + ); + + final resolved = await provider.resolvePlace('gid_999'); + + expect(resolved.providerId, 'gid_999'); + expect(resolved.displayName, 'Mount Hamilton Observatory'); + expect(resolved.latitude, 37.3414); + expect(resolved.longitude, -121.6429); + }); + + test('maps backend 404 to routeNotFound exception', () async { + final mockClient = MockClient((request) async { + return http.Response( + jsonEncode({ + 'error': 'routeNotFound', + 'message': 'No route could be found', + }), + 404, + headers: {'content-type': 'application/json'}, + ); + }); + + final provider = ValhallaRouteProvider( + client: mockClient, + tokenProvider: () async => 'valid-test-token', + ); + + final stops = [ + const RouteStop(id: '1', displayName: '1', latitude: 0, longitude: 0, order: 0), + const RouteStop(id: '2', displayName: '2', latitude: 1, longitude: 1, order: 1), + ]; + + expect( + () => provider.calculateRoute(stops), + throwsA(isA().having( + (e) => e.code, + 'code', + 'routeNotFound', + )), + ); + }); + }); +}