diff --git a/.env.development b/.env.development index d726692ea..7fa9aa104 100644 --- a/.env.development +++ b/.env.development @@ -14,6 +14,8 @@ # Mana Core Auth Service MANA_CORE_AUTH_URL=http://localhost:3001 +# Service key for bot-to-auth communication (Matrix-SSO-Link) +MANA_CORE_SERVICE_KEY=dev-service-key-for-bot-sso-2024 # JWT Keys (shared across apps for token verification) JWT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDGRsOXROB4lprw\n9oXaOIt+cwHe3UxBOoiWiUXcpFuXwb+kBWn/LyjeCIOXtefOwE0S10JEodK+6foe\naqGHanq86qAmmkb4a8sjj5LAxXkHL35sJo8HaYcx5NkJQLxQSRHpTfdfxsKsKwxa\n4R4uqrvToqdo6tl/VMsGDPS8L7KzaiKaSdGugvlVtXWgV1soeXSUPyPwpyAXQg7h\nY4CkTSkJAplrs77RLdj8u6jbHKR3F7QkwiU1JocjhM1GP/suKiqXRu8omLFnu45C\ns09SNSRsOpNY5csrKA4PZ2LCks9VHH7HafFvB+BbRw4+Ssr6myOysAztqi3bZMRW\nLTakWpBbAgMBAAECggEAF5zi0IzaghHxhtkyYfrSRgSynX9+WYBRNu2ch8/SZqAj\neghOXMkZgAPEjtiSMDGqRsr4ReMoYtB2Qea8sOX8kwC1gj4Po1Mhtez0cwexclUf\nebLH3X/y9/1YiZJk5YImOMIuaoC/ELDvFOhIEhJcMbKREbIc+oiMcH6HgN0vViVh\nJptgHTnqnGHNARkEpf+xnxqJJxEgrEMz50b4fApKpoZsWXNnZ3Atc/i2ziGew5z4\npnGJxs9TWSukBZaQvl9iluBBvqmPkCOId+L7CmB44bNURpqQOm8gxEgLcdn06y5j\nIKee3Z4H6OTseFvSIYYqBqCyyyZWHICBZXUCDQKUbQKBgQDnFe+O+pQc5looLFiF\nxuYsfDtJqvoMgQ0BaVAo6wVpPe6w+1NA6ZxghcM0+8zyc70jZvdMXINhdsfWD5Gi\nJ/NEDI8EXJJKMfnFQ7F1Ad5NyTnnn/TsLda4GIGQznPRS6uxUP4ljFtxmU9G8Diz\nUQ47XsLjwzzbTedMTSYoQ46kdwKBgQDbp0dIq047o4A72/BBttKdZbgQmjFmqCXF\n8YRUquIDXh/CJ4OQwOIaOvk2398Rg53c3MsV+XCJaMmWYqnJ4BdITLsqeGKsczoS\nI0DMehDr++aOoX/f29r1c+7J/fV5jtAEUcwIEOR1vyAM+WdiWnnTvdpMPVUDsgaT\ntuH0E8WgPQKBgQCCINci87Z+Q7VXVAmRY7zwJhEY3eArNGzHc6+BKz+D0S1dmll6\nf1LhA9I2PuldSpGiovP1m08cjk/gGipPXyHdGxlaQmravyPA0urWUfQGZ59k8K1y\nZim4x4wGqEuN+4e2tT44lL5VzRhYgSPcznMuOaGTsrjNYiQy0mr/V3O25wKBgHvV\nryaVDaIp553XvXgO7ma2djNF+xv5KHKUWxqwzINBiX4YcOAnHlHTdbUuOcDSByoB\ngK1+16dgYGZccYTSxc2JFOw4usimndKj9WBSYT/p4G4BNuqqNKO1HKbceoxxq20E\nAJd7jpGjkxo9cb/Nammp22yoF0niEDsvG+xTSVOxAoGBAMfxHYCMdPc625upCbqG\nkPSJJGYREKGad80OtXilYXLvBPzV65q32k2YZGjaicPKRAzj72KO4nfIu9SY6bfO\nBvXCtIcvllZQuxyd3Cd8MirujJodKwThLTMd4bAYYMXGz1/W6R6pzunZs5KEpgEr\nczy9Gk9WNp0t8vfzyZZ9aago\n-----END PRIVATE KEY-----" @@ -220,6 +222,7 @@ CONTACTS_GOOGLE_REDIRECT_URI=http://localhost:5184/import?tab=google # ============================================ CALENDAR_BACKEND_PORT=3014 +CALENDAR_BACKEND_URL=http://localhost:3014 CALENDAR_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/calendar # Speech-to-Text Service (mana-stt) diff --git a/packages/bot-services/package.json b/packages/bot-services/package.json index 0caac5f04..36fdd806a 100644 --- a/packages/bot-services/package.json +++ b/packages/bot-services/package.json @@ -69,13 +69,15 @@ "dependencies": { "@nestjs/common": "^11.0.20", "@nestjs/config": "^4.0.2", - "date-fns": "^4.1.0" + "date-fns": "^4.1.0", + "ioredis": "^5.4.2" }, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/config": "^3.0.0 || ^4.0.0" }, "devDependencies": { + "@types/ioredis": "^5.0.0", "@types/node": "^24.10.1", "typescript": "^5.9.3" } diff --git a/packages/bot-services/src/calendar/calendar-api.service.ts b/packages/bot-services/src/calendar/calendar-api.service.ts new file mode 100644 index 000000000..f57faef8b --- /dev/null +++ b/packages/bot-services/src/calendar/calendar-api.service.ts @@ -0,0 +1,300 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { + CalendarEvent, + Calendar, + CreateEventInput, + UpdateEventInput, + ParsedEventInput, +} from './types'; +import { parseGermanDateKeyword, getTodayISO, addDays } from '../shared/utils'; + +/** + * Calendar API Service + * + * Connects to the calendar-backend API for event management. + * This service is used when the user is logged in and has a valid JWT token. + * + * @example + * ```typescript + * // Get events for a user (requires JWT token) + * const events = await calendarApiService.getEvents(token, { start: '2024-01-01', end: '2024-01-31' }); + * + * // Create an event + * const event = await calendarApiService.createEvent(token, { + * title: 'Meeting', + * startTime: new Date('2024-01-15T10:00:00'), + * endTime: new Date('2024-01-15T11:00:00'), + * }); + * ``` + */ +@Injectable() +export class CalendarApiService { + private readonly logger = new Logger(CalendarApiService.name); + private readonly baseUrl: string; + + constructor(baseUrl = 'http://localhost:3014') { + this.baseUrl = baseUrl; + this.logger.log(`Calendar API Service initialized with URL: ${baseUrl}`); + } + + // ===== Event Operations ===== + + /** + * Get events within a date range + */ + async getEvents( + token: string, + filter?: { start?: string; end?: string; calendarId?: string } + ): Promise { + try { + const params = new URLSearchParams(); + if (filter?.start) params.append('start', filter.start); + if (filter?.end) params.append('end', filter.end); + if (filter?.calendarId) params.append('calendarId', filter.calendarId); + + const response = await fetch(`${this.baseUrl}/api/v1/events?${params}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + const data = (await response.json()) as { events?: unknown[] }; + return this.mapApiEvents(data.events || []); + } catch (error) { + this.logger.error('Failed to get events:', error); + return []; + } + } + + /** + * Get today's events + */ + async getTodayEvents(token: string): Promise { + const today = getTodayISO(); + return this.getEvents(token, { start: today, end: today }); + } + + /** + * Get upcoming events (next 7 days) + */ + async getUpcomingEvents(token: string, days = 7): Promise { + const today = getTodayISO(); + const end = addDays(new Date(), days).toISOString().split('T')[0]; + return this.getEvents(token, { start: today, end }); + } + + /** + * Create a new event + */ + async createEvent(token: string, input: CreateEventInput): Promise { + try { + const body: Record = { + title: input.title, + startTime: + input.startTime instanceof Date ? input.startTime.toISOString() : input.startTime, + endTime: input.endTime instanceof Date ? input.endTime.toISOString() : input.endTime, + isAllDay: input.isAllDay || false, + }; + + if (input.description) body.description = input.description; + if (input.location) body.location = input.location; + if (input.calendarId) body.calendarId = input.calendarId; + + const response = await fetch(`${this.baseUrl}/api/v1/events`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + const data = (await response.json()) as { event: unknown }; + return this.mapApiEvent(data.event); + } catch (error) { + this.logger.error('Failed to create event:', error); + return null; + } + } + + /** + * Update an event + */ + async updateEvent( + token: string, + eventId: string, + input: UpdateEventInput + ): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/v1/events/${eventId}`, { + method: 'PUT', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(input), + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + const data = (await response.json()) as { event: unknown }; + return this.mapApiEvent(data.event); + } catch (error) { + this.logger.error('Failed to update event:', error); + return null; + } + } + + /** + * Delete an event + */ + async deleteEvent(token: string, eventId: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/v1/events/${eventId}`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${token}` }, + }); + + return response.ok; + } catch (error) { + this.logger.error('Failed to delete event:', error); + return false; + } + } + + // ===== Calendar Operations ===== + + /** + * Get all calendars + */ + async getCalendars(token: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/v1/calendars`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + const data = (await response.json()) as { calendars?: any[] }; + return (data.calendars || []).map((c: any) => ({ + id: c.id, + name: c.name, + color: c.color, + userId: c.userId || '', + isDefault: c.isDefault || false, + })); + } catch (error) { + this.logger.error('Failed to get calendars:', error); + return []; + } + } + + // ===== Parsing ===== + + /** + * Parse natural language event input + */ + parseEventInput(input: string): ParsedEventInput { + let title = input; + let startTime: Date | null = null; + let endTime: Date | null = null; + let isAllDay = false; + let location: string | null = null; + + // Extract date (@heute, @morgen, etc.) + const dateMatch = title.match(/@(\S+)/); + if (dateMatch) { + const dateStr = dateMatch[1].toLowerCase(); + const parsedDate = parseGermanDateKeyword(dateStr); + + if (parsedDate) { + // Default to 9:00-10:00 for the parsed date + startTime = new Date(`${parsedDate}T09:00:00`); + endTime = new Date(`${parsedDate}T10:00:00`); + } + title = title.replace(dateMatch[0], '').trim(); + } + + // Extract time (um 14 Uhr, 14:00, etc.) + const timeMatch = title.match(/(?:um\s+)?(\d{1,2})(?::(\d{2}))?\s*(?:uhr)?/i); + if (timeMatch) { + const hours = parseInt(timeMatch[1]); + const minutes = timeMatch[2] ? parseInt(timeMatch[2]) : 0; + + if (startTime) { + startTime.setHours(hours, minutes, 0, 0); + endTime = new Date(startTime); + endTime.setHours(hours + 1); // Default 1 hour duration + } else { + // If no date specified, assume today + startTime = new Date(); + startTime.setHours(hours, minutes, 0, 0); + endTime = new Date(startTime); + endTime.setHours(hours + 1); + } + title = title.replace(timeMatch[0], '').trim(); + } + + // Extract location (in ...) + const locationMatch = title.match(/\bin\s+([^,]+)/i); + if (locationMatch) { + location = locationMatch[1].trim(); + title = title.replace(locationMatch[0], '').trim(); + } + + // If no time specified, treat as all-day event + if (!startTime) { + startTime = new Date(); + startTime.setHours(0, 0, 0, 0); + endTime = new Date(startTime); + endTime.setHours(23, 59, 59, 999); + isAllDay = true; + } + + return { + title: title.trim(), + startTime: startTime!, + endTime: endTime!, + isAllDay, + location, + }; + } + + // ===== Private Helpers ===== + + /** + * Map API event format to internal CalendarEvent format + */ + private mapApiEvent(apiEvent: any): CalendarEvent { + return { + id: apiEvent.id, + userId: apiEvent.userId || '', + calendarId: apiEvent.calendarId, + calendarName: apiEvent.calendar?.name || 'Kalender', + title: apiEvent.title, + description: apiEvent.description || null, + location: apiEvent.location || null, + startTime: apiEvent.startTime, + endTime: apiEvent.endTime, + isAllDay: apiEvent.isAllDay || false, + createdAt: apiEvent.createdAt, + }; + } + + /** + * Map array of API events + */ + private mapApiEvents(apiEvents: any[]): CalendarEvent[] { + return apiEvents.map((e) => this.mapApiEvent(e)); + } +} diff --git a/packages/bot-services/src/calendar/calendar.service.ts b/packages/bot-services/src/calendar/calendar.service.ts index 7b9299def..f07de4130 100644 --- a/packages/bot-services/src/calendar/calendar.service.ts +++ b/packages/bot-services/src/calendar/calendar.service.ts @@ -345,6 +345,6 @@ export class CalendarService implements OnModuleInit { // Clean up title title = title.replace(/\s+/g, ' ').trim(); - return { title, startTime, endTime, isAllDay }; + return { title, startTime, endTime, isAllDay, location: null }; } } diff --git a/packages/bot-services/src/calendar/index.ts b/packages/bot-services/src/calendar/index.ts index 4c713ba1f..4c847d28f 100644 --- a/packages/bot-services/src/calendar/index.ts +++ b/packages/bot-services/src/calendar/index.ts @@ -1,8 +1,9 @@ // Module export { CalendarModule, CalendarModuleOptions } from './calendar.module'; -// Service +// Services export { CalendarService, CALENDAR_STORAGE_PROVIDER } from './calendar.service'; +export { CalendarApiService } from './calendar-api.service'; // Types export * from './types'; diff --git a/packages/bot-services/src/calendar/types.ts b/packages/bot-services/src/calendar/types.ts index a469b56a4..ed16e96df 100644 --- a/packages/bot-services/src/calendar/types.ts +++ b/packages/bot-services/src/calendar/types.ts @@ -75,4 +75,5 @@ export interface ParsedEventInput { startTime: Date | null; endTime: Date | null; isAllDay: boolean; + location: string | null; } diff --git a/packages/bot-services/src/index.ts b/packages/bot-services/src/index.ts index 2e448db1f..ff1930bc1 100644 --- a/packages/bot-services/src/index.ts +++ b/packages/bot-services/src/index.ts @@ -27,7 +27,13 @@ // ===== Core Services ===== // Todo -export { TodoModule, TodoModuleOptions, TodoService, TODO_STORAGE_PROVIDER } from './todo/index.js'; +export { + TodoModule, + TodoModuleOptions, + TodoService, + TODO_STORAGE_PROVIDER, + TodoApiService, +} from './todo/index.js'; export type { Task, Project, @@ -44,6 +50,7 @@ export { CalendarModule, CalendarModuleOptions, CalendarService, + CalendarApiService, CALENDAR_STORAGE_PROVIDER, } from './calendar/index.js'; export type { @@ -88,13 +95,26 @@ export type { export { SessionModule, SessionService, + RedisSessionProvider, + REDIS_SESSION_PROVIDER, + REDIS_CLIENT, SESSION_MODULE_OPTIONS, DEFAULT_SESSION_EXPIRY_MS, } from './session/index.js'; -export type { UserSession, LoginResult, SessionStats, SessionModuleOptions } from './session/index.js'; +export type { + UserSession, + LoginResult, + SessionStats, + SessionModuleOptions, + SessionStorageMode, +} from './session/index.js'; // Transcription (Speech-to-Text via mana-stt) -export { TranscriptionModule, TranscriptionService, STT_MODULE_OPTIONS } from './transcription/index.js'; +export { + TranscriptionModule, + TranscriptionService, + STT_MODULE_OPTIONS, +} from './transcription/index.js'; export type { SttResponse, TranscriptionOptions, @@ -102,7 +122,12 @@ export type { } from './transcription/index.js'; // Credit (Credit balance and formatting for Matrix bots) -export { CreditModule, CreditService, CREDIT_MODULE_OPTIONS, CreditErrorCode } from './credit/index.js'; +export { + CreditModule, + CreditService, + CREDIT_MODULE_OPTIONS, + CreditErrorCode, +} from './credit/index.js'; export type { CreditBalance, CreditValidationResult, diff --git a/packages/bot-services/src/session/index.ts b/packages/bot-services/src/session/index.ts index c3203d6d5..f5917f8d7 100644 --- a/packages/bot-services/src/session/index.ts +++ b/packages/bot-services/src/session/index.ts @@ -1,4 +1,11 @@ -export { SessionService } from './session.service'; +export { SessionService, REDIS_SESSION_PROVIDER } from './session.service'; export { SessionModule } from './session.module'; -export type { UserSession, LoginResult, SessionStats, SessionModuleOptions } from './types'; +export { RedisSessionProvider, REDIS_CLIENT } from './redis-session.provider'; +export type { + UserSession, + LoginResult, + SessionStats, + SessionModuleOptions, + SessionStorageMode, +} from './types'; export { SESSION_MODULE_OPTIONS, DEFAULT_SESSION_EXPIRY_MS } from './types'; diff --git a/packages/bot-services/src/session/redis-session.provider.ts b/packages/bot-services/src/session/redis-session.provider.ts new file mode 100644 index 000000000..8606aa638 --- /dev/null +++ b/packages/bot-services/src/session/redis-session.provider.ts @@ -0,0 +1,245 @@ +import { + Injectable, + Logger, + OnModuleInit, + OnModuleDestroy, + Inject, + Optional, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import Redis from 'ioredis'; +import { + UserSession, + SessionModuleOptions, + SESSION_MODULE_OPTIONS, + DEFAULT_SESSION_EXPIRY_MS, +} from './types'; + +/** + * Injection token for Redis client + */ +export const REDIS_CLIENT = 'REDIS_CLIENT'; + +/** + * Key prefix for bot sessions in Redis + */ +const KEY_PREFIX = 'bot:session:'; + +/** + * Redis-based session provider for cross-bot SSO + * + * Sessions are stored in Redis with automatic TTL expiration. + * All bots using this provider share the same session store. + * + * @example + * ```typescript + * // User logs in via todo-bot + * await sessionProvider.setSession('@user:matrix.mana.how', session); + * + * // Same user in picture-bot - already logged in! + * const session = await sessionProvider.getSession('@user:matrix.mana.how'); + * ``` + */ +@Injectable() +export class RedisSessionProvider implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(RedisSessionProvider.name); + private client: Redis | null = null; + private readonly sessionExpirySeconds: number; + + constructor( + @Optional() private configService: ConfigService, + @Optional() @Inject(SESSION_MODULE_OPTIONS) private options?: SessionModuleOptions + ) { + const expiryMs = options?.sessionExpiryMs || DEFAULT_SESSION_EXPIRY_MS; + this.sessionExpirySeconds = Math.floor(expiryMs / 1000); + } + + async onModuleInit() { + const host = + this.options?.redisHost || this.configService?.get('REDIS_HOST', 'localhost'); + const port = this.options?.redisPort || this.configService?.get('REDIS_PORT', 6379); + const password = + this.options?.redisPassword || this.configService?.get('REDIS_PASSWORD'); + + try { + this.client = new Redis({ + host, + port, + password: password || undefined, + retryStrategy: (times) => { + if (times > 3) { + this.logger.warn('Redis connection failed, falling back to in-memory sessions'); + return null; + } + return Math.min(times * 200, 2000); + }, + maxRetriesPerRequest: 1, + }); + + this.client.on('error', (err) => { + this.logger.error(`Redis error: ${err.message}`); + }); + + this.client.on('connect', () => { + this.logger.log(`Connected to Redis at ${host}:${port} for session storage`); + }); + + // Test connection + await this.client.ping(); + this.logger.log('Redis session provider initialized'); + } catch (error) { + this.logger.warn(`Could not connect to Redis: ${error}. Falling back to in-memory sessions.`); + this.client = null; + } + } + + async onModuleDestroy() { + if (this.client) { + await this.client.quit(); + } + } + + /** + * Build Redis key for a Matrix user ID + */ + private buildKey(matrixUserId: string): string { + return `${KEY_PREFIX}${matrixUserId}`; + } + + /** + * Check if Redis is connected + */ + isConnected(): boolean { + return this.client !== null && this.client.status === 'ready'; + } + + /** + * Store a session in Redis + */ + async setSession(matrixUserId: string, session: UserSession): Promise { + if (!this.client) return; + + try { + const data = { + token: session.token, + email: session.email, + expiresAt: session.expiresAt.toISOString(), + data: session.data || {}, + }; + + await this.client.setex( + this.buildKey(matrixUserId), + this.sessionExpirySeconds, + JSON.stringify(data) + ); + this.logger.debug(`Session stored for ${matrixUserId}`); + } catch (error) { + this.logger.error(`Failed to store session: ${error}`); + } + } + + /** + * Get a session from Redis + */ + async getSession(matrixUserId: string): Promise { + if (!this.client) return null; + + try { + const data = await this.client.get(this.buildKey(matrixUserId)); + if (!data) return null; + + const parsed = JSON.parse(data); + const session: UserSession = { + token: parsed.token, + email: parsed.email, + expiresAt: new Date(parsed.expiresAt), + data: parsed.data, + }; + + // Check if expired (should not happen due to TTL, but double-check) + if (session.expiresAt < new Date()) { + await this.deleteSession(matrixUserId); + return null; + } + + return session; + } catch (error) { + this.logger.error(`Failed to get session: ${error}`); + return null; + } + } + + /** + * Get only the token from a session + */ + async getToken(matrixUserId: string): Promise { + const session = await this.getSession(matrixUserId); + return session?.token ?? null; + } + + /** + * Delete a session from Redis + */ + async deleteSession(matrixUserId: string): Promise { + if (!this.client) return; + + try { + await this.client.del(this.buildKey(matrixUserId)); + this.logger.debug(`Session deleted for ${matrixUserId}`); + } catch (error) { + this.logger.error(`Failed to delete session: ${error}`); + } + } + + /** + * Update session data without changing the token + */ + async updateSessionData(matrixUserId: string, key: string, value: unknown): Promise { + const session = await this.getSession(matrixUserId); + if (!session) return; + + session.data = session.data || {}; + session.data[key] = value; + await this.setSession(matrixUserId, session); + } + + /** + * Get session data + */ + async getSessionData(matrixUserId: string, key: string): Promise { + const session = await this.getSession(matrixUserId); + return (session?.data?.[key] as T) ?? null; + } + + /** + * Get all active session keys (for debugging/stats) + */ + async getActiveSessionCount(): Promise { + if (!this.client) return 0; + + try { + const keys = await this.client.keys(`${KEY_PREFIX}*`); + return keys.length; + } catch (error) { + this.logger.error(`Failed to get session count: ${error}`); + return 0; + } + } + + /** + * Health check + */ + async healthCheck(): Promise<{ status: string; latency: number }> { + if (!this.client) { + return { status: 'disconnected', latency: 0 }; + } + + const start = Date.now(); + try { + await this.client.ping(); + return { status: 'ok', latency: Date.now() - start }; + } catch { + return { status: 'error', latency: Date.now() - start }; + } + } +} diff --git a/packages/bot-services/src/session/session.module.ts b/packages/bot-services/src/session/session.module.ts index dd06bab35..c71ef831d 100644 --- a/packages/bot-services/src/session/session.module.ts +++ b/packages/bot-services/src/session/session.module.ts @@ -1,6 +1,7 @@ import { Module, DynamicModule, Global } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { SessionService } from './session.service'; +import { SessionService, REDIS_SESSION_PROVIDER } from './session.service'; +import { RedisSessionProvider } from './redis-session.provider'; import { SessionModuleOptions, SESSION_MODULE_OPTIONS } from './types'; /** @@ -11,19 +12,31 @@ import { SessionModuleOptions, SESSION_MODULE_OPTIONS } from './types'; * * @example * ```typescript - * // With explicit configuration + * // Basic usage (in-memory sessions, per bot) + * @Module({ + * imports: [SessionModule.forRoot()] + * }) + * + * // With Redis for cross-bot SSO * @Module({ * imports: [ - * SessionModule.register({ - * authUrl: 'http://mana-core-auth:3001', - * sessionExpiryMs: 7 * 24 * 60 * 60 * 1000 // 7 days + * SessionModule.forRoot({ + * storageMode: 'redis', + * redisHost: 'localhost', + * redisPort: 6379, * }) * ] * }) * - * // With ConfigService (reads from auth.url or MANA_CORE_AUTH_URL) + * // With Matrix-SSO-Link (automatic login) * @Module({ - * imports: [SessionModule.forRoot()] + * imports: [ + * SessionModule.forRoot({ + * storageMode: 'redis', + * enableMatrixSsoLink: true, + * serviceKey: process.env.MANA_CORE_SERVICE_KEY, + * }) + * ] * }) * ``` */ @@ -34,29 +47,76 @@ export class SessionModule { * Register module with explicit options */ static register(options: SessionModuleOptions = {}): DynamicModule { + const providers: any[] = [ + { + provide: SESSION_MODULE_OPTIONS, + useValue: options, + }, + ]; + + // Add Redis provider if storage mode is redis + if (options.storageMode === 'redis') { + providers.push({ + provide: REDIS_SESSION_PROVIDER, + useClass: RedisSessionProvider, + }); + } + + providers.push(SessionService); + return { module: SessionModule, imports: [ConfigModule], - providers: [ - { - provide: SESSION_MODULE_OPTIONS, - useValue: options, - }, - SessionService, - ], + providers, exports: [SessionService], }; } /** - * Register module with ConfigService (reads auth.url or MANA_CORE_AUTH_URL from config) + * Register module with ConfigService + * + * Reads configuration from environment: + * - MANA_CORE_AUTH_URL: Auth service URL + * - REDIS_HOST, REDIS_PORT: Redis for cross-bot SSO + * - MANA_CORE_SERVICE_KEY: For Matrix-SSO-Link + * - SESSION_STORAGE_MODE: 'memory' or 'redis' */ - static forRoot(): DynamicModule { + static forRoot(options: SessionModuleOptions = {}): DynamicModule { + const providers: any[] = [ + { + provide: SESSION_MODULE_OPTIONS, + useValue: options, + }, + ]; + + // Add Redis provider if storage mode is redis + if (options.storageMode === 'redis') { + providers.push({ + provide: REDIS_SESSION_PROVIDER, + useClass: RedisSessionProvider, + }); + } + + providers.push(SessionService); + return { module: SessionModule, imports: [ConfigModule], - providers: [SessionService], + providers, exports: [SessionService], }; } + + /** + * Register module with Redis enabled for cross-bot SSO + * + * Convenience method that enables Redis storage and Matrix-SSO-Link. + */ + static forRootWithRedis(options: Omit = {}): DynamicModule { + return this.forRoot({ + ...options, + storageMode: 'redis', + enableMatrixSsoLink: options.enableMatrixSsoLink ?? true, + }); + } } diff --git a/packages/bot-services/src/session/session.service.ts b/packages/bot-services/src/session/session.service.ts index 86a83c38c..800426957 100644 --- a/packages/bot-services/src/session/session.service.ts +++ b/packages/bot-services/src/session/session.service.ts @@ -8,21 +8,31 @@ import { SESSION_MODULE_OPTIONS, DEFAULT_SESSION_EXPIRY_MS, } from './types'; +import { RedisSessionProvider } from './redis-session.provider'; + +/** + * Injection token for Redis session provider + */ +export const REDIS_SESSION_PROVIDER = 'REDIS_SESSION_PROVIDER'; /** * Shared session management service for Matrix bots * * Manages user authentication sessions linking Matrix user IDs to mana-core-auth JWT tokens. - * Sessions are stored in-memory and automatically expire. + * + * Features: + * - **In-memory mode** (default): Sessions stored per bot instance + * - **Redis mode**: Sessions shared across ALL bots (SSO) + * - **Matrix-SSO-Link**: Automatic login for users who logged into Matrix via OIDC * * @example * ```typescript - * // In NestJS module - * imports: [SessionModule.register({ authUrl: 'http://mana-core-auth:3001' })] + * // In NestJS module - with Redis for cross-bot SSO + * imports: [SessionModule.forRoot({ storageMode: 'redis' })] * * // In service/controller - * const result = await sessionService.login(matrixUserId, email, password); - * const token = sessionService.getToken(matrixUserId); + * const token = await sessionService.getToken(matrixUserId); + * // Token is available across ALL bots! * ``` */ @Injectable() @@ -32,10 +42,13 @@ export class SessionService { private readonly authUrl: string; private readonly sessionExpiryMs: number; private readonly loginPath: string; + private readonly enableMatrixSsoLink: boolean; + private readonly serviceKey: string | undefined; constructor( @Optional() private configService: ConfigService, - @Optional() @Inject(SESSION_MODULE_OPTIONS) private options?: SessionModuleOptions + @Optional() @Inject(SESSION_MODULE_OPTIONS) private options?: SessionModuleOptions, + @Optional() @Inject(REDIS_SESSION_PROVIDER) private redisProvider?: RedisSessionProvider ) { // Priority: module options > config > environment > default this.authUrl = @@ -47,7 +60,125 @@ export class SessionService { this.sessionExpiryMs = options?.sessionExpiryMs || DEFAULT_SESSION_EXPIRY_MS; this.loginPath = options?.loginPath || '/api/v1/auth/login'; - this.logger.log(`Auth URL: ${this.authUrl}`); + // Matrix-SSO-Link settings + this.enableMatrixSsoLink = options?.enableMatrixSsoLink ?? options?.storageMode === 'redis'; + this.serviceKey = + options?.serviceKey || this.configService?.get('MANA_CORE_SERVICE_KEY'); + + const mode = this.redisProvider?.isConnected() ? 'redis' : 'memory'; + this.logger.log( + `Auth URL: ${this.authUrl}, Storage: ${mode}, Matrix-SSO-Link: ${this.enableMatrixSsoLink}` + ); + } + + /** + * Check if using Redis storage + */ + private useRedis(): boolean { + return this.redisProvider?.isConnected() ?? false; + } + + /** + * Get or create a session for a Matrix user + * + * This method tries multiple sources in order: + * 1. Redis cache (if enabled) + * 2. In-memory cache + * 3. Matrix-SSO-Link lookup (automatic login if user logged into Matrix via OIDC) + * + * @param matrixUserId - Matrix user ID (e.g., "@user:matrix.mana.how") + * @returns JWT token or null if not logged in + */ + async getToken(matrixUserId: string): Promise { + // 1. Try Redis first + if (this.useRedis()) { + const token = await this.redisProvider!.getToken(matrixUserId); + if (token) return token; + } + + // 2. Try in-memory cache + const session = this.sessions.get(matrixUserId); + if (session) { + if (session.expiresAt < new Date()) { + this.sessions.delete(matrixUserId); + } else { + return session.token; + } + } + + // 3. Try Matrix-SSO-Link (automatic login) + if (this.enableMatrixSsoLink) { + const token = await this.fetchMatrixLinkedToken(matrixUserId); + if (token) { + // Cache the token + await this.storeSession(matrixUserId, { + token, + email: '', // Unknown from SSO link + expiresAt: new Date(Date.now() + this.sessionExpiryMs), + }); + return token; + } + } + + return null; + } + + /** + * Fetch token via Matrix-SSO-Link from mana-core-auth + * + * If the user logged into Matrix via OIDC (Sign in with Mana Core), + * their Matrix user ID is linked to their Mana account. + * This method fetches a JWT token for that link. + */ + private async fetchMatrixLinkedToken(matrixUserId: string): Promise { + if (!this.serviceKey) { + this.logger.debug('Matrix-SSO-Link disabled: no service key configured'); + return null; + } + + try { + const response = await fetch( + `${this.authUrl}/api/v1/auth/matrix-session/${encodeURIComponent(matrixUserId)}`, + { + headers: { + 'X-Service-Key': this.serviceKey, + 'Content-Type': 'application/json', + }, + } + ); + + if (!response.ok) { + // 404 = no link exists, which is normal for users who didn't use OIDC + if (response.status !== 404) { + this.logger.warn(`Matrix-SSO-Link lookup failed: ${response.status}`); + } + return null; + } + + const data = (await response.json()) as { token?: string }; + if (data.token) { + this.logger.log(`Matrix-SSO-Link: auto-login for ${matrixUserId}`); + return data.token; + } + + return null; + } catch (error) { + this.logger.debug(`Matrix-SSO-Link lookup error: ${error}`); + return null; + } + } + + /** + * Store session in Redis and/or memory + */ + private async storeSession(matrixUserId: string, session: UserSession): Promise { + // Store in Redis if available + if (this.useRedis()) { + await this.redisProvider!.setSession(matrixUserId, session); + } + + // Also store in memory as fallback + this.sessions.set(matrixUserId, session); } /** @@ -81,12 +212,14 @@ export class SessionService { return { success: false, error: 'Kein Token erhalten' }; } - // Store session with expiry - this.sessions.set(matrixUserId, { + // Store session + const session: UserSession = { token, email, expiresAt: new Date(Date.now() + this.sessionExpiryMs), - }); + }; + + await this.storeSession(matrixUserId, session); this.logger.log(`User ${matrixUserId} logged in as ${email}`); return { success: true, email }; @@ -102,56 +235,66 @@ export class SessionService { /** * Logout a Matrix user */ - logout(matrixUserId: string): void { + async logout(matrixUserId: string): Promise { + // Remove from Redis + if (this.useRedis()) { + await this.redisProvider!.deleteSession(matrixUserId); + } + + // Remove from memory this.sessions.delete(matrixUserId); this.logger.log(`User ${matrixUserId} logged out`); } - /** - * Get JWT token for a Matrix user (null if not logged in or expired) - */ - getToken(matrixUserId: string): string | null { - const session = this.sessions.get(matrixUserId); - - if (!session) return null; - - // Check if token expired - if (session.expiresAt < new Date()) { - this.sessions.delete(matrixUserId); - return null; - } - - return session.token; - } - /** * Check if a Matrix user is logged in */ - isLoggedIn(matrixUserId: string): boolean { - return this.getToken(matrixUserId) !== null; + async isLoggedIn(matrixUserId: string): Promise { + const token = await this.getToken(matrixUserId); + return token !== null; } /** * Get the full session object for a Matrix user */ - getSession(matrixUserId: string): UserSession | null { - const token = this.getToken(matrixUserId); // This handles expiry check - if (!token) return null; - return this.sessions.get(matrixUserId) || null; + async getSession(matrixUserId: string): Promise { + // Try Redis first + if (this.useRedis()) { + const session = await this.redisProvider!.getSession(matrixUserId); + if (session) return session; + } + + // Try memory + const session = this.sessions.get(matrixUserId); + if (!session) return null; + + // Check expiry + if (session.expiresAt < new Date()) { + this.sessions.delete(matrixUserId); + return null; + } + + return session; } /** * Get email for a logged-in Matrix user */ - getEmail(matrixUserId: string): string | null { - const session = this.getSession(matrixUserId); + async getEmail(matrixUserId: string): Promise { + const session = await this.getSession(matrixUserId); return session?.email || null; } /** * Store custom data in a user's session */ - setSessionData(matrixUserId: string, key: string, value: unknown): void { + async setSessionData(matrixUserId: string, key: string, value: unknown): Promise { + // Update in Redis + if (this.useRedis()) { + await this.redisProvider!.updateSessionData(matrixUserId, key, value); + } + + // Update in memory const session = this.sessions.get(matrixUserId); if (session) { session.data = session.data || {}; @@ -162,13 +305,20 @@ export class SessionService { /** * Get custom data from a user's session */ - getSessionData(matrixUserId: string, key: string): T | null { - const session = this.getSession(matrixUserId); + async getSessionData(matrixUserId: string, key: string): Promise { + // Try Redis first + if (this.useRedis()) { + const data = await this.redisProvider!.getSessionData(matrixUserId, key); + if (data !== null) return data; + } + + // Try memory + const session = await this.getSession(matrixUserId); return (session?.data?.[key] as T) || null; } /** - * Get total session count (including expired) + * Get total session count (including expired in memory) */ getSessionCount(): number { return this.sessions.size; @@ -177,27 +327,40 @@ export class SessionService { /** * Get count of active (non-expired) sessions */ - getActiveSessionCount(): number { - const now = new Date(); + async getActiveSessionCount(): Promise { let count = 0; - for (const session of this.sessions.values()) { - if (session.expiresAt > now) count++; + + // Count Redis sessions + if (this.useRedis()) { + count = await this.redisProvider!.getActiveSessionCount(); } + + // If not using Redis, count memory sessions + if (count === 0) { + const now = new Date(); + for (const session of this.sessions.values()) { + if (session.expiresAt > now) count++; + } + } + return count; } /** * Get session statistics */ - getStats(): SessionStats { + async getStats(): Promise { + const active = await this.getActiveSessionCount(); return { total: this.getSessionCount(), - active: this.getActiveSessionCount(), + active, + storageMode: this.useRedis() ? 'redis' : 'memory', + matrixSsoLinkEnabled: this.enableMatrixSsoLink, }; } /** - * Clean up expired sessions (can be called periodically) + * Clean up expired sessions (only for in-memory, Redis auto-expires) */ cleanupExpiredSessions(): number { const now = new Date(); @@ -218,7 +381,7 @@ export class SessionService { } /** - * Get all active session user IDs + * Get all active session user IDs (memory only) */ getActiveUserIds(): string[] { const now = new Date(); @@ -232,4 +395,18 @@ export class SessionService { return userIds; } + + /** + * Health check + */ + async healthCheck(): Promise<{ + redis: { status: string; latency: number } | null; + memory: number; + }> { + const redisHealth = this.redisProvider ? await this.redisProvider.healthCheck() : null; + return { + redis: redisHealth, + memory: this.sessions.size, + }; + } } diff --git a/packages/bot-services/src/session/types.ts b/packages/bot-services/src/session/types.ts index 2b664c997..f3ef46e97 100644 --- a/packages/bot-services/src/session/types.ts +++ b/packages/bot-services/src/session/types.ts @@ -33,8 +33,17 @@ export interface SessionStats { total: number; /** Active (non-expired) sessions */ active: number; + /** Storage mode being used */ + storageMode?: 'memory' | 'redis'; + /** Whether Matrix-SSO-Link is enabled */ + matrixSsoLinkEnabled?: boolean; } +/** + * Session storage mode + */ +export type SessionStorageMode = 'memory' | 'redis'; + /** * Session module configuration options */ @@ -45,6 +54,22 @@ export interface SessionModuleOptions { sessionExpiryMs?: number; /** Custom login endpoint path */ loginPath?: string; + + // Redis configuration (for cross-bot SSO) + /** Storage mode: 'memory' (default) or 'redis' */ + storageMode?: SessionStorageMode; + /** Redis host (default: localhost) */ + redisHost?: string; + /** Redis port (default: 6379) */ + redisPort?: number; + /** Redis password (optional) */ + redisPassword?: string; + + // Matrix-SSO-Link configuration (automatic login via Matrix OIDC) + /** Enable Matrix-SSO-Link lookup (default: true when using Redis) */ + enableMatrixSsoLink?: boolean; + /** Service key for internal API calls to mana-core-auth */ + serviceKey?: string; } export const SESSION_MODULE_OPTIONS = 'SESSION_MODULE_OPTIONS'; diff --git a/packages/bot-services/src/todo/index.ts b/packages/bot-services/src/todo/index.ts index 3e65cef27..c533742f2 100644 --- a/packages/bot-services/src/todo/index.ts +++ b/packages/bot-services/src/todo/index.ts @@ -1,8 +1,9 @@ // Module export { TodoModule, TodoModuleOptions } from './todo.module'; -// Service +// Services export { TodoService, TODO_STORAGE_PROVIDER } from './todo.service'; +export { TodoApiService } from './todo-api.service'; // Types export * from './types'; diff --git a/packages/bot-services/src/todo/todo-api.service.ts b/packages/bot-services/src/todo/todo-api.service.ts new file mode 100644 index 000000000..774e81e63 --- /dev/null +++ b/packages/bot-services/src/todo/todo-api.service.ts @@ -0,0 +1,391 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Task, Project, CreateTaskInput, TaskFilter, TodoStats, ParsedTaskInput } from './types'; +import { parseGermanDateKeyword } from '../shared/utils'; + +/** + * Todo API Service + * + * Connects to the todo-backend API for task management. + * This service is used when the user is logged in and has a valid JWT token. + * It provides the same interface as TodoService but uses HTTP calls instead of local storage. + * + * @example + * ```typescript + * // Get tasks for a user (requires JWT token) + * const tasks = await todoApiService.getTasks(token); + * + * // Create a task + * const task = await todoApiService.createTask(token, { title: 'Buy groceries' }); + * ``` + */ +@Injectable() +export class TodoApiService { + private readonly logger = new Logger(TodoApiService.name); + private readonly baseUrl: string; + + constructor(baseUrl = 'http://localhost:3018') { + this.baseUrl = baseUrl; + this.logger.log(`Todo API Service initialized with URL: ${baseUrl}`); + } + + // ===== Task Operations ===== + + /** + * Get all pending tasks for the user + */ + async getTasks(token: string, filter?: TaskFilter): Promise { + try { + const params = new URLSearchParams(); + if (filter?.completed !== undefined) params.append('completed', String(filter.completed)); + if (filter?.project) params.append('projectId', filter.project); + + const response = await fetch(`${this.baseUrl}/api/v1/tasks?${params}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + const data = (await response.json()) as { tasks?: unknown[] }; + return this.mapApiTasks(data.tasks || []); + } catch (error) { + this.logger.error('Failed to get tasks:', error); + return []; + } + } + + /** + * Get today's tasks + */ + async getTodayTasks(token: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/v1/tasks/today`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + const data = (await response.json()) as { tasks?: any[] }; + return this.mapApiTasks(data.tasks || []); + } catch (error) { + this.logger.error('Failed to get today tasks:', error); + return []; + } + } + + /** + * Get inbox tasks (tasks without a project) + */ + async getInboxTasks(token: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/v1/tasks/inbox`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + const data = (await response.json()) as { tasks?: any[] }; + return this.mapApiTasks(data.tasks || []); + } catch (error) { + this.logger.error('Failed to get inbox tasks:', error); + return []; + } + } + + /** + * Get upcoming tasks + */ + async getUpcomingTasks(token: string, days = 7): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/v1/tasks/upcoming?days=${days}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + const data = (await response.json()) as { tasks?: any[] }; + return this.mapApiTasks(data.tasks || []); + } catch (error) { + this.logger.error('Failed to get upcoming tasks:', error); + return []; + } + } + + /** + * Create a new task + */ + async createTask(token: string, input: CreateTaskInput): Promise { + try { + const body: Record = { + title: input.title, + priority: this.mapPriorityToApi(input.priority), + }; + + if (input.dueDate) { + body.dueDate = input.dueDate; + } + + // Note: Project handling would need project ID lookup + // For now, we skip project assignment via bot + + const response = await fetch(`${this.baseUrl}/api/v1/tasks`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + const data = (await response.json()) as Record; + return this.mapApiTask(data.task); + } catch (error) { + this.logger.error('Failed to create task:', error); + return null; + } + } + + /** + * Complete a task + */ + async completeTask(token: string, taskId: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/v1/tasks/${taskId}/complete`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + const data = (await response.json()) as Record; + return this.mapApiTask(data.task); + } catch (error) { + this.logger.error('Failed to complete task:', error); + return null; + } + } + + /** + * Delete a task + */ + async deleteTask(token: string, taskId: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/v1/tasks/${taskId}`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${token}` }, + }); + + return response.ok; + } catch (error) { + this.logger.error('Failed to delete task:', error); + return false; + } + } + + // ===== Project Operations ===== + + /** + * Get all projects + */ + async getProjects(token: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/v1/projects`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + const data = (await response.json()) as { projects?: any[] }; + return (data.projects || []).map((p: any) => ({ + id: p.id, + name: p.name, + color: p.color, + userId: '', // Not needed for bot + })); + } catch (error) { + this.logger.error('Failed to get projects:', error); + return []; + } + } + + /** + * Get tasks for a specific project + */ + async getProjectTasks(token: string, projectId: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/api/v1/tasks?projectId=${projectId}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + const data = (await response.json()) as { tasks?: any[] }; + return this.mapApiTasks(data.tasks || []); + } catch (error) { + this.logger.error('Failed to get project tasks:', error); + return []; + } + } + + // ===== Stats ===== + + /** + * Get task statistics + */ + async getStats(token: string): Promise { + try { + // Get all tasks and calculate stats + const allTasks = await this.getTasks(token); + const todayTasks = await this.getTodayTasks(token); + + const pending = allTasks.filter((t) => !t.completed).length; + const completed = allTasks.filter((t) => t.completed).length; + + return { + total: allTasks.length, + pending, + completed, + today: todayTasks.length, + overdue: 0, // Would need to calculate based on due dates + }; + } catch (error) { + this.logger.error('Failed to get stats:', error); + return { total: 0, pending: 0, completed: 0, today: 0, overdue: 0 }; + } + } + + // ===== Parsing (reused from TodoService) ===== + + /** + * Parse natural language task input + */ + parseTaskInput(input: string): ParsedTaskInput { + let title = input; + let priority = 4; + let dueDate: string | null = null; + let project: string | null = null; + + // Extract priority (!p1, !p2, !p3, !p4 or !, !!, !!!) + const priorityMatch = title.match(/!p([1-4])\b/i); + if (priorityMatch) { + priority = parseInt(priorityMatch[1]); + title = title.replace(priorityMatch[0], '').trim(); + } else { + const exclamationMatch = title.match(/(!{1,3})(?:\s|$)/); + if (exclamationMatch) { + priority = Math.max(1, 4 - exclamationMatch[1].length); + title = title.replace(exclamationMatch[0], '').trim(); + } + } + + // Extract date (@heute, @morgen, @übermorgen, or date) + const dateMatch = title.match(/@(\S+)/); + if (dateMatch) { + const dateStr = dateMatch[1].toLowerCase(); + const parsedDate = parseGermanDateKeyword(dateStr); + + if (parsedDate) { + dueDate = parsedDate.toISOString().split('T')[0]; + } else { + // Try parsing as date (DD.MM or DD.MM.YYYY) + const dateRegex = /(\d{1,2})\.(\d{1,2})(?:\.(\d{2,4}))?/; + const match = dateStr.match(dateRegex); + if (match) { + const day = parseInt(match[1]); + const month = parseInt(match[2]) - 1; + const year = match[3] ? parseInt(match[3]) : new Date().getFullYear(); + const date = new Date(year, month, day); + dueDate = date.toISOString().split('T')[0]; + } + } + title = title.replace(dateMatch[0], '').trim(); + } + + // Extract project (#projectname) + const projectMatch = title.match(/#(\S+)/); + if (projectMatch) { + project = projectMatch[1]; + title = title.replace(projectMatch[0], '').trim(); + } + + return { title: title.trim(), priority, dueDate, project }; + } + + // ===== Private Helpers ===== + + /** + * Map API task format to internal Task format + */ + private mapApiTask(apiTask: any): Task { + return { + id: apiTask.id, + userId: apiTask.userId || '', + title: apiTask.title, + completed: apiTask.isCompleted || false, + priority: this.mapApiPriority(apiTask.priority), + dueDate: apiTask.dueDate ? apiTask.dueDate.split('T')[0] : null, + project: apiTask.project?.name || null, + labels: apiTask.labels?.map((l: any) => l.name) || [], + createdAt: apiTask.createdAt, + completedAt: apiTask.completedAt, + }; + } + + /** + * Map array of API tasks + */ + private mapApiTasks(apiTasks: any[]): Task[] { + return apiTasks.map((t) => this.mapApiTask(t)); + } + + /** + * Map internal priority (1-4) to API priority (urgent/high/medium/low) + */ + private mapPriorityToApi(priority?: number): string { + switch (priority) { + case 1: + return 'urgent'; + case 2: + return 'high'; + case 3: + return 'medium'; + case 4: + default: + return 'low'; + } + } + + /** + * Map API priority to internal priority (1-4) + */ + private mapApiPriority(apiPriority?: string): number { + switch (apiPriority) { + case 'urgent': + return 1; + case 'high': + return 2; + case 'medium': + return 3; + case 'low': + default: + return 4; + } + } +} diff --git a/packages/matrix-bot-common/src/session/session-helper.ts b/packages/matrix-bot-common/src/session/session-helper.ts index 3ed652824..dc01ca13a 100644 --- a/packages/matrix-bot-common/src/session/session-helper.ts +++ b/packages/matrix-bot-common/src/session/session-helper.ts @@ -1,4 +1,4 @@ -import { SessionService } from '@manacore/bot-services'; +import { type SessionService } from '@manacore/bot-services'; /** * Typed session helper for bot-specific session data @@ -14,8 +14,8 @@ import { SessionService } from '@manacore/bot-services'; * } * * const session = new SessionHelper(sessionService, matrixUserId); - * session.set('currentConversationId', 'abc123'); - * const convId = session.get('currentConversationId'); // string | null + * await session.set('currentConversationId', 'abc123'); + * const convId = await session.get('currentConversationId'); // string | null * ``` */ export class SessionHelper> { @@ -27,29 +27,29 @@ export class SessionHelper> { /** * Set a session value */ - set(key: K, value: T[K]): void { - this.sessionService.setSessionData(this.userId, key as string, value); + async set(key: K, value: T[K]): Promise { + await this.sessionService.setSessionData(this.userId, key as string, value); } /** * Get a session value */ - get(key: K): T[K] | null { + async get(key: K): Promise { return this.sessionService.getSessionData(this.userId, key as string); } /** * Delete a session value */ - delete(key: K): void { - this.sessionService.setSessionData(this.userId, key as string, null); + async delete(key: K): Promise { + await this.sessionService.setSessionData(this.userId, key as string, null); } /** * Check if a session value exists */ - has(key: K): boolean { - return this.get(key) !== null; + async has(key: K): Promise { + return (await this.get(key)) !== null; } /** @@ -62,14 +62,14 @@ export class SessionHelper> { /** * Check if user is logged in */ - isLoggedIn(): boolean { + async isLoggedIn(): Promise { return this.sessionService.isLoggedIn(this.userId); } /** * Get JWT token for API calls */ - getToken(): string | null { + async getToken(): Promise { return this.sessionService.getToken(this.userId); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29cd8711e..7fbdf0645 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -148,7 +148,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9(esbuild@0.19.12) + version: 10.4.9 '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) @@ -196,10 +196,10 @@ importers: version: 0.5.21 ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)) + version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -223,14 +223,14 @@ importers: version: link:../../../../packages/shared-landing-ui astro: specifier: ^5.16.0 - version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.9.2 version: 5.9.3 devDependencies: '@astrojs/tailwind': specifier: ^6.0.2 - version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) '@tailwindcss/typography': specifier: ^0.5.18 version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) @@ -239,13 +239,13 @@ importers: version: 20.19.25 eslint: specifier: ^9.0.0 - version: 9.39.1(jiti@1.21.7) + version: 9.39.1(jiti@2.6.1) eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.2(eslint@9.39.1(jiti@1.21.7)) + version: 9.1.2(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-astro: specifier: ^1.0.0 - version: 1.5.0(eslint@9.39.1(jiti@1.21.7)) + version: 1.5.0(eslint@9.39.1(jiti@2.6.1)) prettier: specifier: ^3.6.2 version: 3.6.2 @@ -626,19 +626,19 @@ importers: version: 18.3.27 '@typescript-eslint/eslint-plugin': specifier: ^7.7.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) '@typescript-eslint/parser': specifier: ^7.7.0 - version: 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + version: 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) dotenv: specifier: ^16.4.7 version: 16.6.1 eslint: specifier: ^9.39.1 - version: 9.39.1(jiti@2.6.1) + version: 9.39.1(jiti@1.21.7) eslint-config-universe: specifier: ^12.0.1 - version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2)(typescript@5.3.3) + version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2)(typescript@5.3.3) prettier: specifier: ^3.2.5 version: 3.6.2 @@ -1557,7 +1557,7 @@ importers: version: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-router: specifier: ~6.0.15 - version: 6.0.15(vmxlpuhz6xqbe2ee7fdabyqx3y) + version: 6.0.15(g2vconqrtzzmzlh6ymhbjirn5e) expo-status-bar: specifier: ~3.0.8 version: 3.0.8(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -1975,7 +1975,7 @@ importers: version: 8.0.9(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-router: specifier: ~6.0.10 - version: 6.0.15(psh6y5usp77eac7hbbid4ov2mi) + version: 6.0.15(ewsdnidpxwg6dzyorlbigkbme4) expo-secure-store: specifier: ^15.0.7 version: 15.0.7(expo@54.0.13) @@ -2374,7 +2374,7 @@ importers: version: 0.5.21 ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.1 version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) @@ -2817,7 +2817,7 @@ importers: version: 18.2.0(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) expo-router: specifier: ~6.0.10 - version: 6.0.15(supujcsjl47mo53hnmela4rs24) + version: 6.0.15(f6my4lgi43u5yo7kczxd3pw7ru) expo-secure-store: specifier: ~15.0.7 version: 15.0.7(expo@54.0.12) @@ -3885,10 +3885,10 @@ importers: version: 0.30.6 jest: specifier: ^30.2.0 - version: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)) + version: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) tsx: specifier: ^4.19.4 version: 4.20.6 @@ -4272,10 +4272,10 @@ importers: version: 0.30.6 jest: specifier: ^30.2.0 - version: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)) + version: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) tsx: specifier: ^4.19.4 version: 4.20.6 @@ -4568,7 +4568,13 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + ioredis: + specifier: ^5.4.2 + version: 5.9.2 devDependencies: + '@types/ioredis': + specifier: ^5.0.0 + version: 5.0.0 '@types/node': specifier: ^24.10.1 version: 24.10.1 @@ -5338,7 +5344,7 @@ importers: version: 1.57.0 jest: specifier: ^29.0.0 - version: 29.7.0(@types/node@24.10.1) + version: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) vitest: specifier: ^3.0.0 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) @@ -5470,7 +5476,7 @@ importers: version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -5639,7 +5645,7 @@ importers: version: 7.1.4 ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.1 version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) @@ -5751,7 +5757,7 @@ importers: version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -5853,7 +5859,7 @@ importers: version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -5926,7 +5932,7 @@ importers: version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -9107,7 +9113,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {'0': node >=0.10.0} + engines: {node: '>=0.10.0'} '@expo/cli@0.22.26': resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==} @@ -12591,6 +12597,10 @@ packages: '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/ioredis@5.0.0': + resolution: {integrity: sha512-zJbJ3FVE17CNl5KXzdeSPtdltc4tMT3TzC6fxQS0sQngkbFZ6h+0uTafsRqu+eSLIugf6Yb0Ea0SUuRr42Nk9g==} + deprecated: This is a stub types definition. ioredis provides its own type definitions, so you do not need this installed. + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -23815,16 +23825,6 @@ snapshots: transitivePeerDependencies: - ts-node - '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': - dependencies: - astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) - autoprefixer: 10.4.22(postcss@8.5.6) - postcss: 8.5.6 - postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) - tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1) - transitivePeerDependencies: - - ts-node - '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': dependencies: astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) @@ -26586,7 +26586,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(supujcsjl47mo53hnmela4rs24) + expo-router: 6.0.15(f6my4lgi43u5yo7kczxd3pw7ru) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -26663,7 +26663,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(psh6y5usp77eac7hbbid4ov2mi) + expo-router: 6.0.15(ewsdnidpxwg6dzyorlbigkbme4) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -26740,7 +26740,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(xyagqkzos5etzn52s4may7634u) + expo-router: 6.0.15(lvmr432nn4pnebfhbi2qjoy364) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -26817,7 +26817,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(k2muy65dii4k2uiuhg4mwyy6ki) + expo-router: 6.0.15(6hayu32hencph7rqfkncbd2qum) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -26894,7 +26894,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(7mqaurqidri6vkknnsci36yp4e) + expo-router: 6.0.15(g2vconqrtzzmzlh6ymhbjirn5e) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -28202,41 +28202,6 @@ snapshots: jest-util: 30.2.0 slash: 3.0.0 - '@jest/core@29.7.0': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.19.1 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.1) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': dependencies: '@jest/console': 29.7.0 @@ -28272,7 +28237,42 @@ snapshots: - supports-color - ts-node - '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': dependencies: '@jest/console': 30.2.0 '@jest/pattern': 30.0.1 @@ -28287,7 +28287,7 @@ snapshots: exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)) + jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) jest-haste-map: 30.2.0 jest-message-util: 30.2.0 jest-regex-util: 30.0.1 @@ -28307,6 +28307,7 @@ snapshots: - esbuild-register - supports-color - ts-node + optional: true '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': dependencies: @@ -28344,6 +28345,80 @@ snapshots: - supports-color - ts-node + '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3))': + dependencies: + '@jest/console': 30.2.0 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 22.19.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.3.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.2.0 + jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) + jest-haste-map: 30.2.0 + jest-message-util: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-resolve-dependencies: 30.2.0 + jest-runner: 30.2.0 + jest-runtime: 30.2.0 + jest-snapshot: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + jest-watcher: 30.2.0 + micromatch: 4.0.8 + pretty-format: 30.2.0 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + optional: true + + '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))': + dependencies: + '@jest/console': 30.2.0 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 22.19.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.3.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.2.0 + jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-haste-map: 30.2.0 + jest-message-util: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-resolve-dependencies: 30.2.0 + jest-runner: 30.2.0 + jest-runtime: 30.2.0 + jest-snapshot: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + jest-watcher: 30.2.0 + micromatch: 4.0.8 + pretty-format: 30.2.0 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + optional: true + '@jest/create-cache-key-function@29.7.0': dependencies: '@jest/types': 29.6.3 @@ -28791,32 +28866,6 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/cli@10.4.9(esbuild@0.19.12)': - dependencies: - '@angular-devkit/core': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics-cli': 17.3.11(chokidar@3.6.0) - '@nestjs/schematics': 10.2.3(chokidar@3.6.0)(typescript@5.7.2) - chalk: 4.1.2 - chokidar: 3.6.0 - cli-table3: 0.6.5 - commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)) - glob: 10.4.5 - inquirer: 8.2.6 - node-emoji: 1.11.0 - ora: 5.4.1 - tree-kill: 1.2.2 - tsconfig-paths: 4.2.0 - tsconfig-paths-webpack-plugin: 4.2.0 - typescript: 5.7.2 - webpack: 5.97.1(esbuild@0.19.12) - webpack-node-externals: 3.0.0 - transitivePeerDependencies: - - esbuild - - uglify-js - - webpack-cli - '@nestjs/cli@10.4.9(esbuild@0.27.0)': dependencies: '@angular-devkit/core': 17.3.11(chokidar@3.6.0) @@ -32492,20 +32541,7 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/react-native@13.3.3(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - jest-matcher-utils: 30.2.0 - picocolors: 1.1.1 - pretty-format: 30.2.0 - react: 19.1.0 - react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) - react-test-renderer: 19.1.0(react@19.1.0) - redent: 3.0.0 - optionalDependencies: - jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - optional: true - - '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 @@ -32515,10 +32551,10 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 optionalDependencies: - jest: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) + jest: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) optional: true - '@testing-library/react-native@13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 @@ -32528,10 +32564,10 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 optionalDependencies: - jest: 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) optional: true - '@testing-library/react-native@13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react-test-renderer@19.1.0(react@18.3.1))(react@18.3.1)': + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react-test-renderer@19.1.0(react@18.3.1))(react@18.3.1)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 @@ -32541,10 +32577,23 @@ snapshots: react-test-renderer: 19.1.0(react@18.3.1) redent: 3.0.0 optionalDependencies: - jest: 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) optional: true - '@testing-library/react-native@13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + jest-matcher-utils: 30.2.0 + picocolors: 1.1.1 + pretty-format: 30.2.0 + react: 19.1.0 + react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-test-renderer: 19.1.0(react@19.1.0) + redent: 3.0.0 + optionalDependencies: + jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + optional: true + + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 @@ -32554,7 +32603,7 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 optionalDependencies: - jest: 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) optional: true '@testing-library/svelte-core@1.0.0(svelte@5.44.0)': @@ -32861,6 +32910,12 @@ snapshots: '@types/http-errors@2.0.5': {} + '@types/ioredis@5.0.0': + dependencies: + ioredis: 5.9.2 + transitivePeerDependencies: + - supports-color + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -33092,16 +33147,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) - '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/type-utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -33150,15 +33205,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/parser': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -33250,14 +33305,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: @@ -33289,14 +33344,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.18.0 debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: @@ -33422,12 +33477,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/type-utils@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) - '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) ts-api-utils: 1.4.3(typescript@5.3.3) optionalDependencies: typescript: 5.3.3 @@ -33458,12 +33513,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/type-utils@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) ts-api-utils: 1.4.3(typescript@5.3.3) optionalDependencies: typescript: 5.3.3 @@ -33645,15 +33700,15 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/utils@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) '@types/json-schema': 7.0.15 '@types/semver': 7.7.1 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) semver: 7.7.3 transitivePeerDependencies: - supports-color @@ -33684,13 +33739,13 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': + '@typescript-eslint/utils@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) transitivePeerDependencies: - supports-color - typescript @@ -34601,108 +34656,6 @@ snapshots: transitivePeerDependencies: - supports-color - astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): - dependencies: - '@astrojs/compiler': 2.13.0 - '@astrojs/internal-helpers': 0.7.5 - '@astrojs/markdown-remark': 6.3.9 - '@astrojs/telemetry': 3.3.0 - '@capsizecss/unpack': 3.0.1 - '@oslojs/encoding': 1.1.0 - '@rollup/pluginutils': 5.3.0(rollup@4.53.3) - acorn: 8.15.0 - aria-query: 5.3.2 - axobject-query: 4.1.0 - boxen: 8.0.1 - ci-info: 4.3.1 - clsx: 2.1.1 - common-ancestor-path: 1.0.1 - cookie: 1.1.0 - cssesc: 3.0.0 - debug: 4.4.3 - deterministic-object-hash: 2.0.2 - devalue: 5.5.0 - diff: 5.2.0 - dlv: 1.1.3 - dset: 3.1.4 - es-module-lexer: 1.7.0 - esbuild: 0.25.12 - estree-walker: 3.0.3 - flattie: 1.1.1 - fontace: 0.3.1 - github-slugger: 2.0.0 - html-escaper: 3.0.3 - http-cache-semantics: 4.2.0 - import-meta-resolve: 4.2.0 - js-yaml: 4.1.1 - magic-string: 0.30.21 - magicast: 0.5.1 - mrmime: 2.0.1 - neotraverse: 0.6.18 - p-limit: 6.2.0 - p-queue: 8.1.1 - package-manager-detector: 1.5.0 - piccolore: 0.1.3 - picomatch: 4.0.3 - prompts: 2.4.2 - rehype: 13.0.2 - semver: 7.7.3 - shiki: 3.15.0 - smol-toml: 1.5.2 - svgo: 4.0.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tsconfck: 3.1.6(typescript@5.9.3) - ultrahtml: 1.6.0 - unifont: 0.6.0 - unist-util-visit: 5.0.0 - unstorage: 1.17.3(@netlify/blobs@10.4.1)(ioredis@5.9.2) - vfile: 6.0.3 - vite: 6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) - vitefu: 1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) - xxhash-wasm: 1.1.0 - yargs-parser: 21.1.1 - yocto-spinner: 0.2.3 - zod: 3.25.76 - zod-to-json-schema: 3.25.0(zod@3.25.76) - zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76) - optionalDependencies: - sharp: 0.34.5 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@types/node' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - db0 - - idb-keyval - - ioredis - - jiti - - less - - lightningcss - - rollup - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - typescript - - uploadthing - - yaml - astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): dependencies: '@astrojs/compiler': 2.13.0 @@ -36005,13 +35958,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@24.10.1): + create-jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@24.10.1) + jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -37117,11 +37070,6 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-compat-utils@0.6.5(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - semver: 7.7.3 - eslint-compat-utils@0.6.5(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -37132,9 +37080,9 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 1.0.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) globals: 16.5.0 @@ -37149,9 +37097,9 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 0.1.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) globals: 16.5.0 @@ -37169,14 +37117,14 @@ snapshots: dependencies: eslint: 8.57.1 + eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) - eslint-config-prettier@9.1.2(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-config-prettier@9.1.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -37201,17 +37149,17 @@ snapshots: - supports-color - typescript - eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2)(typescript@5.3.3): + eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2)(typescript@5.3.3): dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) - eslint: 9.39.1(jiti@2.6.1) - eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-node: 11.1.0(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2) - eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-react-hooks: 4.6.2(eslint@9.39.1(jiti@2.6.1)) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + eslint: 9.39.1(jiti@1.21.7) + eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-node: 11.1.0(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2) + eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-react-hooks: 4.6.2(eslint@9.39.1(jiti@1.21.7)) optionalDependencies: prettier: 3.6.2 transitivePeerDependencies: @@ -37249,7 +37197,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -37260,22 +37208,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) - get-tsconfig: 4.13.0 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.15 - unrs-resolver: 1.11.1 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -37289,12 +37222,12 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) - eslint: 9.39.1(jiti@2.6.1) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + eslint: 9.39.1(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color @@ -37309,39 +37242,25 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) - transitivePeerDependencies: - - supports-color - - eslint-plugin-astro@1.5.0(eslint@9.39.1(jiti@1.21.7)): - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) - '@jridgewell/sourcemap-codec': 1.5.5 - '@typescript-eslint/types': 8.48.0 - astro-eslint-parser: 1.2.2 - eslint: 9.39.1(jiti@1.21.7) - eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@1.21.7)) - globals: 16.5.0 - postcss: 8.5.6 - postcss-selector-parser: 7.1.0 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -37365,6 +37284,12 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 + eslint-plugin-es@3.0.1(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-utils: 2.1.0 + regexpp: 3.2.0 + eslint-plugin-es@3.0.1(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -37418,7 +37343,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -37427,9 +37352,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.1(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@1.21.7)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -37441,7 +37366,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -37476,7 +37401,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -37487,7 +37412,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -37505,7 +37430,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -37516,7 +37441,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -37544,6 +37469,16 @@ snapshots: resolve: 1.22.11 semver: 6.3.1 + eslint-plugin-node@11.1.0(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-plugin-es: 3.0.1(eslint@9.39.1(jiti@1.21.7)) + eslint-utils: 2.1.0 + ignore: 5.3.2 + minimatch: 3.1.2 + resolve: 1.22.11 + semver: 6.3.1 + eslint-plugin-node@11.1.0(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -37574,6 +37509,16 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 8.10.2(eslint@8.57.1) + eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + '@types/eslint': 9.6.1 + eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -37598,6 +37543,10 @@ snapshots: dependencies: eslint: 8.57.1 + eslint-plugin-react-hooks@4.6.2(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-plugin-react-hooks@4.6.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -37628,6 +37577,28 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 + eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@1.21.7)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.1 + eslint: 9.39.1(jiti@1.21.7) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@2.6.1)): dependencies: array-includes: 3.1.9 @@ -38772,54 +38743,7 @@ snapshots: - react-native - supports-color - expo-router@6.0.15(7mqaurqidri6vkknnsci36yp4e): - dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@expo/schema-utils': 0.1.7 - '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) - '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - client-only: 0.0.1 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-server: 1.0.4 - fast-deep-equal: 3.1.3 - invariant: 2.2.4 - nanoid: 3.3.11 - query-string: 7.1.3 - react: 19.1.0 - react-fast-compare: 3.2.2 - react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) - react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - semver: 7.6.3 - server-only: 0.0.1 - sf-symbols-typescript: 2.1.0 - shallowequal: 1.1.0 - use-latest-callback: 0.2.6(react@19.1.0) - vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - optionalDependencies: - '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) - react-dom: 19.1.0(react@19.1.0) - react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12)) - transitivePeerDependencies: - - '@react-native-masked-view/masked-view' - - '@types/react' - - '@types/react-dom' - - supports-color - optional: true - - expo-router@6.0.15(k2muy65dii4k2uiuhg4mwyy6ki): + expo-router@6.0.15(6hayu32hencph7rqfkncbd2qum): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) '@expo/schema-utils': 0.1.7 @@ -38853,7 +38777,7 @@ snapshots: vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.1.0(react@18.3.1))(react@18.3.1) optionalDependencies: '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) - '@testing-library/react-native': 13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react-test-renderer@19.1.0(react@18.3.1))(react@18.3.1) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react-test-renderer@19.1.0(react@18.3.1))(react@18.3.1) react-dom: 19.1.0(react@18.3.1) react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) @@ -38866,7 +38790,7 @@ snapshots: - supports-color optional: true - expo-router@6.0.15(psh6y5usp77eac7hbbid4ov2mi): + expo-router@6.0.15(ewsdnidpxwg6dzyorlbigkbme4): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 @@ -38900,7 +38824,7 @@ snapshots: vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -38912,7 +38836,7 @@ snapshots: - '@types/react-dom' - supports-color - expo-router@6.0.15(supujcsjl47mo53hnmela4rs24): + expo-router@6.0.15(f6my4lgi43u5yo7kczxd3pw7ru): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.12)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 @@ -38946,7 +38870,7 @@ snapshots: vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -38958,7 +38882,7 @@ snapshots: - '@types/react-dom' - supports-color - expo-router@6.0.15(vmxlpuhz6xqbe2ee7fdabyqx3y): + expo-router@6.0.15(g2vconqrtzzmzlh6ymhbjirn5e): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 @@ -38992,7 +38916,7 @@ snapshots: vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -39004,7 +38928,7 @@ snapshots: - '@types/react-dom' - supports-color - expo-router@6.0.15(xyagqkzos5etzn52s4may7634u): + expo-router@6.0.15(lvmr432nn4pnebfhbi2qjoy364): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 @@ -39038,7 +38962,7 @@ snapshots: vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -39941,23 +39865,6 @@ snapshots: forever-agent@0.6.1: {} - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - '@babel/code-frame': 7.27.1 - chalk: 4.1.2 - chokidar: 3.6.0 - cosmiconfig: 8.3.6(typescript@5.7.2) - deepmerge: 4.3.1 - fs-extra: 10.1.0 - memfs: 3.5.3 - minimatch: 3.1.2 - node-abort-controller: 3.1.1 - schema-utils: 3.3.0 - semver: 7.7.3 - tapable: 2.3.0 - typescript: 5.7.2 - webpack: 5.97.1(esbuild@0.19.12) - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.27.0)): dependencies: '@babel/code-frame': 7.27.1 @@ -41430,16 +41337,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@24.10.1): + jest-cli@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): dependencies: - '@jest/core': 29.7.0 + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@24.10.1) + create-jest: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@24.10.1) + jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -41449,15 +41356,15 @@ snapshots: - supports-color - ts-node - jest-cli@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): + jest-cli@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) + jest-config: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -41469,25 +41376,6 @@ snapshots: - ts-node optional: true - jest-cli@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)): - dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) - '@jest/test-result': 30.2.0 - '@jest/types': 30.2.0 - chalk: 4.1.2 - exit-x: 0.2.2 - import-local: 3.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)) - jest-util: 30.2.0 - jest-validate: 30.2.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - jest-cli@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) @@ -41507,15 +41395,15 @@ snapshots: - supports-color - ts-node - jest-cli@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)): + jest-cli@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + jest-config: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -41527,35 +41415,25 @@ snapshots: - ts-node optional: true - jest-config@29.7.0(@types/node@22.19.1): + jest-cli@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): dependencies: - '@babel/core': 7.28.5 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.5) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + '@jest/test-result': 30.2.0 + '@jest/types': 30.2.0 chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.19.1 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-util: 30.2.0 + jest-validate: 30.2.0 + yargs: 17.7.2 transitivePeerDependencies: + - '@types/node' - babel-plugin-macros + - esbuild-register - supports-color + - ts-node + optional: true jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: @@ -41588,7 +41466,38 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@24.10.1): + jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.28.5 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.1 + ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-config@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 '@jest/test-sequencer': 29.7.0 @@ -41614,11 +41523,12 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 24.10.1 + ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): + jest-config@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -41647,12 +41557,13 @@ snapshots: optionalDependencies: '@types/node': 20.19.25 esbuild-register: 3.6.0(esbuild@0.27.0) + ts-node: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color optional: true - jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)): + jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -41681,9 +41592,11 @@ snapshots: optionalDependencies: '@types/node': 22.19.1 esbuild-register: 3.6.0(esbuild@0.27.0) + ts-node: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color + optional: true jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: @@ -41719,7 +41632,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)): + jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -41746,7 +41659,114 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: + '@types/node': 22.19.1 esbuild-register: 3.6.0(esbuild@0.27.0) + ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.8.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + optional: true + + jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.28.5 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 4.3.1 + deepmerge: 4.3.1 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-circus: 30.2.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-runner: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.2.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.1 + esbuild-register: 3.6.0(esbuild@0.27.0) + ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + optional: true + + jest-config@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)): + dependencies: + '@babel/core': 7.28.5 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 4.3.1 + deepmerge: 4.3.1 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-circus: 30.2.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-runner: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.2.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 24.10.1 + esbuild-register: 3.6.0(esbuild@0.27.0) + ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.8.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + optional: true + + jest-config@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.28.5 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 4.3.1 + deepmerge: 4.3.1 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-circus: 30.2.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-runner: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.2.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 24.10.1 + esbuild-register: 3.6.0(esbuild@0.27.0) + ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -42200,24 +42220,24 @@ snapshots: - supports-color - ts-node - jest@29.7.0(@types/node@24.10.1): + jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): dependencies: - '@jest/core': 29.7.0 + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@24.10.1) + jest-cli: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node - jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): + jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) + jest-cli: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -42226,19 +42246,6 @@ snapshots: - ts-node optional: true - jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)): - dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) - '@jest/types': 30.2.0 - import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) @@ -42252,12 +42259,26 @@ snapshots: - supports-color - ts-node - jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)): + jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + jest-cli: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + optional: true + + jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + '@jest/types': 30.2.0 + import-local: 3.2.0 + jest-cli: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -46589,16 +46610,6 @@ snapshots: webpack-sources: 3.3.3 optional: true - react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - acorn-loose: 8.5.2 - neo-async: 2.6.2 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - webpack: 5.97.1(esbuild@0.19.12) - webpack-sources: 3.3.3 - optional: true - react-style-singleton@2.2.3(@types/react@18.3.27)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -48091,17 +48102,6 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - terser: 5.44.1 - webpack: 5.97.1(esbuild@0.19.12) - optionalDependencies: - esbuild: 0.19.12 - terser-webpack-plugin@5.3.14(esbuild@0.27.0)(webpack@5.100.2(esbuild@0.27.0)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -48306,27 +48306,6 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.7.3 - type-fest: 4.41.0 - typescript: 5.9.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.28.5 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.28.5) - esbuild: 0.19.12 - jest-util: 30.2.0 - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 @@ -48348,12 +48327,12 @@ snapshots: esbuild: 0.27.0 jest-util: 30.2.0 - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(typescript@5.9.3): + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)) + jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -48366,7 +48345,6 @@ snapshots: '@jest/transform': 30.2.0 '@jest/types': 30.2.0 babel-jest: 30.2.0(@babel/core@7.28.5) - esbuild: 0.27.0 jest-util: 30.2.0 ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)): @@ -48389,16 +48367,6 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2 - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - chalk: 4.1.2 - enhanced-resolve: 5.18.3 - micromatch: 4.0.8 - semver: 7.7.3 - source-map: 0.7.6 - typescript: 5.9.3 - webpack: 5.97.1(esbuild@0.19.12) - ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -48436,6 +48404,25 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 24.10.1 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.8.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -49092,23 +49079,6 @@ snapshots: lightningcss: 1.30.2 terser: 5.44.1 - vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): - dependencies: - esbuild: 0.25.12 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.53.3 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 20.19.25 - fsevents: 2.3.3 - jiti: 1.21.7 - lightningcss: 1.30.2 - terser: 5.44.1 - tsx: 4.20.6 - yaml: 2.8.1 - vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.12 @@ -49212,10 +49182,6 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 - vitefu@1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): - optionalDependencies: - vite: 6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) - vitefu@1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): optionalDependencies: vite: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) @@ -49672,36 +49638,6 @@ snapshots: - esbuild - uglify-js - webpack@5.97.1(esbuild@0.19.12): - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - browserslist: 4.28.0 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.3 - es-module-lexer: 1.7.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 3.3.0 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)) - watchpack: 2.4.4 - webpack-sources: 3.3.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - webpack@5.97.1(esbuild@0.27.0): dependencies: '@types/eslint-scope': 3.7.7 diff --git a/services/mana-core-auth/src/auth/auth.module.ts b/services/mana-core-auth/src/auth/auth.module.ts index 3554b6b35..31bebcef1 100644 --- a/services/mana-core-auth/src/auth/auth.module.ts +++ b/services/mana-core-auth/src/auth/auth.module.ts @@ -3,7 +3,9 @@ import { AuthController } from './auth.controller'; import { BetterAuthPassthroughController } from './better-auth-passthrough.controller'; import { OidcController } from './oidc.controller'; import { OidcLoginController } from './oidc-login.controller'; +import { MatrixSessionController } from './matrix-session.controller'; import { BetterAuthService } from './services/better-auth.service'; +import { MatrixSessionService } from './services/matrix-session.service'; import { ReferralsModule } from '../referrals/referrals.module'; @Module({ @@ -13,8 +15,9 @@ import { ReferralsModule } from '../referrals/referrals.module'; BetterAuthPassthroughController, OidcController, OidcLoginController, + MatrixSessionController, ], - providers: [BetterAuthService], - exports: [BetterAuthService], + providers: [BetterAuthService, MatrixSessionService], + exports: [BetterAuthService, MatrixSessionService], }) export class AuthModule {} diff --git a/services/mana-core-auth/src/auth/matrix-session.controller.ts b/services/mana-core-auth/src/auth/matrix-session.controller.ts new file mode 100644 index 000000000..5bd35d41b --- /dev/null +++ b/services/mana-core-auth/src/auth/matrix-session.controller.ts @@ -0,0 +1,208 @@ +import { + Controller, + Get, + Post, + Delete, + Param, + Body, + Headers, + UnauthorizedException, + NotFoundException, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { MatrixSessionService } from './services/matrix-session.service'; + +/** + * DTO for linking a Matrix user to a Mana account + */ +class LinkMatrixUserDto { + /** Matrix user ID (e.g., @user:matrix.mana.how) */ + matrixUserId!: string; + /** User's email (optional, for convenience) */ + email?: string; +} + +/** + * Matrix Session Controller + * + * Provides endpoints for Matrix bot authentication via SSO. + * + * Endpoints: + * - POST /api/v1/auth/matrix-user-links - Link Matrix user to Mana account + * - GET /api/v1/auth/matrix-session/:matrixUserId - Get JWT for linked Matrix user + * - DELETE /api/v1/auth/matrix-user-links/:matrixUserId - Unlink Matrix user + * - GET /api/v1/auth/matrix-user-links/check/:matrixUserId - Check if user is linked + * + * Authentication: + * - POST /link requires Bearer token (user authenticating) + * - GET /session requires X-Service-Key (internal bot service) + * - DELETE requires Bearer token (user unlinking) + * - GET /check requires X-Service-Key (internal bot service) + */ +@Controller('api/v1/auth') +export class MatrixSessionController { + constructor(private readonly matrixSessionService: MatrixSessionService) {} + + /** + * Link a Matrix user ID to a Mana account + * + * Called by bots after successful !login command. + * Requires the user's JWT token from login. + * + * @example + * POST /api/v1/auth/matrix-user-links + * Authorization: Bearer + * Body: { "matrixUserId": "@user:matrix.mana.how", "email": "user@example.com" } + */ + @Post('matrix-user-links') + @HttpCode(HttpStatus.CREATED) + async linkMatrixUser( + @Body() dto: LinkMatrixUserDto, + @Headers('authorization') authHeader?: string, + @Headers('x-service-key') serviceKey?: string + ): Promise<{ success: boolean; message: string }> { + // Two auth methods: Bearer token (from user login) or Service key (from bot) + let manaUserId: string; + + if (serviceKey && this.matrixSessionService.validateServiceKey(serviceKey)) { + // Service key auth - must provide userId in body + const bodyWithUserId = dto as LinkMatrixUserDto & { userId?: string }; + if (!bodyWithUserId.userId) { + throw new UnauthorizedException('userId required when using service key'); + } + manaUserId = bodyWithUserId.userId; + } else if (authHeader?.startsWith('Bearer ')) { + // JWT auth - extract user ID from token + const token = authHeader.substring(7); + const payload = this.decodeToken(token); + if (!payload?.sub) { + throw new UnauthorizedException('Invalid token'); + } + manaUserId = payload.sub; + } else { + throw new UnauthorizedException('Authentication required'); + } + + if (!dto.matrixUserId) { + throw new UnauthorizedException('matrixUserId is required'); + } + + await this.matrixSessionService.linkMatrixUser(dto.matrixUserId, manaUserId, dto.email); + + return { + success: true, + message: `Matrix user ${dto.matrixUserId} linked successfully`, + }; + } + + /** + * Get a JWT token for a linked Matrix user + * + * Called by bots to auto-authenticate users. + * Requires service key (internal service authentication). + * + * @example + * GET /api/v1/auth/matrix-session/@user:matrix.mana.how + * X-Service-Key: + */ + @Get('matrix-session/:matrixUserId') + async getMatrixSession( + @Param('matrixUserId') matrixUserId: string, + @Headers('x-service-key') serviceKey?: string + ): Promise<{ token: string; email: string }> { + // Require service key for this endpoint + if (!serviceKey || !this.matrixSessionService.validateServiceKey(serviceKey)) { + throw new UnauthorizedException('Valid service key required'); + } + + const result = await this.matrixSessionService.getSessionForMatrixUser( + decodeURIComponent(matrixUserId) + ); + + if (!result) { + throw new NotFoundException('No link found for this Matrix user'); + } + + return result; + } + + /** + * Unlink a Matrix user from a Mana account + * + * Called when user wants to disconnect their Matrix account. + * Requires the user's JWT token. + * + * @example + * DELETE /api/v1/auth/matrix-user-links/@user:matrix.mana.how + * Authorization: Bearer + */ + @Delete('matrix-user-links/:matrixUserId') + @HttpCode(HttpStatus.OK) + async unlinkMatrixUser( + @Param('matrixUserId') matrixUserId: string, + @Headers('authorization') authHeader?: string, + @Headers('x-service-key') serviceKey?: string + ): Promise<{ success: boolean; message: string }> { + // Allow both Bearer token and service key + if ( + !authHeader?.startsWith('Bearer ') && + !this.matrixSessionService.validateServiceKey(serviceKey || '') + ) { + throw new UnauthorizedException('Authentication required'); + } + + const deleted = await this.matrixSessionService.unlinkMatrixUser( + decodeURIComponent(matrixUserId) + ); + + if (!deleted) { + throw new NotFoundException('No link found for this Matrix user'); + } + + return { + success: true, + message: `Matrix user ${matrixUserId} unlinked successfully`, + }; + } + + /** + * Check if a Matrix user is linked + * + * Requires service key (internal service authentication). + * + * @example + * GET /api/v1/auth/matrix-user-links/check/@user:matrix.mana.how + * X-Service-Key: + */ + @Get('matrix-user-links/check/:matrixUserId') + async checkMatrixLink( + @Param('matrixUserId') matrixUserId: string, + @Headers('x-service-key') serviceKey?: string + ): Promise<{ linked: boolean }> { + // Require service key for this endpoint + if (!serviceKey || !this.matrixSessionService.validateServiceKey(serviceKey)) { + throw new UnauthorizedException('Valid service key required'); + } + + const linked = await this.matrixSessionService.isLinked(decodeURIComponent(matrixUserId)); + + return { linked }; + } + + /** + * Decode JWT token to get payload (without verification) + * Note: This is used only to extract user ID after the bot has verified the token + */ + private decodeToken(token: string): { sub?: string } | null { + try { + const parts = token.split('.'); + if (parts.length !== 3) return null; + + const payload = Buffer.from(parts[1], 'base64url').toString('utf-8'); + return JSON.parse(payload); + } catch { + return null; + } + } +} diff --git a/services/mana-core-auth/src/auth/services/better-auth.service.ts b/services/mana-core-auth/src/auth/services/better-auth.service.ts index 431f96bef..7251755ec 100644 --- a/services/mana-core-auth/src/auth/services/better-auth.service.ts +++ b/services/mana-core-auth/src/auth/services/better-auth.service.ts @@ -1346,6 +1346,66 @@ export class BetterAuthService { } } + // ========================================================================= + // Matrix Bot SSO Methods + // ========================================================================= + + /** + * Generate a JWT token for a specific user (used by Matrix bots) + * + * This method generates a fresh JWT token for an existing user, + * without requiring password authentication. It's used by the + * Matrix-SSO-Link system to auto-authenticate bot users. + * + * @param userId - Mana Core Auth user ID + * @returns JWT access token or null if user not found + */ + async generateTokenForUser(userId: string): Promise { + try { + const db = getDb(this.databaseUrl); + const { users } = await import('../../db/schema/auth.schema'); + const { eq } = await import('drizzle-orm'); + + // Get user from database + const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1); + + if (!user || user.deletedAt) { + this.logger.warn('generateTokenForUser: User not found', { userId }); + return null; + } + + // Generate JWT using Better Auth's signJWT + const api = this.auth.api as any; + + const jwtResult = await api.signJWT({ + body: { + payload: { + sub: user.id, + email: user.email, + role: user.role || 'user', + sid: `bot-session-${Date.now()}`, // Pseudo session ID for bots + }, + }, + }); + + const token = jwtResult?.token; + + if (!token) { + this.logger.error('generateTokenForUser: signJWT returned empty token'); + return null; + } + + this.logger.debug('Generated token for user via Matrix-SSO-Link', { userId }); + return token; + } catch (error) { + this.logger.error( + 'generateTokenForUser failed', + error instanceof Error ? error.stack : undefined + ); + return null; + } + } + // ========================================================================= // SSO Methods // ========================================================================= diff --git a/services/mana-core-auth/src/auth/services/matrix-session.service.ts b/services/mana-core-auth/src/auth/services/matrix-session.service.ts new file mode 100644 index 000000000..3cfcf4b0f --- /dev/null +++ b/services/mana-core-auth/src/auth/services/matrix-session.service.ts @@ -0,0 +1,191 @@ +import { Injectable, Logger, UnauthorizedException, NotFoundException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { eq } from 'drizzle-orm'; +import { nanoid } from 'nanoid'; +import { getDb } from '../../db/connection'; +import { matrixUserLinks, users } from '../../db/schema/auth.schema'; +import { BetterAuthService } from './better-auth.service'; + +/** + * Matrix Session Service + * + * Manages the link between Matrix user IDs and Mana Core Auth accounts. + * Enables automatic bot authentication for users who have linked their accounts. + * + * Flow: + * 1. User logs into a Matrix bot via !login email password + * 2. Bot calls POST /api/v1/auth/matrix-user-links to store the link + * 3. Later, bot can call GET /api/v1/auth/matrix-session/:matrixUserId + * 4. If a link exists, a fresh JWT token is returned + */ +@Injectable() +export class MatrixSessionService { + private readonly logger = new Logger(MatrixSessionService.name); + private readonly db; + private readonly serviceKey: string; + + constructor( + private readonly configService: ConfigService, + private readonly betterAuthService: BetterAuthService + ) { + const databaseUrl = this.configService.get('DATABASE_URL'); + if (!databaseUrl) { + throw new Error('DATABASE_URL is required'); + } + this.db = getDb(databaseUrl); + this.serviceKey = this.configService.get('MANA_CORE_SERVICE_KEY', ''); + } + + /** + * Validate service key from X-Service-Key header + */ + validateServiceKey(providedKey: string): boolean { + if (!this.serviceKey) { + this.logger.warn('MANA_CORE_SERVICE_KEY not configured - service key validation disabled'); + return false; + } + return providedKey === this.serviceKey; + } + + /** + * Create or update a link between a Matrix user ID and a Mana user + * + * @param matrixUserId - Matrix user ID (e.g., @user:matrix.mana.how) + * @param manaUserId - Mana Core Auth user ID + * @param email - User's email (optional, for convenience) + */ + async linkMatrixUser(matrixUserId: string, manaUserId: string, email?: string): Promise { + // Check if link already exists + const existing = await this.db + .select() + .from(matrixUserLinks) + .where(eq(matrixUserLinks.matrixUserId, matrixUserId)) + .limit(1); + + if (existing.length > 0) { + // Update existing link + await this.db + .update(matrixUserLinks) + .set({ + userId: manaUserId, + email, + lastUsedAt: new Date(), + }) + .where(eq(matrixUserLinks.matrixUserId, matrixUserId)); + + this.logger.log(`Updated Matrix link: ${matrixUserId} -> ${manaUserId}`); + } else { + // Create new link + await this.db.insert(matrixUserLinks).values({ + id: nanoid(), + matrixUserId, + userId: manaUserId, + email, + linkedAt: new Date(), + }); + + this.logger.log(`Created Matrix link: ${matrixUserId} -> ${manaUserId}`); + } + } + + /** + * Remove a link for a Matrix user ID + */ + async unlinkMatrixUser(matrixUserId: string): Promise { + const result = await this.db + .delete(matrixUserLinks) + .where(eq(matrixUserLinks.matrixUserId, matrixUserId)) + .returning(); + + if (result.length > 0) { + this.logger.log(`Removed Matrix link: ${matrixUserId}`); + return true; + } + return false; + } + + /** + * Get a fresh JWT token for a linked Matrix user + * + * @param matrixUserId - Matrix user ID + * @returns JWT token or null if no link exists + */ + async getSessionForMatrixUser( + matrixUserId: string + ): Promise<{ token: string; email: string } | null> { + // Find the link + const links = await this.db + .select({ + userId: matrixUserLinks.userId, + email: matrixUserLinks.email, + }) + .from(matrixUserLinks) + .where(eq(matrixUserLinks.matrixUserId, matrixUserId)) + .limit(1); + + if (links.length === 0) { + return null; + } + + const link = links[0]; + + // Update last used timestamp + await this.db + .update(matrixUserLinks) + .set({ lastUsedAt: new Date() }) + .where(eq(matrixUserLinks.matrixUserId, matrixUserId)); + + // Get user details if email not stored + let email = link.email; + if (!email) { + const userRecords = await this.db + .select({ email: users.email }) + .from(users) + .where(eq(users.id, link.userId)) + .limit(1); + + if (userRecords.length > 0) { + email = userRecords[0].email; + } + } + + // Generate a fresh JWT token for this user + const token = await this.betterAuthService.generateTokenForUser(link.userId); + + if (!token) { + this.logger.error(`Failed to generate token for user ${link.userId}`); + return null; + } + + this.logger.debug(`Generated token for Matrix user ${matrixUserId}`); + return { token, email: email || '' }; + } + + /** + * Get all Matrix links for a Mana user + */ + async getLinksForUser(manaUserId: string): Promise<{ matrixUserId: string; linkedAt: Date }[]> { + const links = await this.db + .select({ + matrixUserId: matrixUserLinks.matrixUserId, + linkedAt: matrixUserLinks.linkedAt, + }) + .from(matrixUserLinks) + .where(eq(matrixUserLinks.userId, manaUserId)); + + return links; + } + + /** + * Check if a Matrix user is linked + */ + async isLinked(matrixUserId: string): Promise { + const links = await this.db + .select({ id: matrixUserLinks.id }) + .from(matrixUserLinks) + .where(eq(matrixUserLinks.matrixUserId, matrixUserId)) + .limit(1); + + return links.length > 0; + } +} diff --git a/services/mana-core-auth/src/db/schema/auth.schema.ts b/services/mana-core-auth/src/db/schema/auth.schema.ts index 957c23500..d1c68f3b8 100644 --- a/services/mana-core-auth/src/db/schema/auth.schema.ts +++ b/services/mana-core-auth/src/db/schema/auth.schema.ts @@ -187,6 +187,26 @@ export const oauthConsents = authSchema.table('oauth_consents', { updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), }); +// Matrix User Links table (for Bot SSO) +// Links Matrix user IDs to Mana user accounts for automatic bot authentication +export const matrixUserLinks = authSchema.table( + 'matrix_user_links', + { + id: text('id').primaryKey(), // nanoid + matrixUserId: text('matrix_user_id').unique().notNull(), // e.g., @user:matrix.mana.how + userId: text('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + linkedAt: timestamp('linked_at', { withTimezone: true }).defaultNow().notNull(), + lastUsedAt: timestamp('last_used_at', { withTimezone: true }), + // Optional: store email for convenience (denormalized from users table) + email: text('email'), + }, + (table) => ({ + userIdIdx: index('matrix_user_links_user_id_idx').on(table.userId), + }) +); + // User settings table (synced across all apps) export const userSettings = authSchema.table('user_settings', { userId: text('user_id') diff --git a/services/matrix-calendar-bot/src/bot/bot.module.ts b/services/matrix-calendar-bot/src/bot/bot.module.ts index cb54ee87d..d548c6247 100644 --- a/services/matrix-calendar-bot/src/bot/bot.module.ts +++ b/services/matrix-calendar-bot/src/bot/bot.module.ts @@ -1,18 +1,35 @@ import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { MatrixService } from './matrix.service'; import { CalendarModule } from '../calendar/calendar.module'; -import { TranscriptionModule, SessionModule, CreditModule } from '@manacore/bot-services'; +import { + TranscriptionModule, + SessionModule, + CreditModule, + CalendarApiService, +} from '@manacore/bot-services'; + +// Factory provider for CalendarApiService +const calendarApiServiceProvider = { + provide: CalendarApiService, + useFactory: (configService: ConfigService) => { + const baseUrl = configService.get('CALENDAR_BACKEND_URL', 'http://localhost:3014'); + return new CalendarApiService(baseUrl); + }, + inject: [ConfigService], +}; @Module({ imports: [ + ConfigModule, CalendarModule, TranscriptionModule.register({ sttUrl: process.env.STT_URL || 'http://localhost:3020', }), - SessionModule.forRoot(), + SessionModule.forRoot({ storageMode: 'redis' }), CreditModule.forRoot(), ], - providers: [MatrixService], + providers: [MatrixService, calendarApiServiceProvider], exports: [MatrixService], }) export class BotModule {} diff --git a/services/matrix-calendar-bot/src/bot/matrix.service.ts b/services/matrix-calendar-bot/src/bot/matrix.service.ts index 5ffcf28be..556c9e45d 100644 --- a/services/matrix-calendar-bot/src/bot/matrix.service.ts +++ b/services/matrix-calendar-bot/src/bot/matrix.service.ts @@ -7,7 +7,13 @@ import { KeywordCommandDetector, COMMON_KEYWORDS, } from '@manacore/matrix-bot-common'; -import { TranscriptionService, SessionService, CreditService } from '@manacore/bot-services'; +import { + TranscriptionService, + SessionService, + CreditService, + CalendarApiService, + CalendarEvent as ApiCalendarEvent, +} from '@manacore/bot-services'; import { CalendarService, CalendarEvent } from '../calendar/calendar.service'; import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration'; @@ -19,7 +25,10 @@ export class MatrixService extends BaseMatrixService { [ ...COMMON_KEYWORDS, { keywords: ['was kannst du'], command: 'help' }, - { keywords: ['was steht heute an', 'termine heute', 'heute termine', "today's events"], command: 'today' }, + { + keywords: ['was steht heute an', 'termine heute', 'heute termine', "today's events"], + command: 'today', + }, { keywords: ['termine morgen', 'morgen termine', 'was ist morgen'], command: 'tomorrow' }, { keywords: ['diese woche', 'wochenübersicht', 'week', 'woche'], command: 'week' }, { keywords: ['zeige kalender', 'meine kalender', 'calendars'], command: 'calendars' }, @@ -32,12 +41,39 @@ export class MatrixService extends BaseMatrixService { configService: ConfigService, private readonly transcriptionService: TranscriptionService, private calendarService: CalendarService, + private calendarApiService: CalendarApiService, private sessionService: SessionService, private creditService: CreditService ) { super(configService); } + /** + * Check if user is logged in and has a valid token for API access + */ + private async getToken(userId: string): Promise { + return this.sessionService.getToken(userId); + } + + /** + * Normalize event from API or local format to common format + */ + private normalizeEvent(event: CalendarEvent | ApiCalendarEvent): CalendarEvent { + return { + id: event.id, + title: event.title, + description: event.description || null, + location: event.location || null, + startTime: event.startTime, + endTime: event.endTime, + isAllDay: event.isAllDay, + calendarId: event.calendarId || '', + calendarName: (event as CalendarEvent).calendarName || 'Kalender', + createdAt: event.createdAt || new Date().toISOString(), + userId: event.userId || '', + }; + } + protected override async handleAudioMessage( roomId: string, event: MatrixRoomEvent, @@ -64,9 +100,11 @@ export class MatrixService extends BaseMatrixService { protected getConfig(): MatrixBotConfig { return { - homeserverUrl: this.configService.get('matrix.homeserverUrl') || 'http://localhost:8008', + homeserverUrl: + this.configService.get('matrix.homeserverUrl') || 'http://localhost:8008', accessToken: this.configService.get('matrix.accessToken') || '', - storagePath: this.configService.get('matrix.storagePath') || './data/bot-storage.json', + storagePath: + this.configService.get('matrix.storagePath') || './data/bot-storage.json', allowedRooms: this.configService.get('matrix.allowedRooms') || [], }; } @@ -176,7 +214,17 @@ export class MatrixService extends BaseMatrixService { } private async handleTodayEvents(roomId: string, event: MatrixRoomEvent, userId: string) { - const events = await this.calendarService.getTodayEvents(userId); + const token = await this.getToken(userId); + let events: CalendarEvent[]; + + if (token) { + // Use API service + const apiEvents = await this.calendarApiService.getTodayEvents(token); + events = apiEvents.map((e) => this.normalizeEvent(e)); + } else { + // Use local storage + events = await this.calendarService.getTodayEvents(userId); + } if (events.length === 0) { await this.sendReply( @@ -187,12 +235,31 @@ export class MatrixService extends BaseMatrixService { return; } - const response = this.formatEventList('📅 **Termine heute:**', events); + let response = this.formatEventList('📅 **Termine heute:**', events); + if (token) { + response += '\n\n🔄 Synchronisiert'; + } await this.sendReply(roomId, event, response); } private async handleTomorrowEvents(roomId: string, event: MatrixRoomEvent, userId: string) { - const events = await this.calendarService.getTomorrowEvents(userId); + const token = await this.getToken(userId); + let events: CalendarEvent[]; + + if (token) { + // Use API service - get events for tomorrow + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const tomorrowStr = tomorrow.toISOString().split('T')[0]; + const apiEvents = await this.calendarApiService.getEvents(token, { + start: tomorrowStr, + end: tomorrowStr, + }); + events = apiEvents.map((e) => this.normalizeEvent(e)); + } else { + // Use local storage + events = await this.calendarService.getTomorrowEvents(userId); + } if (events.length === 0) { await this.sendReply( @@ -203,12 +270,25 @@ export class MatrixService extends BaseMatrixService { return; } - const response = this.formatEventList('📅 **Termine morgen:**', events); + let response = this.formatEventList('📅 **Termine morgen:**', events); + if (token) { + response += '\n\n🔄 Synchronisiert'; + } await this.sendReply(roomId, event, response); } private async handleWeekEvents(roomId: string, event: MatrixRoomEvent, userId: string) { - const events = await this.calendarService.getWeekEvents(userId); + const token = await this.getToken(userId); + let events: CalendarEvent[]; + + if (token) { + // Use API service + const apiEvents = await this.calendarApiService.getUpcomingEvents(token, 7); + events = apiEvents.map((e) => this.normalizeEvent(e)); + } else { + // Use local storage + events = await this.calendarService.getWeekEvents(userId); + } if (events.length === 0) { await this.sendReply( @@ -219,12 +299,25 @@ export class MatrixService extends BaseMatrixService { return; } - const response = this.formatEventList('📅 **Termine diese Woche:**', events); + let response = this.formatEventList('📅 **Termine diese Woche:**', events); + if (token) { + response += '\n\n🔄 Synchronisiert'; + } await this.sendReply(roomId, event, response); } private async handleUpcomingEvents(roomId: string, event: MatrixRoomEvent, userId: string) { - const events = await this.calendarService.getUpcomingEvents(userId, 14); + const token = await this.getToken(userId); + let events: CalendarEvent[]; + + if (token) { + // Use API service + const apiEvents = await this.calendarApiService.getUpcomingEvents(token, 14); + events = apiEvents.map((e) => this.normalizeEvent(e)); + } else { + // Use local storage + events = await this.calendarService.getUpcomingEvents(userId, 14); + } if (events.length === 0) { await this.sendReply( @@ -235,11 +328,19 @@ export class MatrixService extends BaseMatrixService { return; } - const response = this.formatEventList('📅 **Anstehende Termine:**', events); + let response = this.formatEventList('📅 **Anstehende Termine:**', events); + if (token) { + response += '\n\n🔄 Synchronisiert'; + } await this.sendReply(roomId, event, response); } - private async handleCreateEvent(roomId: string, event: MatrixRoomEvent, userId: string, input: string) { + private async handleCreateEvent( + roomId: string, + event: MatrixRoomEvent, + userId: string, + input: string + ) { if (!input.trim()) { await this.sendReply( roomId, @@ -249,8 +350,10 @@ export class MatrixService extends BaseMatrixService { return; } + // Check if user is logged in + const token = await this.getToken(userId); + // Validate credits if user is logged in - const token = this.sessionService.getToken(userId); if (token) { const validation = await this.creditService.validateCredits(token, EVENT_CREATE_CREDITS); if (!validation.hasCredits) { @@ -264,45 +367,87 @@ export class MatrixService extends BaseMatrixService { } } - const { title, startTime, endTime, isAllDay } = this.calendarService.parseEventInput(input); + let calendarEvent: CalendarEvent; - if (!startTime || !endTime) { - await this.sendReply( - roomId, - event, - '❌ Konnte Datum/Uhrzeit nicht erkennen.\n\nBeispiele:\n• `!termin Meeting morgen um 14:00`\n• `!termin Arzt am 15.02. um 10:00`\n• `!termin Urlaub am 01.03. ganztägig`' - ); - return; - } + if (token) { + // Use API service + const { title, startTime, endTime, isAllDay, location } = + this.calendarApiService.parseEventInput(input); - if (!title) { - await this.sendReply(roomId, event, '❌ Bitte gib einen Titel für den Termin an.'); - return; - } - - const calendarEvent = await this.calendarService.createEvent( - userId, - title, - startTime, - endTime, - { - isAllDay, + if (!startTime || !endTime) { + await this.sendReply( + roomId, + event, + '❌ Konnte Datum/Uhrzeit nicht erkennen.\n\nBeispiele:\n• `!termin Meeting morgen um 14:00`\n• `!termin Arzt am 15.02. um 10:00`\n• `!termin Urlaub am 01.03. ganztägig`' + ); + return; } - ); + + if (!title) { + await this.sendReply(roomId, event, '❌ Bitte gib einen Titel für den Termin an.'); + return; + } + + const apiEvent = await this.calendarApiService.createEvent(token, { + title, + startTime, + endTime, + isAllDay, + location: location || undefined, + }); + + if (!apiEvent) { + await this.sendReply( + roomId, + event, + '❌ Fehler beim Erstellen des Termins. Bitte versuche es erneut.' + ); + return; + } + + calendarEvent = this.normalizeEvent(apiEvent); + } else { + // Use local storage + const { title, startTime, endTime, isAllDay } = this.calendarService.parseEventInput(input); + + if (!startTime || !endTime) { + await this.sendReply( + roomId, + event, + '❌ Konnte Datum/Uhrzeit nicht erkennen.\n\nBeispiele:\n• `!termin Meeting morgen um 14:00`\n• `!termin Arzt am 15.02. um 10:00`\n• `!termin Urlaub am 01.03. ganztägig`' + ); + return; + } + + if (!title) { + await this.sendReply(roomId, event, '❌ Bitte gib einen Titel für den Termin an.'); + return; + } + + calendarEvent = await this.calendarService.createEvent(userId, title, startTime, endTime, { + isAllDay, + }); + } const timeStr = this.calendarService.formatEventTime(calendarEvent); - let response = `✅ Termin erstellt: **${title}**\n📆 ${timeStr}`; + let response = `✅ Termin erstellt: **${calendarEvent.title}**\n📆 ${timeStr}`; - // Show credit deduction if logged in + // Show credit deduction and sync status if logged in if (token) { const balance = await this.creditService.getBalance(token); response += `\n⚡ -${EVENT_CREATE_CREDITS} Credits (${balance.balance.toFixed(2)} verbleibend)`; + response += '\n🔄 Synchronisiert mit calendar-backend'; } await this.sendReply(roomId, event, response); } - private async handleEventDetails(roomId: string, event: MatrixRoomEvent, userId: string, args: string) { + private async handleEventDetails( + roomId: string, + event: MatrixRoomEvent, + userId: string, + args: string + ) { const eventNumber = parseInt(args.trim()); if (isNaN(eventNumber) || eventNumber < 1) { @@ -314,7 +459,19 @@ export class MatrixService extends BaseMatrixService { return; } - const calendarEvent = await this.calendarService.getEventByIndex(userId, eventNumber); + const token = await this.getToken(userId); + let calendarEvent: CalendarEvent | null = null; + + if (token) { + // Use API service - get event list first + const apiEvents = await this.calendarApiService.getUpcomingEvents(token, 30); + if (eventNumber > 0 && eventNumber <= apiEvents.length) { + calendarEvent = this.normalizeEvent(apiEvents[eventNumber - 1]); + } + } else { + // Use local storage + calendarEvent = await this.calendarService.getEventByIndex(userId, eventNumber); + } if (!calendarEvent) { await this.sendReply(roomId, event, `❌ Termin #${eventNumber} nicht gefunden.`); @@ -334,10 +491,19 @@ export class MatrixService extends BaseMatrixService { response += `\n📝 ${calendarEvent.description}`; } + if (token) { + response += '\n\n🔄 Synchronisiert'; + } + await this.sendReply(roomId, event, response); } - private async handleDeleteEvent(roomId: string, event: MatrixRoomEvent, userId: string, args: string) { + private async handleDeleteEvent( + roomId: string, + event: MatrixRoomEvent, + userId: string, + args: string + ) { const eventNumber = parseInt(args.trim()); if (isNaN(eventNumber) || eventNumber < 1) { @@ -349,34 +515,80 @@ export class MatrixService extends BaseMatrixService { return; } - const deletedEvent = await this.calendarService.deleteEvent(userId, eventNumber); + const token = await this.getToken(userId); + let deletedEvent: CalendarEvent | null = null; + + if (token) { + // Use API service - get event list first to find event by index + const apiEvents = await this.calendarApiService.getUpcomingEvents(token, 30); + if (eventNumber > 0 && eventNumber <= apiEvents.length) { + const targetEvent = apiEvents[eventNumber - 1]; + const success = await this.calendarApiService.deleteEvent(token, targetEvent.id); + if (success) { + deletedEvent = this.normalizeEvent(targetEvent); + } + } + } else { + // Use local storage + deletedEvent = await this.calendarService.deleteEvent(userId, eventNumber); + } if (!deletedEvent) { await this.sendReply(roomId, event, `❌ Termin #${eventNumber} nicht gefunden.`); return; } - await this.sendReply(roomId, event, `🗑️ Gelöscht: ${deletedEvent.title}`); + let response = `🗑️ Gelöscht: ${deletedEvent.title}`; + if (token) { + response += '\n\n🔄 Synchronisiert'; + } + await this.sendReply(roomId, event, response); } private async handleCalendars(roomId: string, event: MatrixRoomEvent, userId: string) { - const calendars = await this.calendarService.getCalendars(userId); + const token = await this.getToken(userId); + let calendars: { name: string }[]; + + if (token) { + // Use API service + calendars = await this.calendarApiService.getCalendars(token); + } else { + // Use local storage + calendars = await this.calendarService.getCalendars(userId); + } let response = '📁 **Deine Kalender:**\n\n'; for (const calendar of calendars) { response += `• ${calendar.name}\n`; } + if (token) { + response += '\n🔄 Synchronisiert'; + } + await this.sendReply(roomId, event, response); } private async handleStatus(roomId: string, event: MatrixRoomEvent, userId: string) { - const events = await this.calendarService.getUpcomingEvents(userId, 7); - const todayEvents = await this.calendarService.getTodayEvents(userId); + const token = await this.getToken(userId); + const session = await this.sessionService.getSession(userId); - // Check login status and credits - const token = this.sessionService.getToken(userId); - const session = this.sessionService.getSession(userId); + let todayEvents: CalendarEvent[]; + let events: CalendarEvent[]; + + if (token) { + // Use API service + const apiTodayEvents = await this.calendarApiService.getTodayEvents(token); + const apiEvents = await this.calendarApiService.getUpcomingEvents(token, 7); + todayEvents = apiTodayEvents.map((e) => this.normalizeEvent(e)); + events = apiEvents.map((e) => this.normalizeEvent(e)); + } else { + // Use local storage + todayEvents = await this.calendarService.getTodayEvents(userId); + events = await this.calendarService.getUpcomingEvents(userId, 7); + } + + const syncStatus = token ? '🔄 Synchronisiert mit calendar-backend' : '💾 Lokaler Speicher'; let response = `📊 **Status**\n\n`; response += `• Termine heute: ${todayEvents.length}\n`; @@ -388,9 +600,10 @@ export class MatrixService extends BaseMatrixService { response += `⚡ Credits: ${balance.balance.toFixed(2)}\n\n`; } else { response += `👤 Nicht angemeldet\n`; - response += `💡 Login: \`!login email passwort\`\n\n`; + response += `💡 Login: \`!login email passwort\` für Synchronisation mit calendar-web\n\n`; } + response += `${syncStatus}\n`; response += `Bot: ✅ Online`; await this.sendReply(roomId, event, response); @@ -415,7 +628,7 @@ export class MatrixService extends BaseMatrixService { return; } - const token = this.sessionService.getToken(userId); + const token = await this.sessionService.getToken(userId); if (token) { const balance = await this.creditService.getBalance(token); await this.sendReply( @@ -429,7 +642,7 @@ export class MatrixService extends BaseMatrixService { } private async handleLogout(roomId: string, event: MatrixRoomEvent, userId: string) { - const session = this.sessionService.getSession(userId); + const session = await this.sessionService.getSession(userId); if (!session) { await this.sendReply(roomId, event, '❌ Du bist nicht angemeldet.'); return; diff --git a/services/matrix-questions-bot/src/bot/matrix.service.ts b/services/matrix-questions-bot/src/bot/matrix.service.ts index bed3d0ef8..e34fbf46a 100644 --- a/services/matrix-questions-bot/src/bot/matrix.service.ts +++ b/services/matrix-questions-bot/src/bot/matrix.service.ts @@ -47,9 +47,11 @@ export class MatrixService extends BaseMatrixService { protected getConfig(): MatrixBotConfig { return { - homeserverUrl: this.configService.get('matrix.homeserverUrl') || 'http://localhost:8008', + homeserverUrl: + this.configService.get('matrix.homeserverUrl') || 'http://localhost:8008', accessToken: this.configService.get('matrix.accessToken') || '', - storagePath: this.configService.get('matrix.storagePath') || './data/bot-storage.json', + storagePath: + this.configService.get('matrix.storagePath') || './data/bot-storage.json', allowedRooms: this.configService.get('matrix.allowedRooms') || [], }; } @@ -207,8 +209,8 @@ export class MatrixService extends BaseMatrixService { } } - private requireAuth(sender: string): string { - const token = this.sessionService.getToken(sender); + private async requireAuth(sender: string): Promise { + const token = await this.sessionService.getToken(sender); if (!token) { throw new Error('Nicht angemeldet. Nutze !login email passwort'); } @@ -226,12 +228,18 @@ export class MatrixService extends BaseMatrixService { const result = await this.sessionService.login(sender, email, password); if (result.success) { - const token = this.sessionService.getToken(sender); + const token = await this.sessionService.getToken(sender); if (token) { const balance = await this.creditService.getBalance(token); - await this.sendMessage(roomId, `

✅ Erfolgreich angemeldet als ${email}
⚡ Credits: ${balance.balance.toFixed(2)}

`); + await this.sendMessage( + roomId, + `

✅ Erfolgreich angemeldet als ${email}
⚡ Credits: ${balance.balance.toFixed(2)}

` + ); } else { - await this.sendMessage(roomId, `

✅ Erfolgreich angemeldet als ${email}

`); + await this.sendMessage( + roomId, + `

✅ Erfolgreich angemeldet als ${email}

` + ); } } else { await this.sendMessage(roomId, `

❌ Login fehlgeschlagen: ${result.error}

`); @@ -240,10 +248,10 @@ export class MatrixService extends BaseMatrixService { private async handleStatus(roomId: string, sender: string) { const backendOk = await this.questionsService.checkHealth(); - const loggedIn = this.sessionService.isLoggedIn(sender); - const sessions = this.sessionService.getSessionCount(); - const session = this.sessionService.getSession(sender); - const token = this.sessionService.getToken(sender); + const loggedIn = await this.sessionService.isLoggedIn(sender); + const sessions = await this.sessionService.getSessionCount(); + const session = await this.sessionService.getSession(sender); + const token = await this.sessionService.getToken(sender); let statusHtml = `

Questions Bot Status

    `; statusHtml += `
  • Backend: ${backendOk ? '✅ Online' : '❌ Offline'}
  • `; @@ -264,7 +272,7 @@ export class MatrixService extends BaseMatrixService { // Question handlers private async handleListQuestions(roomId: string, sender: string, statusFilter?: string) { - const token = this.requireAuth(sender); + const token = await this.requireAuth(sender); const options: Record = {}; if (statusFilter) { @@ -306,13 +314,14 @@ export class MatrixService extends BaseMatrixService { html += `
  • ${status} ${priority}${q.title}
  • `; } html += ''; - html += '

    Nutze !frage [nr] fuer Details oder !recherche [nr]

    '; + html += + '

    Nutze !frage [nr] fuer Details oder !recherche [nr]

    '; await this.sendMessage(roomId, html); } private async handleQuestionDetails(roomId: string, sender: string, numberStr: string) { - const token = this.requireAuth(sender); + const token = await this.requireAuth(sender); const question = this.getQuestionByNumber(sender, numberStr); if (!question) { @@ -339,7 +348,8 @@ export class MatrixService extends BaseMatrixService { if (q.tags?.length) html += `
  • Tags: ${q.tags.join(', ')}
  • `; if (q.category) html += `
  • Kategorie: ${q.category}
  • `; html += `
  • Erstellt: ${new Date(q.createdAt).toLocaleDateString('de-DE')}
  • `; - if (q.answeredAt) html += `
  • Beantwortet: ${new Date(q.answeredAt).toLocaleDateString('de-DE')}
  • `; + if (q.answeredAt) + html += `
  • Beantwortet: ${new Date(q.answeredAt).toLocaleDateString('de-DE')}
  • `; html += '
'; html += `

Nutze !recherche ${numberStr} um eine Recherche zu starten

`; @@ -353,7 +363,7 @@ export class MatrixService extends BaseMatrixService { return; } - const token = this.requireAuth(sender); + const token = await this.requireAuth(sender); const result = await this.questionsService.createQuestion(token, title); if (result.error) { @@ -370,7 +380,7 @@ export class MatrixService extends BaseMatrixService { } private async handleDeleteQuestion(roomId: string, sender: string, numberStr: string) { - const token = this.requireAuth(sender); + const token = await this.requireAuth(sender); const question = this.getQuestionByNumber(sender, numberStr); if (!question) { @@ -390,7 +400,7 @@ export class MatrixService extends BaseMatrixService { } private async handleArchiveQuestion(roomId: string, sender: string, numberStr: string) { - const token = this.requireAuth(sender); + const token = await this.requireAuth(sender); const question = this.getQuestionByNumber(sender, numberStr); if (!question) { @@ -409,8 +419,13 @@ export class MatrixService extends BaseMatrixService { } // Research handlers - private async handleStartResearch(roomId: string, sender: string, numberStr: string, depthStr?: string) { - const token = this.requireAuth(sender); + private async handleStartResearch( + roomId: string, + sender: string, + numberStr: string, + depthStr?: string + ) { + const token = await this.requireAuth(sender); const question = this.getQuestionByNumber(sender, numberStr); if (!question) { @@ -428,7 +443,10 @@ export class MatrixService extends BaseMatrixService { }; const depth = depthMap[depthStr?.toLowerCase() || ''] || 'quick'; - await this.sendMessage(roomId, `

Starte ${depth}-Recherche fuer: ${question.title}...

`); + await this.sendMessage( + roomId, + `

Starte ${depth}-Recherche fuer: ${question.title}...

` + ); const result = await this.questionsService.startResearch(token, question.id, depth); @@ -466,7 +484,7 @@ export class MatrixService extends BaseMatrixService { } private async handleResearchResult(roomId: string, sender: string, numberStr: string) { - const token = this.requireAuth(sender); + const token = await this.requireAuth(sender); const question = this.getQuestionByNumber(sender, numberStr); if (!question) { @@ -511,7 +529,7 @@ export class MatrixService extends BaseMatrixService { } private async handleSources(roomId: string, sender: string, numberStr: string) { - const token = this.requireAuth(sender); + const token = await this.requireAuth(sender); const question = this.getQuestionByNumber(sender, numberStr); if (!question) { @@ -535,7 +553,9 @@ export class MatrixService extends BaseMatrixService { let html = `

Quellen fuer: ${question.title}

    `; for (const source of sources.slice(0, 10)) { - const relevance = source.relevanceScore ? ` (${Math.round(source.relevanceScore * 100)}%)` : ''; + const relevance = source.relevanceScore + ? ` (${Math.round(source.relevanceScore * 100)}%)` + : ''; html += `
  1. ${source.title}${relevance}
    ${source.domain}
  2. `; } html += '
'; @@ -549,7 +569,7 @@ export class MatrixService extends BaseMatrixService { // Answer handlers private async handleAnswer(roomId: string, sender: string, numberStr: string) { - const token = this.requireAuth(sender); + const token = await this.requireAuth(sender); const question = this.getQuestionByNumber(sender, numberStr); if (!question) { @@ -579,7 +599,9 @@ export class MatrixService extends BaseMatrixService { const answer = answers[0]; const accepted = answer.isAccepted ? ' ✅' : ''; const rating = answer.rating ? ` (${answer.rating}/5 Sterne)` : ''; - const confidence = answer.confidence ? ` [${Math.round(answer.confidence * 100)}% Konfidenz]` : ''; + const confidence = answer.confidence + ? ` [${Math.round(answer.confidence * 100)}% Konfidenz]` + : ''; let html = `

Antwort${accepted}${rating}

`; html += `

Model: ${answer.modelId}${confidence}

`; @@ -599,11 +621,19 @@ export class MatrixService extends BaseMatrixService { await this.sendMessage(roomId, html); } - private async handleRateAnswer(roomId: string, sender: string, numberStr: string, ratingStr: string) { - const token = this.requireAuth(sender); + private async handleRateAnswer( + roomId: string, + sender: string, + numberStr: string, + ratingStr: string + ) { + const token = await this.requireAuth(sender); if (!this.answersMapper.hasList(sender)) { - await this.sendMessage(roomId, '

Zeige zuerst eine Antwort mit !antwort [nr]

'); + await this.sendMessage( + roomId, + '

Zeige zuerst eine Antwort mit !antwort [nr]

' + ); return; } @@ -630,10 +660,13 @@ export class MatrixService extends BaseMatrixService { } private async handleAcceptAnswer(roomId: string, sender: string, numberStr: string) { - const token = this.requireAuth(sender); + const token = await this.requireAuth(sender); if (!this.answersMapper.hasList(sender)) { - await this.sendMessage(roomId, '

Zeige zuerst eine Antwort mit !antwort [nr]

'); + await this.sendMessage( + roomId, + '

Zeige zuerst eine Antwort mit !antwort [nr]

' + ); return; } @@ -655,7 +688,7 @@ export class MatrixService extends BaseMatrixService { // Collection handlers private async handleListCollections(roomId: string, sender: string) { - const token = this.requireAuth(sender); + const token = await this.requireAuth(sender); const result = await this.questionsService.getCollections(token); if (result.error) { @@ -691,7 +724,7 @@ export class MatrixService extends BaseMatrixService { return; } - const token = this.requireAuth(sender); + const token = await this.requireAuth(sender); const result = await this.questionsService.createCollection(token, name); if (result.error) { @@ -700,7 +733,10 @@ export class MatrixService extends BaseMatrixService { } this.collectionsMapper.clearList(sender); - await this.sendMessage(roomId, `

Sammlung ${result.data!.name} erstellt.

`); + await this.sendMessage( + roomId, + `

Sammlung ${result.data!.name} erstellt.

` + ); } // Search handler @@ -710,7 +746,7 @@ export class MatrixService extends BaseMatrixService { return; } - const token = this.requireAuth(sender); + const token = await this.requireAuth(sender); const result = await this.questionsService.getQuestions(token, { search: query }); if (result.error) { @@ -745,10 +781,10 @@ export class MatrixService extends BaseMatrixService { private getStatusEmoji(status: string): string { const map: Record = { - open: '❓', // Question mark + open: '❓', // Question mark researching: '🔍', // Magnifying glass - answered: '✅', // Check mark - archived: '📦', // Package + answered: '✅', // Check mark + archived: '📦', // Package }; return map[status] || '❓'; } @@ -765,8 +801,8 @@ export class MatrixService extends BaseMatrixService { private getPriorityIndicator(priority: string): string { const map: Record = { - urgent: '🔴 ', // Red circle - high: '🟠 ', // Orange circle + urgent: '🔴 ', // Red circle + high: '🟠 ', // Orange circle normal: '', low: '', }; diff --git a/services/matrix-stats-bot/src/bot/matrix.service.ts b/services/matrix-stats-bot/src/bot/matrix.service.ts index e4c619c20..3a182c228 100644 --- a/services/matrix-stats-bot/src/bot/matrix.service.ts +++ b/services/matrix-stats-bot/src/bot/matrix.service.ts @@ -206,11 +206,13 @@ Daten von Umami Analytics (self-hosted).`; const result = await this.sessionService.login(sender, email, password); if (result.success) { - const token = this.sessionService.getToken(sender); + const token = await this.sessionService.getToken(sender); if (token) { const balance = await this.creditService.getBalance(token); - await this.sendMessage(roomId, - `✅ Erfolgreich angemeldet als **${email}**\n⚡ Credits: ${balance.balance.toFixed(2)}`); + await this.sendMessage( + roomId, + `✅ Erfolgreich angemeldet als **${email}**\n⚡ Credits: ${balance.balance.toFixed(2)}` + ); } else { await this.sendMessage(roomId, `✅ Erfolgreich angemeldet als **${email}**`); } @@ -220,14 +222,14 @@ Daten von Umami Analytics (self-hosted).`; } private async handleLogout(roomId: string, sender: string) { - this.sessionService.logout(sender); + await this.sessionService.logout(sender); await this.sendMessage(roomId, '👋 Erfolgreich abgemeldet.'); } private async handleStatus(roomId: string, sender: string) { - const loggedIn = this.sessionService.isLoggedIn(sender); - const session = this.sessionService.getSession(sender); - const token = this.sessionService.getToken(sender); + const loggedIn = await this.sessionService.isLoggedIn(sender); + const session = await this.sessionService.getSession(sender); + const token = await this.sessionService.getToken(sender); let response = '**📊 Stats Bot Status**\n\n'; diff --git a/services/matrix-todo-bot/src/bot/bot.module.ts b/services/matrix-todo-bot/src/bot/bot.module.ts index cc1ed2837..b2b0247da 100644 --- a/services/matrix-todo-bot/src/bot/bot.module.ts +++ b/services/matrix-todo-bot/src/bot/bot.module.ts @@ -1,16 +1,33 @@ import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { MatrixService } from './matrix.service'; import { TodoModule } from '../todo/todo.module'; -import { TranscriptionModule, SessionModule, CreditModule } from '@manacore/bot-services'; +import { + TranscriptionModule, + SessionModule, + CreditModule, + TodoApiService, +} from '@manacore/bot-services'; + +// Factory provider for TodoApiService +const todoApiServiceProvider = { + provide: TodoApiService, + useFactory: (configService: ConfigService) => { + const baseUrl = configService.get('TODO_BACKEND_URL', 'http://localhost:3018'); + return new TodoApiService(baseUrl); + }, + inject: [ConfigService], +}; @Module({ imports: [ + ConfigModule, TodoModule, TranscriptionModule.forRoot(), - SessionModule.forRoot(), + SessionModule.forRoot({ storageMode: 'redis' }), CreditModule.forRoot(), ], - providers: [MatrixService], + providers: [MatrixService, todoApiServiceProvider], exports: [MatrixService], }) export class BotModule {} diff --git a/services/matrix-todo-bot/src/bot/matrix.service.ts b/services/matrix-todo-bot/src/bot/matrix.service.ts index 8c06a6a99..c2a156d60 100644 --- a/services/matrix-todo-bot/src/bot/matrix.service.ts +++ b/services/matrix-todo-bot/src/bot/matrix.service.ts @@ -8,7 +8,13 @@ import { COMMON_KEYWORDS, } from '@manacore/matrix-bot-common'; import { TodoService, Task } from '../todo/todo.service'; -import { TranscriptionService, SessionService, CreditService } from '@manacore/bot-services'; +import { + TranscriptionService, + SessionService, + CreditService, + TodoApiService, + Task as ApiTask, +} from '@manacore/bot-services'; import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration'; // Credit cost for task creation (micro-credits) @@ -20,7 +26,10 @@ export class MatrixService extends BaseMatrixService { [ ...COMMON_KEYWORDS, { keywords: ['was kannst du'], command: 'help' }, - { keywords: ['zeige aufgaben', 'meine aufgaben', 'was muss ich', 'show tasks', 'list'], command: 'list' }, + { + keywords: ['zeige aufgaben', 'meine aufgaben', 'was muss ich', 'show tasks', 'list'], + command: 'list', + }, { keywords: ['heute', 'today', 'was steht an'], command: 'today' }, { keywords: ['inbox', 'eingang', 'ohne datum'], command: 'inbox' }, { keywords: ['projekte', 'projects'], command: 'projects' }, @@ -32,6 +41,7 @@ export class MatrixService extends BaseMatrixService { constructor( configService: ConfigService, private todoService: TodoService, + private todoApiService: TodoApiService, private transcriptionService: TranscriptionService, private sessionService: SessionService, private creditService: CreditService @@ -39,11 +49,38 @@ export class MatrixService extends BaseMatrixService { super(configService); } + /** + * Check if user is logged in and has a valid token for API access + */ + private async getToken(userId: string): Promise { + return this.sessionService.getToken(userId); + } + + /** + * Normalize task from API or local format to common format + */ + private normalizeTask(task: Task | ApiTask): Task { + return { + id: task.id, + title: task.title, + completed: task.completed, + priority: task.priority, + dueDate: task.dueDate, + project: task.project, + labels: task.labels || [], + createdAt: task.createdAt, + completedAt: task.completedAt || null, + userId: task.userId, + }; + } + protected getConfig(): MatrixBotConfig { return { - homeserverUrl: this.configService.get('matrix.homeserverUrl') || 'http://localhost:8008', + homeserverUrl: + this.configService.get('matrix.homeserverUrl') || 'http://localhost:8008', accessToken: this.configService.get('matrix.accessToken') || '', - storagePath: this.configService.get('matrix.storagePath') || './data/bot-storage.json', + storagePath: + this.configService.get('matrix.storagePath') || './data/bot-storage.json', allowedRooms: this.configService.get('matrix.allowedRooms') || [], }; } @@ -74,11 +111,7 @@ export class MatrixService extends BaseMatrixService { } } catch (error) { this.logger.error(`Error handling message: ${error}`); - await this.sendReply( - roomId, - event, - 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.' - ); + await this.sendReply(roomId, event, 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.'); } } @@ -118,8 +151,10 @@ export class MatrixService extends BaseMatrixService { return; } + // Check if user is logged in + const token = await this.getToken(sender); + // Check credits if user is logged in - const token = this.sessionService.getToken(sender); if (token) { const validation = await this.creditService.validateCredits(token, TASK_CREATE_CREDITS); if (!validation.hasCredits) { @@ -128,36 +163,59 @@ export class MatrixService extends BaseMatrixService { validation.availableCredits, 'Aufgabe erstellen' ); - await this.sendReply(roomId, event, `Transkription: "${transcription}"\n\n${errorMsg.text}`); + await this.sendReply( + roomId, + event, + `Transkription: "${transcription}"\n\n${errorMsg.text}` + ); return; } } - // Parse the transcription as a task input - const { title, priority, dueDate, project } = this.todoService.parseTaskInput(transcription); + let task: Task; - // Create the task - const task = await this.todoService.createTask(sender, title, { - priority, - dueDate, - project, - }); + if (token) { + // Use API service (syncs with todo-web and mobile) + const { title, priority, dueDate, project } = + this.todoApiService.parseTaskInput(transcription); + const apiTask = await this.todoApiService.createTask(token, { title, priority, dueDate }); + if (!apiTask) { + await this.sendReply( + roomId, + event, + `Transkription: "${transcription}"\n\nFehler beim Erstellen der Aufgabe.` + ); + return; + } + task = this.normalizeTask(apiTask); + task.project = project; + } else { + // Use local storage (offline mode) + const { title, priority, dueDate, project } = + this.todoService.parseTaskInput(transcription); + task = await this.todoService.createTask(sender, title, { + priority, + dueDate, + project, + }); + } let responseText = `Transkription: "${transcription}"\n\nAufgabe erstellt: **${task.title}**`; const details: string[] = []; - if (priority < 4) details.push(`Prioritat ${priority}`); - if (dueDate) details.push(`Datum: ${this.formatDate(dueDate)}`); - if (project) details.push(`Projekt: ${project}`); + if (task.priority < 4) details.push(`Prioritat ${task.priority}`); + if (task.dueDate) details.push(`Datum: ${this.formatDate(task.dueDate)}`); + if (task.project) details.push(`Projekt: ${task.project}`); if (details.length > 0) { responseText += `\n${details.join(' | ')}`; } - // Show credit deduction if logged in + // Show credit deduction and sync status if logged in if (token) { const balance = await this.creditService.getBalance(token); responseText += `\n\n⚡ -${TASK_CREATE_CREDITS} Credits (${balance.balance.toFixed(2)} verbleibend)`; + responseText += '\n🔄 Synchronisiert mit todo-backend'; } await this.sendReply(roomId, event, responseText); @@ -247,7 +305,12 @@ export class MatrixService extends BaseMatrixService { } } - private async handleAddTask(roomId: string, event: MatrixRoomEvent, userId: string, input: string) { + private async handleAddTask( + roomId: string, + event: MatrixRoomEvent, + userId: string, + input: string + ) { if (!input.trim()) { await this.sendReply( roomId, @@ -257,8 +320,10 @@ export class MatrixService extends BaseMatrixService { return; } + // Check if user is logged in + const token = await this.getToken(userId); + // Check credits if user is logged in - const token = this.sessionService.getToken(userId); if (token) { const validation = await this.creditService.validateCredits(token, TASK_CREATE_CREDITS); if (!validation.hasCredits) { @@ -272,36 +337,65 @@ export class MatrixService extends BaseMatrixService { } } - const { title, priority, dueDate, project } = this.todoService.parseTaskInput(input); + let task: Task; - const task = await this.todoService.createTask(userId, title, { - priority, - dueDate, - project, - }); + if (token) { + // Use API service (syncs with todo-web and mobile) + const { title, priority, dueDate, project } = this.todoApiService.parseTaskInput(input); + const apiTask = await this.todoApiService.createTask(token, { title, priority, dueDate }); + if (!apiTask) { + await this.sendReply( + roomId, + event, + 'Fehler beim Erstellen der Aufgabe. Bitte versuche es erneut.' + ); + return; + } + task = this.normalizeTask(apiTask); + task.project = project; // Note: project handling via API needs project ID lookup + } else { + // Use local storage (offline mode) + const { title, priority, dueDate, project } = this.todoService.parseTaskInput(input); + task = await this.todoService.createTask(userId, title, { + priority, + dueDate, + project, + }); + } let response = `Aufgabe erstellt: **${task.title}**`; const details: string[] = []; - if (priority < 4) details.push(`Prioritaet ${priority}`); - if (dueDate) details.push(`Datum: ${this.formatDate(dueDate)}`); - if (project) details.push(`Projekt: ${project}`); + if (task.priority < 4) details.push(`Prioritaet ${task.priority}`); + if (task.dueDate) details.push(`Datum: ${this.formatDate(task.dueDate)}`); + if (task.project) details.push(`Projekt: ${task.project}`); if (details.length > 0) { response += `\n${details.join(' | ')}`; } - // Show credit deduction if logged in + // Show credit deduction and sync status if logged in if (token) { const balance = await this.creditService.getBalance(token); response += `\n\n⚡ -${TASK_CREATE_CREDITS} Credits (${balance.balance.toFixed(2)} verbleibend)`; + response += '\n🔄 Synchronisiert mit todo-backend'; } await this.sendReply(roomId, event, response); } private async handleListTasks(roomId: string, event: MatrixRoomEvent, userId: string) { - const tasks = await this.todoService.getAllPendingTasks(userId); + const token = await this.getToken(userId); + let tasks: Task[]; + + if (token) { + // Use API service + const apiTasks = await this.todoApiService.getTasks(token, { completed: false }); + tasks = apiTasks.map((t) => this.normalizeTask(t)); + } else { + // Use local storage + tasks = await this.todoService.getAllPendingTasks(userId); + } if (tasks.length === 0) { await this.sendReply( @@ -312,12 +406,25 @@ export class MatrixService extends BaseMatrixService { return; } - const response = this.formatTaskList('**Alle offenen Aufgaben:**', tasks); + let response = this.formatTaskList('**Alle offenen Aufgaben:**', tasks); + if (token) { + response += '\n\n🔄 Synchronisiert'; + } await this.sendReply(roomId, event, response); } private async handleTodayTasks(roomId: string, event: MatrixRoomEvent, userId: string) { - const tasks = await this.todoService.getTodayTasks(userId); + const token = await this.getToken(userId); + let tasks: Task[]; + + if (token) { + // Use API service + const apiTasks = await this.todoApiService.getTodayTasks(token); + tasks = apiTasks.map((t) => this.normalizeTask(t)); + } else { + // Use local storage + tasks = await this.todoService.getTodayTasks(userId); + } if (tasks.length === 0) { await this.sendReply( @@ -328,23 +435,44 @@ export class MatrixService extends BaseMatrixService { return; } - const response = this.formatTaskList('**Aufgaben fuer heute:**', tasks); + let response = this.formatTaskList('**Aufgaben fuer heute:**', tasks); + if (token) { + response += '\n\n🔄 Synchronisiert'; + } await this.sendReply(roomId, event, response); } private async handleInboxTasks(roomId: string, event: MatrixRoomEvent, userId: string) { - const tasks = await this.todoService.getInboxTasks(userId); + const token = await this.getToken(userId); + let tasks: Task[]; + + if (token) { + // Use API service + const apiTasks = await this.todoApiService.getInboxTasks(token); + tasks = apiTasks.map((t) => this.normalizeTask(t)); + } else { + // Use local storage + tasks = await this.todoService.getInboxTasks(userId); + } if (tasks.length === 0) { await this.sendReply(roomId, event, 'Inbox ist leer.\n\nAufgaben ohne Datum landen hier.'); return; } - const response = this.formatTaskList('**Inbox (ohne Datum):**', tasks); + let response = this.formatTaskList('**Inbox (ohne Datum):**', tasks); + if (token) { + response += '\n\n🔄 Synchronisiert'; + } await this.sendReply(roomId, event, response); } - private async handleCompleteTask(roomId: string, event: MatrixRoomEvent, userId: string, args: string) { + private async handleCompleteTask( + roomId: string, + event: MatrixRoomEvent, + userId: string, + args: string + ) { const taskNumber = parseInt(args.trim()); if (isNaN(taskNumber) || taskNumber < 1) { @@ -356,17 +484,42 @@ export class MatrixService extends BaseMatrixService { return; } - const task = await this.todoService.completeTask(userId, taskNumber); + const token = await this.getToken(userId); + let task: Task | null = null; + + if (token) { + // Use API service - need to get task list first to find task by index + const apiTasks = await this.todoApiService.getTasks(token, { completed: false }); + if (taskNumber > 0 && taskNumber <= apiTasks.length) { + const targetTask = apiTasks[taskNumber - 1]; + const completedTask = await this.todoApiService.completeTask(token, targetTask.id); + if (completedTask) { + task = this.normalizeTask(completedTask); + } + } + } else { + // Use local storage + task = await this.todoService.completeTask(userId, taskNumber); + } if (!task) { await this.sendReply(roomId, event, `Aufgabe #${taskNumber} nicht gefunden.`); return; } - await this.sendReply(roomId, event, `Erledigt: ~~${task.title}~~`); + let response = `Erledigt: ~~${task.title}~~`; + if (token) { + response += '\n\n🔄 Synchronisiert'; + } + await this.sendReply(roomId, event, response); } - private async handleDeleteTask(roomId: string, event: MatrixRoomEvent, userId: string, args: string) { + private async handleDeleteTask( + roomId: string, + event: MatrixRoomEvent, + userId: string, + args: string + ) { const taskNumber = parseInt(args.trim()); if (isNaN(taskNumber) || taskNumber < 1) { @@ -378,18 +531,48 @@ export class MatrixService extends BaseMatrixService { return; } - const task = await this.todoService.deleteTask(userId, taskNumber); + const token = await this.getToken(userId); + let task: Task | null = null; + + if (token) { + // Use API service - need to get task list first to find task by index + const apiTasks = await this.todoApiService.getTasks(token, { completed: false }); + if (taskNumber > 0 && taskNumber <= apiTasks.length) { + const targetTask = apiTasks[taskNumber - 1]; + const deleted = await this.todoApiService.deleteTask(token, targetTask.id); + if (deleted) { + task = this.normalizeTask(targetTask); + } + } + } else { + // Use local storage + task = await this.todoService.deleteTask(userId, taskNumber); + } if (!task) { await this.sendReply(roomId, event, `Aufgabe #${taskNumber} nicht gefunden.`); return; } - await this.sendReply(roomId, event, `Geloescht: ${task.title}`); + let response = `Geloescht: ${task.title}`; + if (token) { + response += '\n\n🔄 Synchronisiert'; + } + await this.sendReply(roomId, event, response); } private async handleProjects(roomId: string, event: MatrixRoomEvent, userId: string) { - const projects = await this.todoService.getProjects(userId); + const token = await this.getToken(userId); + let projects: { name: string }[]; + + if (token) { + // Use API service + const apiProjects = await this.todoApiService.getProjects(token); + projects = apiProjects; + } else { + // Use local storage + projects = await this.todoService.getProjects(userId); + } if (projects.length === 0) { await this.sendReply( @@ -405,11 +588,19 @@ export class MatrixService extends BaseMatrixService { response += `- ${project.name}\n`; } response += '\nZeige Projektaufgaben mit `!project [Name]`'; + if (token) { + response += '\n\n🔄 Synchronisiert'; + } await this.sendReply(roomId, event, response); } - private async handleProjectTasks(roomId: string, event: MatrixRoomEvent, userId: string, args: string) { + private async handleProjectTasks( + roomId: string, + event: MatrixRoomEvent, + userId: string, + args: string + ) { const projectName = args.trim(); if (!projectName) { @@ -421,22 +612,50 @@ export class MatrixService extends BaseMatrixService { return; } - const tasks = await this.todoService.getProjectTasks(userId, projectName); + const token = await this.getToken(userId); + let tasks: Task[]; + + if (token) { + // Use API service - need to find project ID first + const projects = await this.todoApiService.getProjects(token); + const project = projects.find((p) => p.name.toLowerCase() === projectName.toLowerCase()); + if (project) { + const apiTasks = await this.todoApiService.getProjectTasks(token, project.id); + tasks = apiTasks.map((t) => this.normalizeTask(t)); + } else { + tasks = []; + } + } else { + // Use local storage + tasks = await this.todoService.getProjectTasks(userId, projectName); + } if (tasks.length === 0) { await this.sendReply(roomId, event, `Keine Aufgaben im Projekt "${projectName}".`); return; } - const response = this.formatTaskList(`**Projekt: ${projectName}**`, tasks); + let response = this.formatTaskList(`**Projekt: ${projectName}**`, tasks); + if (token) { + response += '\n\n🔄 Synchronisiert'; + } await this.sendReply(roomId, event, response); } private async handleStatus(roomId: string, event: MatrixRoomEvent, userId: string) { - const stats = await this.todoService.getStats(userId); - const isLoggedIn = this.sessionService.isLoggedIn(userId); + const token = await this.getToken(userId); + const isLoggedIn = await this.sessionService.isLoggedIn(userId); const email = this.sessionService.getEmail(userId); - const token = this.sessionService.getToken(userId); + + let stats: { total: number; completed: number; pending: number; today: number }; + + if (token) { + // Use API service + stats = await this.todoApiService.getStats(token); + } else { + // Use local storage + stats = await this.todoService.getStats(userId); + } // Get credit balance if logged in let creditInfo = ''; @@ -452,6 +671,8 @@ export class MatrixService extends BaseMatrixService { } } + const syncStatus = token ? '🔄 Synchronisiert mit todo-backend' : '💾 Lokaler Speicher'; + const response = `**Status** 👤 Angemeldet: ${isLoggedIn ? `Ja (${email})` : 'Nein'}${creditInfo} @@ -461,7 +682,8 @@ export class MatrixService extends BaseMatrixService { - Erledigt: ${stats.completed} - Gesamt: ${stats.total} -Bot: Online${!isLoggedIn ? '\n\nTipp: Mit `!login email passwort` anmelden fuer Credit-Tracking' : ''}`; +${syncStatus} +Bot: Online${!isLoggedIn ? '\n\nTipp: Mit `!login email passwort` anmelden fuer Synchronisation mit todo-web' : ''}`; await this.sendReply(roomId, event, response); } @@ -501,11 +723,7 @@ Bot: Online${!isLoggedIn ? '\n\nTipp: Mit `!login email passwort` anmelden fuer await this.sendReply(roomId, event, 'Hilfe wurde angepinnt!'); } catch (error) { this.logger.error('Failed to pin help:', error); - await this.sendReply( - roomId, - event, - 'Konnte Hilfe nicht anpinnen (fehlende Berechtigung?)' - ); + await this.sendReply(roomId, event, 'Konnte Hilfe nicht anpinnen (fehlende Berechtigung?)'); } }