Merge pull request #256 from SELab-2/feat/caching
feat: Request caching
This commit is contained in:
		
						commit
						33c2f63871
					
				
					 15 changed files with 320 additions and 263 deletions
				
			
		|  | @ -13,7 +13,7 @@ | ||||||
| #DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api | #DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api | ||||||
| # The default fallback language. | # The default fallback language. | ||||||
| #DWENGO_FALLBACK_LANGUAGE=nl | #DWENGO_FALLBACK_LANGUAGE=nl | ||||||
| # Whether running in production mode or not. Possible values are "prod" or "dev". | # Whether running in production mode or not. Possible values are "prod", "staging", "test" or "dev". | ||||||
| #DWENGO_RUN_MODE=dev | #DWENGO_RUN_MODE=dev | ||||||
| 
 | 
 | ||||||
| # ! Change this! The hostname or IP address of the database | # ! Change this! The hostname or IP address of the database | ||||||
|  | @ -66,3 +66,12 @@ DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://hostname/realms/teacher/protocol/openid | ||||||
| # The address of the Loki instance, a log aggregation system. | # The address of the Loki instance, a log aggregation system. | ||||||
| # If running your stack in docker, this should use the docker service name. | # If running your stack in docker, this should use the docker service name. | ||||||
| #DWENGO_LOGGING_LOKI_HOST=http://localhost:3102 | #DWENGO_LOGGING_LOKI_HOST=http://localhost:3102 | ||||||
|  | 
 | ||||||
|  | # The hostname or IP address of the caching server, e.g. Memcached. | ||||||
|  | # If running your stack in docker, this should use the docker service name. | ||||||
|  | #DWENGO_CACHE_HOST=localhost | ||||||
|  | #DWENGO_CACHE_PORT=11211 | ||||||
|  | # The time-to-live (TTL) for cached items in seconds. | ||||||
|  | #DWENGO_CACHE_TTL=3600 | ||||||
|  | # If your cache server benefits from a prefix, you can set it here. | ||||||
|  | #DWENGO_CACHE_KEY_PREFIX=dwengo | ||||||
|  |  | ||||||
|  | @ -35,3 +35,7 @@ DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/op | ||||||
| 
 | 
 | ||||||
| DWENGO_LOGGING_LEVEL=info | DWENGO_LOGGING_LEVEL=info | ||||||
| DWENGO_LOGGING_LOKI_HOST=http://logging:3102 | DWENGO_LOGGING_LOKI_HOST=http://logging:3102 | ||||||
|  | 
 | ||||||
|  | DWENGO_CACHE_HOST=caching | ||||||
|  | #DWENGO_CACHE_PORT=11211 | ||||||
|  | DWENGO_CACHE_TTL=604800 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,6 @@ | ||||||
| PORT=3000 | PORT=3000 | ||||||
|  | DWENGO_RUN_MODE=staging | ||||||
|  | 
 | ||||||
| DWENGO_DB_HOST=db | DWENGO_DB_HOST=db | ||||||
| DWENGO_DB_PORT=5432 | DWENGO_DB_PORT=5432 | ||||||
| DWENGO_DB_USERNAME=postgres | DWENGO_DB_USERNAME=postgres | ||||||
|  | @ -18,4 +20,8 @@ DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/*,http://idp:7080,https://idp:7080 | ||||||
| 
 | 
 | ||||||
| # Logging and monitoring | # Logging and monitoring | ||||||
| 
 | 
 | ||||||
| LOKI_HOST=http://logging:3102 | DWENGO_LOGGING_LEVEL=debug | ||||||
|  | DWENGO_LOGGING_LOKI_HOST=http://logging:3102 | ||||||
|  | 
 | ||||||
|  | DWENGO_CACHE_HOST=caching | ||||||
|  | DWENGO_CACHE_TTL=86400 | ||||||
|  |  | ||||||
|  | @ -27,6 +27,13 @@ FROM node:22 AS production-stage | ||||||
| 
 | 
 | ||||||
| WORKDIR /app/dwengo | WORKDIR /app/dwengo | ||||||
| 
 | 
 | ||||||
|  | COPY package*.json ./ | ||||||
|  | COPY backend/package.json ./backend/ | ||||||
|  | # Backend depends on common | ||||||
|  | COPY common/package.json ./common/ | ||||||
|  | 
 | ||||||
|  | RUN npm install --silent --only=production | ||||||
|  | 
 | ||||||
| # Copy static files | # Copy static files | ||||||
| 
 | 
 | ||||||
| COPY ./backend/i18n ./i18n | COPY ./backend/i18n ./i18n | ||||||
|  | @ -37,15 +44,6 @@ COPY --from=build-stage /app/dwengo/common/dist ./common/dist | ||||||
| COPY --from=build-stage /app/dwengo/backend/dist ./backend/dist | COPY --from=build-stage /app/dwengo/backend/dist ./backend/dist | ||||||
| COPY --from=build-stage /app/dwengo/docs/api/swagger.json ./docs/api/swagger.json | COPY --from=build-stage /app/dwengo/docs/api/swagger.json ./docs/api/swagger.json | ||||||
| 
 | 
 | ||||||
| COPY package*.json ./ |  | ||||||
| COPY backend/package.json ./backend/ |  | ||||||
| # Backend depends on common |  | ||||||
| COPY common/package.json ./common/ |  | ||||||
| 
 |  | ||||||
| RUN npm install --silent --only=production |  | ||||||
| 
 |  | ||||||
| COPY ./backend/i18n ./backend/i18n |  | ||||||
| 
 |  | ||||||
| EXPOSE 3000 | EXPOSE 3000 | ||||||
| 
 | 
 | ||||||
| CMD ["node", "--env-file=/app/dwengo/backend/.env", "/app/dwengo/backend/dist/app.js"] | CMD ["node", "--env-file=/app/dwengo/backend/.env", "/app/dwengo/backend/dist/app.js"] | ||||||
|  |  | ||||||
|  | @ -38,6 +38,7 @@ | ||||||
|         "jwks-rsa": "^3.1.0", |         "jwks-rsa": "^3.1.0", | ||||||
|         "loki-logger-ts": "^1.0.2", |         "loki-logger-ts": "^1.0.2", | ||||||
|         "marked": "^15.0.7", |         "marked": "^15.0.7", | ||||||
|  |         "memjs": "^1.3.2", | ||||||
|         "mime-types": "^3.0.1", |         "mime-types": "^3.0.1", | ||||||
|         "nanoid": "^5.1.5", |         "nanoid": "^5.1.5", | ||||||
|         "response-time": "^2.3.3", |         "response-time": "^2.3.3", | ||||||
|  | @ -53,6 +54,7 @@ | ||||||
|         "@types/express": "^5.0.0", |         "@types/express": "^5.0.0", | ||||||
|         "@types/express-fileupload": "^1.5.1", |         "@types/express-fileupload": "^1.5.1", | ||||||
|         "@types/js-yaml": "^4.0.9", |         "@types/js-yaml": "^4.0.9", | ||||||
|  |         "@types/memjs": "^1.3.3", | ||||||
|         "@types/mime-types": "^2.1.4", |         "@types/mime-types": "^2.1.4", | ||||||
|         "@types/node": "^22.13.4", |         "@types/node": "^22.13.4", | ||||||
|         "@types/response-time": "^2.3.8", |         "@types/response-time": "^2.3.8", | ||||||
|  |  | ||||||
							
								
								
									
										44
									
								
								backend/src/caching.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								backend/src/caching.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,44 @@ | ||||||
|  | import { getLogger } from './logging/initalize.js'; | ||||||
|  | import { envVars, getEnvVar } from './util/envVars.js'; | ||||||
|  | import { Client } from 'memjs'; | ||||||
|  | 
 | ||||||
|  | export type CacheClient = Client; | ||||||
|  | 
 | ||||||
|  | let cacheClient: CacheClient; | ||||||
|  | 
 | ||||||
|  | async function initializeClient(): Promise<CacheClient> { | ||||||
|  |     if (cacheClient !== undefined) { | ||||||
|  |         return cacheClient; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const cachingHost = getEnvVar(envVars.CacheHost); | ||||||
|  |     const cachingPort = getEnvVar(envVars.CachePort); | ||||||
|  |     const cachingUrl = `${cachingHost}:${cachingPort}`; | ||||||
|  | 
 | ||||||
|  |     if (cachingHost === '') { | ||||||
|  |         return cacheClient; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     cacheClient = Client.create(cachingUrl); | ||||||
|  | 
 | ||||||
|  |     getLogger().info(`Memcached client initialized at ${cachingUrl}`); | ||||||
|  | 
 | ||||||
|  |     return cacheClient; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getCacheClient(): Promise<CacheClient> { | ||||||
|  |     cacheClient ||= await initializeClient(); | ||||||
|  |     return cacheClient; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function checkCachingHealth(): Promise<boolean> { | ||||||
|  |     try { | ||||||
|  |         const client = await getCacheClient(); | ||||||
|  |         await client.set('health', Buffer.from('ok'), { expires: 60 }); | ||||||
|  |         const reply = await cacheClient.get('health'); | ||||||
|  |         return reply?.value?.toString() === 'ok'; | ||||||
|  |     } catch (error) { | ||||||
|  |         getLogger().error('Caching Health Check Failed:', error); | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -29,7 +29,8 @@ function initializeLogger(): Logger { | ||||||
|         format: format.combine(format.cli(), format.simple()), |         format: format.combine(format.cli(), format.simple()), | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     if (getEnvVar(envVars.RunMode) === 'dev') { |     const runMode = getEnvVar(envVars.RunMode); | ||||||
|  |     if (runMode === 'dev' || runMode.includes('test')) { | ||||||
|         logger = createLogger({ |         logger = createLogger({ | ||||||
|             transports: [consoleTransport], |             transports: [consoleTransport], | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { DWENGO_API_BASE } from '../config.js'; | import { DWENGO_API_BASE } from '../config.js'; | ||||||
| import { fetchWithLogging } from '../util/api-helper.js'; | import { fetchRemote } from '../util/api-helper.js'; | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
|     FilteredLearningObject, |     FilteredLearningObject, | ||||||
|  | @ -39,10 +39,7 @@ function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLear | ||||||
|  */ |  */ | ||||||
| export async function getLearningObjectById(hruid: string, language: string): Promise<FilteredLearningObject | null> { | export async function getLearningObjectById(hruid: string, language: string): Promise<FilteredLearningObject | null> { | ||||||
|     const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; |     const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`; | ||||||
|     const metadata = await fetchWithLogging<LearningObjectMetadata>( |     const metadata = await fetchRemote<LearningObjectMetadata>(metadataUrl, `Metadata for Learning Object HRUID "${hruid}" (language ${language})`); | ||||||
|         metadataUrl, |  | ||||||
|         `Metadata for Learning Object HRUID "${hruid}" (language ${language})` |  | ||||||
|     ); |  | ||||||
| 
 | 
 | ||||||
|     if (!metadata) { |     if (!metadata) { | ||||||
|         getLogger().error(`⚠️ WARNING: Learning object "${hruid}" not found.`); |         getLogger().error(`⚠️ WARNING: Learning object "${hruid}" not found.`); | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { DWENGO_API_BASE } from '../../config.js'; | import { DWENGO_API_BASE } from '../../config.js'; | ||||||
| import { fetchWithLogging } from '../../util/api-helper.js'; | import { fetchRemote } from '../../util/api-helper.js'; | ||||||
| import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js'; | import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js'; | ||||||
| import { LearningObjectProvider } from './learning-object-provider.js'; | import { LearningObjectProvider } from './learning-object-provider.js'; | ||||||
| import { getLogger, Logger } from '../../logging/initalize.js'; | import { getLogger, Logger } from '../../logging/initalize.js'; | ||||||
|  | @ -88,7 +88,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | ||||||
|      */ |      */ | ||||||
|     async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null> { |     async getLearningObjectById(id: LearningObjectIdentifierDTO): Promise<FilteredLearningObject | null> { | ||||||
|         const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`; |         const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata`; | ||||||
|         const metadata = await fetchWithLogging<LearningObjectMetadata>( |         const metadata = await fetchRemote<LearningObjectMetadata>( | ||||||
|             metadataUrl, |             metadataUrl, | ||||||
|             `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, |             `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, | ||||||
|             { |             { | ||||||
|  | @ -124,7 +124,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { | ||||||
|      */ |      */ | ||||||
|     async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> { |     async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> { | ||||||
|         const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; |         const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; | ||||||
|         const html = await fetchWithLogging<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { |         const html = await fetchRemote<string>(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { | ||||||
|             params: { ...id }, |             params: { ...id }, | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { fetchWithLogging } from '../../util/api-helper.js'; | import { fetchRemote } from '../../util/api-helper.js'; | ||||||
| import { DWENGO_API_BASE } from '../../config.js'; | import { DWENGO_API_BASE } from '../../config.js'; | ||||||
| import { LearningPathProvider } from './learning-path-provider.js'; | import { LearningPathProvider } from './learning-path-provider.js'; | ||||||
| import { getLogger, Logger } from '../../logging/initalize.js'; | import { getLogger, Logger } from '../../logging/initalize.js'; | ||||||
|  | @ -42,7 +42,7 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { | ||||||
|         const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`; |         const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`; | ||||||
|         const params = { pathIdList: JSON.stringify({ hruids }), language }; |         const params = { pathIdList: JSON.stringify({ hruids }), language }; | ||||||
| 
 | 
 | ||||||
|         const learningPaths = await fetchWithLogging<LearningPath[]>(apiUrl, `Learning paths for ${source}`, { params }); |         const learningPaths = await fetchRemote<LearningPath[]>(apiUrl, `Learning paths for ${source}`, { params }); | ||||||
| 
 | 
 | ||||||
|         if (!learningPaths || learningPaths.length === 0) { |         if (!learningPaths || learningPaths.length === 0) { | ||||||
|             logger.warn(`⚠️ WARNING: No learning paths found for ${source}.`); |             logger.warn(`⚠️ WARNING: No learning paths found for ${source}.`); | ||||||
|  | @ -66,12 +66,11 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { | ||||||
|         const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; |         const apiUrl = `${DWENGO_API_BASE}/learningPath/search`; | ||||||
|         const params = { all: query, language }; |         const params = { all: query, language }; | ||||||
| 
 | 
 | ||||||
|         const searchResults = await fetchWithLogging<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params }); |         const searchResults = await fetchRemote<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params }); | ||||||
| 
 | 
 | ||||||
|         if (searchResults) { |         if (searchResults) { | ||||||
|             await Promise.all(searchResults?.map(async (it) => addProgressToLearningPath(it, personalizedFor))); |             await Promise.all(searchResults?.map(async (it) => addProgressToLearningPath(it, personalizedFor))); | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|         return searchResults ?? []; |         return searchResults ?? []; | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,28 +1,66 @@ | ||||||
| import axios, { AxiosRequestConfig } from 'axios'; | import axios, { AxiosRequestConfig } from 'axios'; | ||||||
| import { getLogger, Logger } from '../logging/initalize.js'; | import { getLogger, Logger } from '../logging/initalize.js'; | ||||||
| import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||||
|  | import { getCacheClient } from '../caching.js'; | ||||||
|  | import { envVars, getEnvVar, getNumericEnvVar } from './envVars.js'; | ||||||
|  | import { createHash } from 'crypto'; | ||||||
| 
 | 
 | ||||||
|  | const cacheClient = await getCacheClient(); | ||||||
| const logger: Logger = getLogger(); | const logger: Logger = getLogger(); | ||||||
|  | const runMode: string = getEnvVar(envVars.RunMode); | ||||||
|  | const prefix: string = getEnvVar(envVars.CacheKeyPrefix); | ||||||
|  | 
 | ||||||
|  | interface Options { | ||||||
|  |     params?: Record<string, unknown> | LearningObjectIdentifier; | ||||||
|  |     query?: Record<string, unknown>; | ||||||
|  |     responseType?: 'json' | 'text'; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Utility function to fetch data from an API endpoint with error handling. |  * Utility function to fetch data from an API endpoint with error handling and caching. | ||||||
|  * Logs errors but does NOT throw exceptions to keep the system running. |  * Logs errors but does NOT throw exceptions to keep the system running. | ||||||
|  * |  * | ||||||
|  * @param url The API endpoint to fetch from. |  * @param url The API endpoint to fetch from. | ||||||
|  * @param description A short description of what is being fetched (for logging). |  * @param description A short description of what is being fetched (for logging). | ||||||
|  * @param options Contains further options such as params (the query params) and responseType (whether the response |  * @param options Contains further options such as params (the query params) and responseType (whether the response | ||||||
|  *                should be parsed as JSON ("json") or whether it should be returned as plain text ("text") |  *                should be parsed as JSON ("json") or whether it should be returned as plain text ("text") | ||||||
|  |  * @param cacheTTL Time-to-live for the cache in seconds (default: 60 seconds). | ||||||
|  * @returns The response data if successful, or null if an error occurs. |  * @returns The response data if successful, or null if an error occurs. | ||||||
|  */ |  */ | ||||||
| export async function fetchWithLogging<T>( | export async function fetchRemote<T>(url: string, description: string, options?: Options, cacheTTL?: number): Promise<T | null> { | ||||||
|     url: string, |     if (runMode !== 'dev' && !runMode.includes('test') && cacheClient !== undefined) { | ||||||
|     description: string, |         return fetchWithCache<T>(url, description, options, cacheTTL); | ||||||
|     options?: { |  | ||||||
|         params?: Record<string, unknown> | LearningObjectIdentifier; |  | ||||||
|         query?: Record<string, unknown>; |  | ||||||
|         responseType?: 'json' | 'text'; |  | ||||||
|     } |     } | ||||||
| ): Promise<T | null> { | 
 | ||||||
|  |     getLogger().debug(`🔄 INFO: Bypassing cache for ${description} at "${url}".`); | ||||||
|  |     return fetchWithLogging(url, description, options); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function fetchWithCache<T>(url: string, description: string, options?: Options, cacheTTL?: number): Promise<T | null> { | ||||||
|  |     // Combine the URL and parameters to create a unique cache key.
 | ||||||
|  |     // NOTE Using a hash function to keep the key short, since Memcached has a limit on key size
 | ||||||
|  |     const urlWithParams = `${url}${options?.params ? JSON.stringify(options.params) : ''}`; | ||||||
|  |     const hashedUrl = createHash('sha256').update(urlWithParams).digest('hex'); | ||||||
|  |     const key = `${prefix}:${hashedUrl}`; | ||||||
|  | 
 | ||||||
|  |     const cachedData = await cacheClient.get(key); | ||||||
|  | 
 | ||||||
|  |     if (cachedData?.value) { | ||||||
|  |         logger.debug(`✅ INFO: Cache hit for ${description} at "${url}" (key: "${key}")`); | ||||||
|  |         return JSON.parse(cachedData.value.toString()) as T; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     logger.debug(`🔄 INFO: Cache miss for ${description} at "${url}". Fetching data...`); | ||||||
|  |     const response = await fetchWithLogging<T>(url, description, options); | ||||||
|  | 
 | ||||||
|  |     const ttl = cacheTTL || getNumericEnvVar(envVars.CacheTTL); | ||||||
|  |     await cacheClient.set(key, JSON.stringify(response), { expires: ttl }); | ||||||
|  |     logger.debug(`✅ INFO: Cached response for ${description} at "${url}" for ${ttl} seconds. (key: "${key}")`); | ||||||
|  | 
 | ||||||
|  |     return response; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function fetchWithLogging<T>(url: string, description: string, options?: Options): Promise<T | null> { | ||||||
|     try { |     try { | ||||||
|         const config: AxiosRequestConfig = options || {}; |         const config: AxiosRequestConfig = options || {}; | ||||||
|         const response = await axios.get<T>(url, config); |         const response = await axios.get<T>(url, config); | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ const STUDENT_IDP_PREFIX = IDP_PREFIX + 'STUDENT_'; | ||||||
| const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_'; | const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_'; | ||||||
| const CORS_PREFIX = PREFIX + 'CORS_'; | const CORS_PREFIX = PREFIX + 'CORS_'; | ||||||
| const LOGGING_PREFIX = PREFIX + 'LOGGING_'; | const LOGGING_PREFIX = PREFIX + 'LOGGING_'; | ||||||
|  | const CACHE_PREFIX = PREFIX + 'CACHE_'; | ||||||
| 
 | 
 | ||||||
| interface EnvVar { | interface EnvVar { | ||||||
|     key: string; |     key: string; | ||||||
|  | @ -39,6 +40,11 @@ export const envVars: Record<string, EnvVar> = { | ||||||
| 
 | 
 | ||||||
|     LogLevel: { key: LOGGING_PREFIX + 'LEVEL', defaultValue: 'info' }, |     LogLevel: { key: LOGGING_PREFIX + 'LEVEL', defaultValue: 'info' }, | ||||||
|     LokiHost: { key: LOGGING_PREFIX + 'LOKI_HOST', defaultValue: 'http://localhost:3102' }, |     LokiHost: { key: LOGGING_PREFIX + 'LOKI_HOST', defaultValue: 'http://localhost:3102' }, | ||||||
|  | 
 | ||||||
|  |     CacheHost: { key: CACHE_PREFIX + 'HOST' }, | ||||||
|  |     CachePort: { key: CACHE_PREFIX + 'PORT', defaultValue: 11211 }, | ||||||
|  |     CacheTTL: { key: CACHE_PREFIX + 'TTL', defaultValue: 60 * 60 * 24 }, // 24 hours
 | ||||||
|  |     CacheKeyPrefix: { key: CACHE_PREFIX + 'KEY_PREFIX', defaultValue: 'dwengo' }, | ||||||
| } as const; | } as const; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -56,7 +62,7 @@ export function getEnvVar(envVar: EnvVar): string { | ||||||
|     } else if (envVar.required) { |     } else if (envVar.required) { | ||||||
|         throw new Error(`Missing environment variable: ${envVar.key}`); |         throw new Error(`Missing environment variable: ${envVar.key}`); | ||||||
|     } else { |     } else { | ||||||
|         return String(envVar.defaultValue) || ''; |         return envVar.defaultValue !== undefined ? String(envVar.defaultValue) || '' : ''; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -157,6 +157,17 @@ services: | ||||||
|         volumes: |         volumes: | ||||||
|             - dwengo_grafana_data:/var/lib/grafana |             - dwengo_grafana_data:/var/lib/grafana | ||||||
| 
 | 
 | ||||||
|  |     caching: | ||||||
|  |         image: memcached | ||||||
|  |         restart: always | ||||||
|  |         ports: | ||||||
|  |             - '11211:11211' | ||||||
|  |         command: | ||||||
|  |             - --conn-limit=1024 | ||||||
|  |             - --memory-limit=2048 | ||||||
|  |             - -I 128m | ||||||
|  |             - --threads=4 | ||||||
|  | 
 | ||||||
| volumes: | volumes: | ||||||
|     dwengo_grafana_data: |     dwengo_grafana_data: | ||||||
|     dwengo_letsencrypt: |     dwengo_letsencrypt: | ||||||
|  |  | ||||||
|  | @ -97,6 +97,17 @@ services: | ||||||
|             - ./config/grafana/grafana.ini:/etc/grafana/grafana.ini |             - ./config/grafana/grafana.ini:/etc/grafana/grafana.ini | ||||||
|         restart: unless-stopped |         restart: unless-stopped | ||||||
| 
 | 
 | ||||||
|  |     caching: | ||||||
|  |         image: memcached | ||||||
|  |         restart: always | ||||||
|  |         ports: | ||||||
|  |             - '11211:11211' | ||||||
|  |         command: | ||||||
|  |             - --conn-limit=1024 | ||||||
|  |             - --memory-limit=2048 | ||||||
|  |             - -I 128m | ||||||
|  |             - --threads=4 | ||||||
|  | 
 | ||||||
| volumes: | volumes: | ||||||
|     dwengo_grafana_data: |     dwengo_grafana_data: | ||||||
|     dwengo_loki_data: |     dwengo_loki_data: | ||||||
|  |  | ||||||
							
								
								
									
										389
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										389
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -51,6 +51,7 @@ | ||||||
|                 "jwks-rsa": "^3.1.0", |                 "jwks-rsa": "^3.1.0", | ||||||
|                 "loki-logger-ts": "^1.0.2", |                 "loki-logger-ts": "^1.0.2", | ||||||
|                 "marked": "^15.0.7", |                 "marked": "^15.0.7", | ||||||
|  |                 "memjs": "^1.3.2", | ||||||
|                 "mime-types": "^3.0.1", |                 "mime-types": "^3.0.1", | ||||||
|                 "nanoid": "^5.1.5", |                 "nanoid": "^5.1.5", | ||||||
|                 "response-time": "^2.3.3", |                 "response-time": "^2.3.3", | ||||||
|  | @ -66,6 +67,7 @@ | ||||||
|                 "@types/express": "^5.0.0", |                 "@types/express": "^5.0.0", | ||||||
|                 "@types/express-fileupload": "^1.5.1", |                 "@types/express-fileupload": "^1.5.1", | ||||||
|                 "@types/js-yaml": "^4.0.9", |                 "@types/js-yaml": "^4.0.9", | ||||||
|  |                 "@types/memjs": "^1.3.3", | ||||||
|                 "@types/mime-types": "^2.1.4", |                 "@types/mime-types": "^2.1.4", | ||||||
|                 "@types/node": "^22.13.4", |                 "@types/node": "^22.13.4", | ||||||
|                 "@types/response-time": "^2.3.8", |                 "@types/response-time": "^2.3.8", | ||||||
|  | @ -78,6 +80,19 @@ | ||||||
|                 "vitest": "^3.0.6" |                 "vitest": "^3.0.6" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "backend/node_modules/globals": { | ||||||
|  |             "version": "15.15.0", | ||||||
|  |             "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", | ||||||
|  |             "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", | ||||||
|  |             "dev": true, | ||||||
|  |             "license": "MIT", | ||||||
|  |             "engines": { | ||||||
|  |                 "node": ">=18" | ||||||
|  |             }, | ||||||
|  |             "funding": { | ||||||
|  |                 "url": "https://github.com/sponsors/sindresorhus" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         "common": { |         "common": { | ||||||
|             "name": "@dwengo-1/common", |             "name": "@dwengo-1/common", | ||||||
|             "version": "0.2.0" |             "version": "0.2.0" | ||||||
|  | @ -753,9 +768,9 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@codemirror/search": { |         "node_modules/@codemirror/search": { | ||||||
|             "version": "6.5.10", |             "version": "6.5.11", | ||||||
|             "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.10.tgz", |             "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", | ||||||
|             "integrity": "sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg==", |             "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@codemirror/state": "^6.0.0", |                 "@codemirror/state": "^6.0.0", | ||||||
|  | @ -1492,9 +1507,9 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@eslint/core": { |         "node_modules/@eslint/core": { | ||||||
|             "version": "0.13.0", |             "version": "0.14.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", |             "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", | ||||||
|             "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", |             "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "Apache-2.0", |             "license": "Apache-2.0", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|  | @ -1539,19 +1554,6 @@ | ||||||
|                 "concat-map": "0.0.1" |                 "concat-map": "0.0.1" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@eslint/eslintrc/node_modules/globals": { |  | ||||||
|             "version": "14.0.0", |  | ||||||
|             "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", |  | ||||||
|             "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", |  | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |  | ||||||
|             "engines": { |  | ||||||
|                 "node": ">=18" |  | ||||||
|             }, |  | ||||||
|             "funding": { |  | ||||||
|                 "url": "https://github.com/sponsors/sindresorhus" |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "node_modules/@eslint/eslintrc/node_modules/ignore": { |         "node_modules/@eslint/eslintrc/node_modules/ignore": { | ||||||
|             "version": "5.3.2", |             "version": "5.3.2", | ||||||
|             "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", |             "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", | ||||||
|  | @ -1576,13 +1578,16 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@eslint/js": { |         "node_modules/@eslint/js": { | ||||||
|             "version": "9.26.0", |             "version": "9.27.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", |             "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", | ||||||
|             "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", |             "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" |                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" | ||||||
|  |             }, | ||||||
|  |             "funding": { | ||||||
|  |                 "url": "https://eslint.org/donate" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@eslint/object-schema": { |         "node_modules/@eslint/object-schema": { | ||||||
|  | @ -1596,13 +1601,13 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@eslint/plugin-kit": { |         "node_modules/@eslint/plugin-kit": { | ||||||
|             "version": "0.2.8", |             "version": "0.3.1", | ||||||
|             "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", |             "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", | ||||||
|             "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", |             "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "Apache-2.0", |             "license": "Apache-2.0", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@eslint/core": "^0.13.0", |                 "@eslint/core": "^0.14.0", | ||||||
|                 "levn": "^0.4.1" |                 "levn": "^0.4.1" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|  | @ -2075,28 +2080,6 @@ | ||||||
|                 "@mikro-orm/core": "^6.0.0" |                 "@mikro-orm/core": "^6.0.0" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@modelcontextprotocol/sdk": { |  | ||||||
|             "version": "1.11.2", |  | ||||||
|             "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz", |  | ||||||
|             "integrity": "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==", |  | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |  | ||||||
|             "dependencies": { |  | ||||||
|                 "content-type": "^1.0.5", |  | ||||||
|                 "cors": "^2.8.5", |  | ||||||
|                 "cross-spawn": "^7.0.3", |  | ||||||
|                 "eventsource": "^3.0.2", |  | ||||||
|                 "express": "^5.0.1", |  | ||||||
|                 "express-rate-limit": "^7.5.0", |  | ||||||
|                 "pkce-challenge": "^5.0.0", |  | ||||||
|                 "raw-body": "^3.0.0", |  | ||||||
|                 "zod": "^3.23.8", |  | ||||||
|                 "zod-to-json-schema": "^3.24.1" |  | ||||||
|             }, |  | ||||||
|             "engines": { |  | ||||||
|                 "node": ">=18" |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "node_modules/@napi-rs/snappy-android-arm-eabi": { |         "node_modules/@napi-rs/snappy-android-arm-eabi": { | ||||||
|             "version": "7.2.2", |             "version": "7.2.2", | ||||||
|             "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm-eabi/-/snappy-android-arm-eabi-7.2.2.tgz", |             "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm-eabi/-/snappy-android-arm-eabi-7.2.2.tgz", | ||||||
|  | @ -3107,6 +3090,16 @@ | ||||||
|                 "@types/node": "*" |                 "@types/node": "*" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/@types/memjs": { | ||||||
|  |             "version": "1.3.3", | ||||||
|  |             "resolved": "https://registry.npmjs.org/@types/memjs/-/memjs-1.3.3.tgz", | ||||||
|  |             "integrity": "sha512-OqZuZ2T0ErrygA/WUqQjmsB1GpxAt84OCgKPKbsSWwYKA/oe/lTiqvqMgFOv/Ngl8p+nytgdhrdxJtikOgimCg==", | ||||||
|  |             "dev": true, | ||||||
|  |             "license": "MIT", | ||||||
|  |             "dependencies": { | ||||||
|  |                 "@types/node": "*" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         "node_modules/@types/mime": { |         "node_modules/@types/mime": { | ||||||
|             "version": "1.3.5", |             "version": "1.3.5", | ||||||
|             "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", |             "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", | ||||||
|  | @ -3127,9 +3120,9 @@ | ||||||
|             "license": "MIT" |             "license": "MIT" | ||||||
|         }, |         }, | ||||||
|         "node_modules/@types/node": { |         "node_modules/@types/node": { | ||||||
|             "version": "22.15.17", |             "version": "22.15.18", | ||||||
|             "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", |             "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz", | ||||||
|             "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", |             "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "undici-types": "~6.21.0" |                 "undici-types": "~6.21.0" | ||||||
|  | @ -3611,30 +3604,30 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@volar/language-core": { |         "node_modules/@volar/language-core": { | ||||||
|             "version": "2.4.13", |             "version": "2.4.14", | ||||||
|             "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.13.tgz", |             "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.14.tgz", | ||||||
|             "integrity": "sha512-MnQJ7eKchJx5Oz+YdbqyFUk8BN6jasdJv31n/7r6/WwlOOv7qzvot6B66887l2ST3bUW4Mewml54euzpJWA6bg==", |             "integrity": "sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@volar/source-map": "2.4.13" |                 "@volar/source-map": "2.4.14" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@volar/source-map": { |         "node_modules/@volar/source-map": { | ||||||
|             "version": "2.4.13", |             "version": "2.4.14", | ||||||
|             "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.13.tgz", |             "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.14.tgz", | ||||||
|             "integrity": "sha512-l/EBcc2FkvHgz2ZxV+OZK3kMSroMr7nN3sZLF2/f6kWW66q8+tEL4giiYyFjt0BcubqJhBt6soYIrAPhg/Yr+Q==", |             "integrity": "sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT" |             "license": "MIT" | ||||||
|         }, |         }, | ||||||
|         "node_modules/@volar/typescript": { |         "node_modules/@volar/typescript": { | ||||||
|             "version": "2.4.13", |             "version": "2.4.14", | ||||||
|             "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.13.tgz", |             "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.14.tgz", | ||||||
|             "integrity": "sha512-Ukz4xv84swJPupZeoFsQoeJEOm7U9pqsEnaGGgt5ni3SCTa22m8oJP5Nng3Wed7Uw5RBELdLxxORX8YhJPyOgQ==", |             "integrity": "sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@volar/language-core": "2.4.13", |                 "@volar/language-core": "2.4.14", | ||||||
|                 "path-browserify": "^1.0.1", |                 "path-browserify": "^1.0.1", | ||||||
|                 "vscode-uri": "^3.0.8" |                 "vscode-uri": "^3.0.8" | ||||||
|             } |             } | ||||||
|  | @ -3693,16 +3686,16 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@vue/compiler-core": { |         "node_modules/@vue/compiler-core": { | ||||||
|             "version": "3.5.13", |             "version": "3.5.14", | ||||||
|             "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", |             "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.14.tgz", | ||||||
|             "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", |             "integrity": "sha512-k7qMHMbKvoCXIxPhquKQVw3Twid3Kg4s7+oYURxLGRd56LiuHJVrvFKI4fm2AM3c8apqODPfVJGoh8nePbXMRA==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@babel/parser": "^7.25.3", |                 "@babel/parser": "^7.27.2", | ||||||
|                 "@vue/shared": "3.5.13", |                 "@vue/shared": "3.5.14", | ||||||
|                 "entities": "^4.5.0", |                 "entities": "^4.5.0", | ||||||
|                 "estree-walker": "^2.0.2", |                 "estree-walker": "^2.0.2", | ||||||
|                 "source-map-js": "^1.2.0" |                 "source-map-js": "^1.2.1" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@vue/compiler-core/node_modules/entities": { |         "node_modules/@vue/compiler-core/node_modules/entities": { | ||||||
|  | @ -3718,40 +3711,40 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@vue/compiler-dom": { |         "node_modules/@vue/compiler-dom": { | ||||||
|             "version": "3.5.13", |             "version": "3.5.14", | ||||||
|             "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", |             "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.14.tgz", | ||||||
|             "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", |             "integrity": "sha512-1aOCSqxGOea5I80U2hQJvXYpPm/aXo95xL/m/mMhgyPUsKe9jhjwWpziNAw7tYRnbz1I61rd9Mld4W9KmmRoug==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@vue/compiler-core": "3.5.13", |                 "@vue/compiler-core": "3.5.14", | ||||||
|                 "@vue/shared": "3.5.13" |                 "@vue/shared": "3.5.14" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@vue/compiler-sfc": { |         "node_modules/@vue/compiler-sfc": { | ||||||
|             "version": "3.5.13", |             "version": "3.5.14", | ||||||
|             "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", |             "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.14.tgz", | ||||||
|             "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", |             "integrity": "sha512-9T6m/9mMr81Lj58JpzsiSIjBgv2LiVoWjIVa7kuXHICUi8LiDSIotMpPRXYJsXKqyARrzjT24NAwttrMnMaCXA==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@babel/parser": "^7.25.3", |                 "@babel/parser": "^7.27.2", | ||||||
|                 "@vue/compiler-core": "3.5.13", |                 "@vue/compiler-core": "3.5.14", | ||||||
|                 "@vue/compiler-dom": "3.5.13", |                 "@vue/compiler-dom": "3.5.14", | ||||||
|                 "@vue/compiler-ssr": "3.5.13", |                 "@vue/compiler-ssr": "3.5.14", | ||||||
|                 "@vue/shared": "3.5.13", |                 "@vue/shared": "3.5.14", | ||||||
|                 "estree-walker": "^2.0.2", |                 "estree-walker": "^2.0.2", | ||||||
|                 "magic-string": "^0.30.11", |                 "magic-string": "^0.30.17", | ||||||
|                 "postcss": "^8.4.48", |                 "postcss": "^8.5.3", | ||||||
|                 "source-map-js": "^1.2.0" |                 "source-map-js": "^1.2.1" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@vue/compiler-ssr": { |         "node_modules/@vue/compiler-ssr": { | ||||||
|             "version": "3.5.13", |             "version": "3.5.14", | ||||||
|             "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", |             "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.14.tgz", | ||||||
|             "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", |             "integrity": "sha512-Y0G7PcBxr1yllnHuS/NxNCSPWnRGH4Ogrp0tsLA5QemDZuJLs99YjAKQ7KqkHE0vCg4QTKlQzXLKCMF7WPSl7Q==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@vue/compiler-dom": "3.5.13", |                 "@vue/compiler-dom": "3.5.14", | ||||||
|                 "@vue/shared": "3.5.13" |                 "@vue/shared": "3.5.14" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@vue/compiler-vue2": { |         "node_modules/@vue/compiler-vue2": { | ||||||
|  | @ -3882,53 +3875,53 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@vue/reactivity": { |         "node_modules/@vue/reactivity": { | ||||||
|             "version": "3.5.13", |             "version": "3.5.14", | ||||||
|             "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", |             "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.14.tgz", | ||||||
|             "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", |             "integrity": "sha512-7cK1Hp343Fu/SUCCO52vCabjvsYu7ZkOqyYu7bXV9P2yyfjUMUXHZafEbq244sP7gf+EZEz+77QixBTuEqkQQw==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@vue/shared": "3.5.13" |                 "@vue/shared": "3.5.14" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@vue/runtime-core": { |         "node_modules/@vue/runtime-core": { | ||||||
|             "version": "3.5.13", |             "version": "3.5.14", | ||||||
|             "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", |             "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.14.tgz", | ||||||
|             "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", |             "integrity": "sha512-w9JWEANwHXNgieAhxPpEpJa+0V5G0hz3NmjAZwlOebtfKyp2hKxKF0+qSh0Xs6/PhfGihuSdqMprMVcQU/E6ag==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@vue/reactivity": "3.5.13", |                 "@vue/reactivity": "3.5.14", | ||||||
|                 "@vue/shared": "3.5.13" |                 "@vue/shared": "3.5.14" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@vue/runtime-dom": { |         "node_modules/@vue/runtime-dom": { | ||||||
|             "version": "3.5.13", |             "version": "3.5.14", | ||||||
|             "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", |             "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.14.tgz", | ||||||
|             "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", |             "integrity": "sha512-lCfR++IakeI35TVR80QgOelsUIdcKjd65rWAMfdSlCYnaEY5t3hYwru7vvcWaqmrK+LpI7ZDDYiGU5V3xjMacw==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@vue/reactivity": "3.5.13", |                 "@vue/reactivity": "3.5.14", | ||||||
|                 "@vue/runtime-core": "3.5.13", |                 "@vue/runtime-core": "3.5.14", | ||||||
|                 "@vue/shared": "3.5.13", |                 "@vue/shared": "3.5.14", | ||||||
|                 "csstype": "^3.1.3" |                 "csstype": "^3.1.3" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@vue/server-renderer": { |         "node_modules/@vue/server-renderer": { | ||||||
|             "version": "3.5.13", |             "version": "3.5.14", | ||||||
|             "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", |             "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.14.tgz", | ||||||
|             "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", |             "integrity": "sha512-Rf/ISLqokIvcySIYnv3tNWq40PLpNLDLSJwwVWzG6MNtyIhfbcrAxo5ZL9nARJhqjZyWWa40oRb2IDuejeuv6w==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@vue/compiler-ssr": "3.5.13", |                 "@vue/compiler-ssr": "3.5.14", | ||||||
|                 "@vue/shared": "3.5.13" |                 "@vue/shared": "3.5.14" | ||||||
|             }, |             }, | ||||||
|             "peerDependencies": { |             "peerDependencies": { | ||||||
|                 "vue": "3.5.13" |                 "vue": "3.5.14" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@vue/shared": { |         "node_modules/@vue/shared": { | ||||||
|             "version": "3.5.13", |             "version": "3.5.14", | ||||||
|             "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", |             "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.14.tgz", | ||||||
|             "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", |             "integrity": "sha512-oXTwNxVfc9EtP1zzXAlSlgARLXNC84frFYkS0HHz0h3E4WZSP9sywqjqzGCP9Y34M8ipNmd380pVgmMuwELDyQ==", | ||||||
|             "license": "MIT" |             "license": "MIT" | ||||||
|         }, |         }, | ||||||
|         "node_modules/@vue/test-utils": { |         "node_modules/@vue/test-utils": { | ||||||
|  | @ -3962,14 +3955,14 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@vueuse/core": { |         "node_modules/@vueuse/core": { | ||||||
|             "version": "13.1.0", |             "version": "13.2.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.1.0.tgz", |             "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.2.0.tgz", | ||||||
|             "integrity": "sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q==", |             "integrity": "sha512-n5TZoIAxbWAQ3PqdVPDzLgIRQOujFfMlatdI+f7ditSmoEeNpPBvp7h2zamzikCmrhFIePAwdEQB6ENccHr7Rg==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@types/web-bluetooth": "^0.0.21", |                 "@types/web-bluetooth": "^0.0.21", | ||||||
|                 "@vueuse/metadata": "13.1.0", |                 "@vueuse/metadata": "13.2.0", | ||||||
|                 "@vueuse/shared": "13.1.0" |                 "@vueuse/shared": "13.2.0" | ||||||
|             }, |             }, | ||||||
|             "funding": { |             "funding": { | ||||||
|                 "url": "https://github.com/sponsors/antfu" |                 "url": "https://github.com/sponsors/antfu" | ||||||
|  | @ -3979,18 +3972,18 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@vueuse/metadata": { |         "node_modules/@vueuse/metadata": { | ||||||
|             "version": "13.1.0", |             "version": "13.2.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.1.0.tgz", |             "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.2.0.tgz", | ||||||
|             "integrity": "sha512-+TDd7/a78jale5YbHX9KHW3cEDav1lz1JptwDvep2zSG8XjCsVE+9mHIzjTOaPbHUAk5XiE4jXLz51/tS+aKQw==", |             "integrity": "sha512-kPpzuQCU0+D8DZCzK0iPpIcXI+6ufWSgwnjJ6//GNpEn+SHViaCtR+XurzORChSgvpHO9YC8gGM97Y1kB+UabA==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "funding": { |             "funding": { | ||||||
|                 "url": "https://github.com/sponsors/antfu" |                 "url": "https://github.com/sponsors/antfu" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@vueuse/shared": { |         "node_modules/@vueuse/shared": { | ||||||
|             "version": "13.1.0", |             "version": "13.2.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.1.0.tgz", |             "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.2.0.tgz", | ||||||
|             "integrity": "sha512-IVS/qRRjhPTZ6C2/AM3jieqXACGwFZwWTdw5sNTSKk2m/ZpkuuN+ri+WCVUP8TqaKwJYt/KuMwmXspMAw8E6ew==", |             "integrity": "sha512-vx9ZPDF5HcU9up3Jgt3G62dMUfZEdk6tLyBAHYAG4F4n73vpaA7J5hdncDI/lS9Vm7GA/FPlbOmh9TrDZROTpg==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "funding": { |             "funding": { | ||||||
|                 "url": "https://github.com/sponsors/antfu" |                 "url": "https://github.com/sponsors/antfu" | ||||||
|  | @ -5448,9 +5441,9 @@ | ||||||
|             "license": "MIT" |             "license": "MIT" | ||||||
|         }, |         }, | ||||||
|         "node_modules/electron-to-chromium": { |         "node_modules/electron-to-chromium": { | ||||||
|             "version": "1.5.152", |             "version": "1.5.155", | ||||||
|             "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.152.tgz", |             "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", | ||||||
|             "integrity": "sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==", |             "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "ISC" |             "license": "ISC" | ||||||
|         }, |         }, | ||||||
|  | @ -5656,9 +5649,9 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/eslint": { |         "node_modules/eslint": { | ||||||
|             "version": "9.26.0", |             "version": "9.27.0", | ||||||
|             "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", |             "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", | ||||||
|             "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", |             "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|  | @ -5666,14 +5659,13 @@ | ||||||
|                 "@eslint-community/regexpp": "^4.12.1", |                 "@eslint-community/regexpp": "^4.12.1", | ||||||
|                 "@eslint/config-array": "^0.20.0", |                 "@eslint/config-array": "^0.20.0", | ||||||
|                 "@eslint/config-helpers": "^0.2.1", |                 "@eslint/config-helpers": "^0.2.1", | ||||||
|                 "@eslint/core": "^0.13.0", |                 "@eslint/core": "^0.14.0", | ||||||
|                 "@eslint/eslintrc": "^3.3.1", |                 "@eslint/eslintrc": "^3.3.1", | ||||||
|                 "@eslint/js": "9.26.0", |                 "@eslint/js": "9.27.0", | ||||||
|                 "@eslint/plugin-kit": "^0.2.8", |                 "@eslint/plugin-kit": "^0.3.1", | ||||||
|                 "@humanfs/node": "^0.16.6", |                 "@humanfs/node": "^0.16.6", | ||||||
|                 "@humanwhocodes/module-importer": "^1.0.1", |                 "@humanwhocodes/module-importer": "^1.0.1", | ||||||
|                 "@humanwhocodes/retry": "^0.4.2", |                 "@humanwhocodes/retry": "^0.4.2", | ||||||
|                 "@modelcontextprotocol/sdk": "^1.8.0", |  | ||||||
|                 "@types/estree": "^1.0.6", |                 "@types/estree": "^1.0.6", | ||||||
|                 "@types/json-schema": "^7.0.15", |                 "@types/json-schema": "^7.0.15", | ||||||
|                 "ajv": "^6.12.4", |                 "ajv": "^6.12.4", | ||||||
|  | @ -5697,8 +5689,7 @@ | ||||||
|                 "lodash.merge": "^4.6.2", |                 "lodash.merge": "^4.6.2", | ||||||
|                 "minimatch": "^3.1.2", |                 "minimatch": "^3.1.2", | ||||||
|                 "natural-compare": "^1.4.0", |                 "natural-compare": "^1.4.0", | ||||||
|                 "optionator": "^0.9.3", |                 "optionator": "^0.9.3" | ||||||
|                 "zod": "^3.24.2" |  | ||||||
|             }, |             }, | ||||||
|             "bin": { |             "bin": { | ||||||
|                 "eslint": "bin/eslint.js" |                 "eslint": "bin/eslint.js" | ||||||
|  | @ -6105,29 +6096,6 @@ | ||||||
|                 "node": ">= 0.6" |                 "node": ">= 0.6" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/eventsource": { |  | ||||||
|             "version": "3.0.7", |  | ||||||
|             "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", |  | ||||||
|             "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", |  | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |  | ||||||
|             "dependencies": { |  | ||||||
|                 "eventsource-parser": "^3.0.1" |  | ||||||
|             }, |  | ||||||
|             "engines": { |  | ||||||
|                 "node": ">=18.0.0" |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "node_modules/eventsource-parser": { |  | ||||||
|             "version": "3.0.1", |  | ||||||
|             "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", |  | ||||||
|             "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", |  | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |  | ||||||
|             "engines": { |  | ||||||
|                 "node": ">=18.0.0" |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "node_modules/execa": { |         "node_modules/execa": { | ||||||
|             "version": "9.5.3", |             "version": "9.5.3", | ||||||
|             "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.3.tgz", |             "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.3.tgz", | ||||||
|  | @ -6242,22 +6210,6 @@ | ||||||
|                 "node": ">= 8.0.0" |                 "node": ">= 8.0.0" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/express-rate-limit": { |  | ||||||
|             "version": "7.5.0", |  | ||||||
|             "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", |  | ||||||
|             "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", |  | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |  | ||||||
|             "engines": { |  | ||||||
|                 "node": ">= 16" |  | ||||||
|             }, |  | ||||||
|             "funding": { |  | ||||||
|                 "url": "https://github.com/sponsors/express-rate-limit" |  | ||||||
|             }, |  | ||||||
|             "peerDependencies": { |  | ||||||
|                 "express": "^4.11 || 5 || ^5.0.0-beta.1" |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "node_modules/express-unless": { |         "node_modules/express-unless": { | ||||||
|             "version": "2.1.3", |             "version": "2.1.3", | ||||||
|             "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", |             "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", | ||||||
|  | @ -6861,9 +6813,9 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/globals": { |         "node_modules/globals": { | ||||||
|             "version": "15.15.0", |             "version": "14.0.0", | ||||||
|             "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", |             "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", | ||||||
|             "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", |             "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "engines": { |             "engines": { | ||||||
|  | @ -8354,6 +8306,15 @@ | ||||||
|                 "node": ">= 0.8" |                 "node": ">= 0.8" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/memjs": { | ||||||
|  |             "version": "1.3.2", | ||||||
|  |             "resolved": "https://registry.npmjs.org/memjs/-/memjs-1.3.2.tgz", | ||||||
|  |             "integrity": "sha512-qUEg2g8vxPe+zPn09KidjIStHPtoBO8Cttm8bgJFWWabbsjQ9Av9Ky+6UcvKx6ue0LLb/LEhtcyQpRyKfzeXcg==", | ||||||
|  |             "license": "MIT", | ||||||
|  |             "engines": { | ||||||
|  |                 "node": ">=0.10.0" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         "node_modules/memoize-one": { |         "node_modules/memoize-one": { | ||||||
|             "version": "6.0.0", |             "version": "6.0.0", | ||||||
|             "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", |             "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", | ||||||
|  | @ -9504,16 +9465,6 @@ | ||||||
|                 "node": ">=0.10" |                 "node": ">=0.10" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/pkce-challenge": { |  | ||||||
|             "version": "5.0.0", |  | ||||||
|             "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", |  | ||||||
|             "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", |  | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |  | ||||||
|             "engines": { |  | ||||||
|                 "node": ">=16.20.0" |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "node_modules/playwright": { |         "node_modules/playwright": { | ||||||
|             "version": "1.50.1", |             "version": "1.50.1", | ||||||
|             "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz", |             "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz", | ||||||
|  | @ -9759,9 +9710,9 @@ | ||||||
|             "license": "ISC" |             "license": "ISC" | ||||||
|         }, |         }, | ||||||
|         "node_modules/protobufjs": { |         "node_modules/protobufjs": { | ||||||
|             "version": "7.5.1", |             "version": "7.5.2", | ||||||
|             "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.1.tgz", |             "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.2.tgz", | ||||||
|             "integrity": "sha512-3qx3IRjR9WPQKagdwrKjO3Gu8RgQR2qqw+1KnigWhoVjFqegIj1K3bP11sGqhxrO46/XL7lekuG4jmjL+4cLsw==", |             "integrity": "sha512-f2ls6rpO6G153Cy+o2XQ+Y0sARLOZ17+OGVLHrc3VUKcLHYKEKWbkSujdBWQXM7gKn5NTfp0XnRPZn1MIu8n9w==", | ||||||
|             "hasInstallScript": true, |             "hasInstallScript": true, | ||||||
|             "license": "BSD-3-Clause", |             "license": "BSD-3-Clause", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|  | @ -10966,9 +10917,9 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/svelte": { |         "node_modules/svelte": { | ||||||
|             "version": "5.28.6", |             "version": "5.30.1", | ||||||
|             "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.28.6.tgz", |             "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.30.1.tgz", | ||||||
|             "integrity": "sha512-9qqr7mw8YR9PAnxGFfzCK6PUlNGtns7wVavrhnxyf3fpB1mP/Ol55Z2UnIapsSzNNl3k9qw7cZ22PdE8+xT/jQ==", |             "integrity": "sha512-QIYtKnJGkubWXtNkrUBKVCvyo9gjcccdbnvXfwsGNhvbeNNdQjRDTa/BiQcJ2kWXbXPQbWKyT7CUu53KIj1rfw==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@ampproject/remapping": "^2.3.0", |                 "@ampproject/remapping": "^2.3.0", | ||||||
|  | @ -11093,13 +11044,13 @@ | ||||||
|             "license": "MIT" |             "license": "MIT" | ||||||
|         }, |         }, | ||||||
|         "node_modules/synckit": { |         "node_modules/synckit": { | ||||||
|             "version": "0.11.4", |             "version": "0.11.5", | ||||||
|             "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", |             "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.5.tgz", | ||||||
|             "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", |             "integrity": "sha512-frqvfWyDA5VPVdrWfH24uM6SI/O8NLpVbIIJxb8t/a3YGsp4AW9CYgSKC0OaSEfexnp7Y1pVh2Y6IHO8ggGDmA==", | ||||||
|             "dev": true, |             "dev": true, | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@pkgr/core": "^0.2.3", |                 "@pkgr/core": "^0.2.4", | ||||||
|                 "tslib": "^2.8.1" |                 "tslib": "^2.8.1" | ||||||
|             }, |             }, | ||||||
|             "engines": { |             "engines": { | ||||||
|  | @ -12118,16 +12069,16 @@ | ||||||
|             "license": "MIT" |             "license": "MIT" | ||||||
|         }, |         }, | ||||||
|         "node_modules/vue": { |         "node_modules/vue": { | ||||||
|             "version": "3.5.13", |             "version": "3.5.14", | ||||||
|             "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", |             "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.14.tgz", | ||||||
|             "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", |             "integrity": "sha512-LbOm50/vZFG6Mhy6KscQYXZMQ0LMCC/y40HDJPPvGFQ+i/lUH+PJHR6C3assgOQiXdl6tAfsXHbXYVBZZu65ew==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "@vue/compiler-dom": "3.5.13", |                 "@vue/compiler-dom": "3.5.14", | ||||||
|                 "@vue/compiler-sfc": "3.5.13", |                 "@vue/compiler-sfc": "3.5.14", | ||||||
|                 "@vue/runtime-dom": "3.5.13", |                 "@vue/runtime-dom": "3.5.14", | ||||||
|                 "@vue/server-renderer": "3.5.13", |                 "@vue/server-renderer": "3.5.14", | ||||||
|                 "@vue/shared": "3.5.13" |                 "@vue/shared": "3.5.14" | ||||||
|             }, |             }, | ||||||
|             "peerDependencies": { |             "peerDependencies": { | ||||||
|                 "typescript": "*" |                 "typescript": "*" | ||||||
|  | @ -12262,9 +12213,9 @@ | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/vuetify": { |         "node_modules/vuetify": { | ||||||
|             "version": "3.8.4", |             "version": "3.8.5", | ||||||
|             "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.8.4.tgz", |             "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.8.5.tgz", | ||||||
|             "integrity": "sha512-hfA1eqA6vhrF5LF8Yfk0uHdNUmh8Uckxn5wREiThO82HW/9Vfreh+IpxPgEtCsAhV33KW+NVamltQCu3HczRKw==", |             "integrity": "sha512-W/mTaNDyO6NRqAQmnkMUn9TYvRb//BPF/vk7h3+2xNJOyI9ev90JmYjrihOtb+6QDrB79wVUH0Y+0OjYK73GsA==", | ||||||
|             "license": "MIT", |             "license": "MIT", | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": "^12.20 || >=14.13" |                 "node": "^12.20 || >=14.13" | ||||||
|  | @ -12819,26 +12770,6 @@ | ||||||
|             "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", |             "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", | ||||||
|             "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", |             "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", | ||||||
|             "license": "MIT" |             "license": "MIT" | ||||||
|         }, |  | ||||||
|         "node_modules/zod": { |  | ||||||
|             "version": "3.24.4", |  | ||||||
|             "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", |  | ||||||
|             "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", |  | ||||||
|             "dev": true, |  | ||||||
|             "license": "MIT", |  | ||||||
|             "funding": { |  | ||||||
|                 "url": "https://github.com/sponsors/colinhacks" |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "node_modules/zod-to-json-schema": { |  | ||||||
|             "version": "3.24.5", |  | ||||||
|             "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", |  | ||||||
|             "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", |  | ||||||
|             "dev": true, |  | ||||||
|             "license": "ISC", |  | ||||||
|             "peerDependencies": { |  | ||||||
|                 "zod": "^3.24.1" |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 GitHub
							GitHub