diff --git a/apps/api/package.json b/apps/api/package.json index 1232081..375107d 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -9,6 +9,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { + "@microsoft/microsoft-graph-client": "3.0.7", "@nestjs-modules/mailer": "^2.3.7", "@nestjs/common": "^11.0.0", "@nestjs/config": "^4.0.0", @@ -24,13 +25,16 @@ "class-transformer": "^0.5.1", "class-validator": "^0.15.1", "cookie-parser": "^1.4.7", + "ews-javascript-api": "0.15.3", "ldapts": "^8.1.8", + "node-ical": "0.26.1", "nodemailer": "^9.0.1", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "reflect-metadata": "^0.2.0", - "rxjs": "^7.0.0" + "rxjs": "^7.0.0", + "tsdav": "2.2.2" }, "devDependencies": { "@nestjs/cli": "^11.0.0", diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index edb8833..d062864 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -146,3 +146,25 @@ model SearchProvider { @@index([userId]) } + +model CalendarSource { + id String @id @default(uuid()) + userId String + tenantId String + name String + type String // 'caldav' | 'ics' | 'exchange' + exchangeMode String? // 'ews' | 'graph' — only for exchange type + url String + username String? + encryptedPassword String? // AES-256-GCM ciphertext (iv:authTag:ciphertext hex) + color String? @default("#3B82F6") + isVisible Boolean @default(true) + syncIntervalMin Int @default(15) + lastSyncAt DateTime? + lastSyncError String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId]) + @@index([tenantId]) +} diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 4fae6fc..bd6c90d 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -8,6 +8,7 @@ import { ForcePasswordChangeInterceptor } from './auth/interceptors/force-passwo import { HealthModule } from './health/health.module'; import { LdapModule } from './ldap/ldap.module'; import { MailModule } from './mail/mail.module'; +import { CalendarModule } from './calendar/calendar.module'; import { DashboardModule } from './dashboard/dashboard.module'; import { DomaincheckModule } from './domaincheck/domaincheck.module'; import { ModuleRegistryModule } from './module-registry/module-registry.module'; @@ -29,6 +30,7 @@ import { UserModule } from './user/user.module'; ModuleRegistryModule, DomaincheckModule, DashboardModule, + CalendarModule, ], providers: [ // Global JWT guard: all routes require auth unless @Public() diff --git a/apps/api/src/calendar/calendar.controller.ts b/apps/api/src/calendar/calendar.controller.ts new file mode 100644 index 0000000..98e1276 --- /dev/null +++ b/apps/api/src/calendar/calendar.controller.ts @@ -0,0 +1,131 @@ +import { + Body, + Controller, + Delete, + ForbiddenException, + Get, + Param, + Patch, + Post, + Query, + Req, +} from '@nestjs/common'; +import { Request } from 'express'; +import { CalendarService } from './calendar.service'; +import { CreateCalendarSourceDto } from './dto/create-calendar-source.dto'; +import { UpdateCalendarSourceDto } from './dto/update-calendar-source.dto'; +import { CalendarEventsQueryDto } from './dto/calendar-events-query.dto'; + +/** + * REST controller for calendar source management and event aggregation. + * + * All endpoints require JWT auth (global JwtAuthGuard). + * Every handler extracts userId and tenantId from the request + * and scopes all operations to the calling user (T-05-12). + * + * Routes: + * - GET /calendar/sources — list user's calendar sources (no passwords) + * - POST /calendar/sources — add a new calendar source + * - PATCH /calendar/sources/:id — update a calendar source + * - DELETE /calendar/sources/:id — remove a calendar source + * - POST /calendar/sources/:id/test — test connection to a source + * - GET /calendar/events — aggregated events from visible sources + */ +@Controller('calendar') +export class CalendarController { + constructor(private readonly calendarService: CalendarService) {} + + private extractContext(req: Request) { + const userId = (req as any).user?.id; + const tenantId = + (req as any).tenantId ?? (req as any).user?.tenantId; + + if (!tenantId) { + throw new ForbiddenException('No tenant context'); + } + if (!userId) { + throw new ForbiddenException('No user context'); + } + + return { userId, tenantId }; + } + + /** + * GET /calendar/sources + * Returns all calendar sources for the user WITHOUT encryptedPassword. + * T-05-09: Uses Prisma select to exclude credentials; returns hasCredentials boolean. + */ + @Get('sources') + async getSources(@Req() req: Request) { + const { userId } = this.extractContext(req); + return this.calendarService.getSources(userId); + } + + /** + * POST /calendar/sources + * Creates a new calendar source. Password is encrypted before storage. + * T-05-11: URL validated as https-only + SSRF private IP check. + */ + @Post('sources') + async addSource( + @Req() req: Request, + @Body() dto: CreateCalendarSourceDto, + ) { + const { userId, tenantId } = this.extractContext(req); + return this.calendarService.addSource(userId, tenantId, dto); + } + + /** + * PATCH /calendar/sources/:id + * Updates an existing calendar source. Ownership verified. + * CAL-02: isVisible toggle is part of this DTO. + */ + @Patch('sources/:id') + async updateSource( + @Param('id') id: string, + @Req() req: Request, + @Body() dto: UpdateCalendarSourceDto, + ) { + const { userId } = this.extractContext(req); + return this.calendarService.updateSource(id, userId, dto); + } + + /** + * DELETE /calendar/sources/:id + * Removes a calendar source. Ownership verified (T-05-12). + */ + @Delete('sources/:id') + async deleteSource( + @Param('id') id: string, + @Req() req: Request, + ) { + const { userId } = this.extractContext(req); + return this.calendarService.deleteSource(id, userId); + } + + /** + * POST /calendar/sources/:id/test + * Tests connection to a calendar source. Implemented in Task 3. + */ + @Post('sources/:id/test') + async testSource( + @Param('id') id: string, + @Req() req: Request, + ) { + const { userId } = this.extractContext(req); + return this.calendarService.testConnection(id, userId); + } + + /** + * GET /calendar/events + * Returns aggregated events from all visible sources. Implemented in Task 3. + */ + @Get('events') + async getEvents( + @Req() req: Request, + @Query() query: CalendarEventsQueryDto, + ) { + const { userId } = this.extractContext(req); + return this.calendarService.aggregateEvents(userId, query.from, query.to); + } +} diff --git a/apps/api/src/calendar/calendar.module.ts b/apps/api/src/calendar/calendar.module.ts new file mode 100644 index 0000000..2136c56 --- /dev/null +++ b/apps/api/src/calendar/calendar.module.ts @@ -0,0 +1,33 @@ +import { Module } from '@nestjs/common'; +import { CalendarController } from './calendar.controller'; +import { CalendarService } from './calendar.service'; +import { CalendarCryptoService } from './crypto.service'; +import { CalDAVProvider } from './providers/caldav.provider'; +import { ICSProvider } from './providers/ics.provider'; +import { ExchangeProvider } from './providers/exchange.provider'; + +/** + * NestJS module for calendar source management and event aggregation. + * + * Provides: + * - CalendarCryptoService: AES-256-GCM encryption for calendar credentials + * - CalendarService: Source CRUD + event aggregation across providers + * - CalDAVProvider: CalDAV protocol integration via tsdav + * - ICSProvider: ICS file fetch + parse via node-ical + * - ExchangeProvider: Exchange Online (Graph) + Exchange Server (EWS) integration + * - CalendarController: REST API for sources and events + * + * Exports CalendarService for potential use by other modules. + */ +@Module({ + controllers: [CalendarController], + providers: [ + CalendarService, + CalendarCryptoService, + CalDAVProvider, + ICSProvider, + ExchangeProvider, + ], + exports: [CalendarService], +}) +export class CalendarModule {} diff --git a/apps/api/src/calendar/calendar.service.ts b/apps/api/src/calendar/calendar.service.ts new file mode 100644 index 0000000..9472941 --- /dev/null +++ b/apps/api/src/calendar/calendar.service.ts @@ -0,0 +1,281 @@ +import { + ForbiddenException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { CalendarCryptoService } from './crypto.service'; +import { CreateCalendarSourceDto } from './dto/create-calendar-source.dto'; +import { UpdateCalendarSourceDto } from './dto/update-calendar-source.dto'; + +/** + * Common interface for normalized calendar events across all provider types. + */ +export interface CalendarEvent { + id: string; + sourceId: string; + title: string; + start: Date; + end: Date; + allDay: boolean; + location?: string; + description?: string; + color?: string; +} + +/** + * Provider interface for calendar source integrations. + * Each provider (ICS, CalDAV, Exchange) implements this contract. + */ +export interface CalendarProvider { + fetchEvents( + source: { url: string; username?: string; password?: string; exchangeMode?: string | null; id: string; color?: string | null }, + from: Date, + to: Date, + ): Promise; + + testConnection( + source: { url: string; username?: string; password?: string; exchangeMode?: string | null; id: string }, + ): Promise; +} + +/** + * Prisma `select` for safe source responses — NEVER includes encryptedPassword. + * T-05-09: Return hasCredentials boolean instead. + */ +const SOURCE_SAFE_SELECT = { + id: true, + userId: true, + tenantId: true, + name: true, + type: true, + exchangeMode: true, + url: true, + username: true, + // encryptedPassword: NEVER included — T-05-09 + color: true, + isVisible: true, + syncIntervalMin: true, + lastSyncAt: true, + lastSyncError: true, + createdAt: true, + updatedAt: true, +} as const; + +/** + * Private IP ranges for SSRF protection (T-05-11). + */ +const PRIVATE_IP_PATTERNS = [ + /^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, + /^192\.168\.\d{1,3}\.\d{1,3}$/, + /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, + /^169\.254\.\d{1,3}\.\d{1,3}$/, + /^172\.(1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}$/, + /^0\.0\.0\.0$/, + /^\[::1\]$/, + /^\[fc00:/, + /^\[fd00:/, + /^\[fe80:/, +]; + +/** + * Service for calendar source CRUD and event aggregation. + * + * Source config is per-user (D-09), not per-tenant. + * Credentials encrypted at rest via CalendarCryptoService (T-05-10). + */ +@Injectable() +export class CalendarService { + private readonly logger = new Logger(CalendarService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly crypto: CalendarCryptoService, + ) {} + + /** + * Returns all calendar sources for a user WITHOUT encryptedPassword. + * Adds a `hasCredentials` boolean so the UI knows if credentials are set. + */ + async getSources(userId: string) { + const sources = await this.prisma.calendarSource.findMany({ + where: { userId }, + select: { + ...SOURCE_SAFE_SELECT, + encryptedPassword: true, // Need it only to derive hasCredentials + }, + orderBy: { createdAt: 'asc' }, + }); + + return sources.map(({ encryptedPassword, ...source }) => ({ + ...source, + hasCredentials: !!encryptedPassword, + })); + } + + /** + * Creates a new calendar source. Encrypts password before storage. + * T-05-11: Validates URL against private IP ranges (SSRF). + */ + async addSource(userId: string, tenantId: string, dto: CreateCalendarSourceDto) { + await this.validateUrlNotPrivate(dto.url); + + const data: Record = { + userId, + tenantId, + name: dto.name, + type: dto.type, + url: dto.url, + username: dto.username ?? null, + exchangeMode: dto.exchangeMode ?? null, + color: dto.color ?? '#3B82F6', + }; + + if (dto.password) { + data.encryptedPassword = this.crypto.encrypt(dto.password); + } + + const created = await this.prisma.calendarSource.create({ + data: data as any, + select: SOURCE_SAFE_SELECT, + }); + + return { ...created, hasCredentials: !!dto.password }; + } + + /** + * Updates a calendar source. Ownership check ensures user can only modify their own sources. + * Re-encrypts password if provided; T-05-12 ownership enforcement. + */ + async updateSource(id: string, userId: string, dto: UpdateCalendarSourceDto) { + const existing = await this.prisma.calendarSource.findUnique({ + where: { id }, + select: { userId: true }, + }); + + if (!existing) { + throw new NotFoundException('Calendar source not found'); + } + if (existing.userId !== userId) { + throw new ForbiddenException('Not your calendar source'); + } + + if (dto.url) { + await this.validateUrlNotPrivate(dto.url); + } + + const data: Record = {}; + if (dto.name !== undefined) data.name = dto.name; + if (dto.type !== undefined) data.type = dto.type; + if (dto.url !== undefined) data.url = dto.url; + if (dto.username !== undefined) data.username = dto.username; + if (dto.exchangeMode !== undefined) data.exchangeMode = dto.exchangeMode; + if (dto.color !== undefined) data.color = dto.color; + if (dto.isVisible !== undefined) data.isVisible = dto.isVisible; + + if (dto.password !== undefined) { + data.encryptedPassword = dto.password + ? this.crypto.encrypt(dto.password) + : null; + } + + const updated = await this.prisma.calendarSource.update({ + where: { id }, + data: data as any, + select: { + ...SOURCE_SAFE_SELECT, + encryptedPassword: true, + }, + }); + + const { encryptedPassword, ...safe } = updated; + return { ...safe, hasCredentials: !!encryptedPassword }; + } + + /** + * Deletes a calendar source. Ownership check enforced (T-05-12). + */ + async deleteSource(id: string, userId: string) { + const existing = await this.prisma.calendarSource.findUnique({ + where: { id }, + select: { userId: true }, + }); + + if (!existing) { + throw new NotFoundException('Calendar source not found'); + } + if (existing.userId !== userId) { + throw new ForbiddenException('Not your calendar source'); + } + + await this.prisma.calendarSource.delete({ where: { id } }); + return { deleted: true }; + } + + /** + * Test connection to a calendar source via its provider. + * Stub — full implementation in Task 3. + */ + async testConnection(id: string, userId: string): Promise<{ success: boolean; error?: string }> { + const source = await this.prisma.calendarSource.findUnique({ where: { id } }); + if (!source) throw new NotFoundException('Calendar source not found'); + if (source.userId !== userId) throw new ForbiddenException('Not your calendar source'); + return { success: false, error: 'Not yet implemented' }; + } + + /** + * Aggregate events from all visible sources for a user. + * Stub — full implementation in Task 3. + */ + async aggregateEvents( + userId: string, + from?: string, + to?: string, + ): Promise { + return []; + } + + /** + * Decrypts stored credentials for a source (used internally by providers). + * NEVER expose this in API responses. + */ + decryptSourcePassword(encryptedPassword: string): string { + return this.crypto.decrypt(encryptedPassword); + } + + /** + * SSRF protection: reject URLs that resolve to private IP ranges. + * T-05-11: Combined with https-only DTO validation. + */ + private async validateUrlNotPrivate(url: string): Promise { + try { + const parsed = new URL(url); + const hostname = parsed.hostname; + + // Check against private IP patterns + for (const pattern of PRIVATE_IP_PATTERNS) { + if (pattern.test(hostname)) { + throw new ForbiddenException( + 'Calendar source URL must not point to private/internal networks', + ); + } + } + + // Also block localhost variants + if ( + hostname === 'localhost' || + hostname === 'ip6-localhost' || + hostname.endsWith('.local') || + hostname.endsWith('.internal') + ) { + throw new ForbiddenException( + 'Calendar source URL must not point to localhost or local networks', + ); + } + } catch (error) { + if (error instanceof ForbiddenException) throw error; + throw new ForbiddenException('Invalid calendar source URL'); + } + } +} diff --git a/apps/api/src/calendar/crypto.service.ts b/apps/api/src/calendar/crypto.service.ts new file mode 100644 index 0000000..f562452 --- /dev/null +++ b/apps/api/src/calendar/crypto.service.ts @@ -0,0 +1,75 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'; + +/** + * Encryption service for calendar source credentials. + * + * Uses AES-256-GCM with a 32-byte hex key from CALENDAR_ENCRYPTION_KEY env var. + * Encrypted values are stored as `iv:authTag:ciphertext` (hex-joined). + * + * Security: T-05-10 — credentials encrypted at rest, never returned in GET responses. + */ +@Injectable() +export class CalendarCryptoService implements OnModuleInit { + private key!: Buffer; + + constructor(private readonly configService: ConfigService) {} + + onModuleInit() { + const hexKey = this.configService.get('CALENDAR_ENCRYPTION_KEY'); + + if (!hexKey) { + throw new Error( + 'CALENDAR_ENCRYPTION_KEY is not set. Generate one with: openssl rand -hex 32', + ); + } + + if (hexKey.length !== 64) { + throw new Error( + `CALENDAR_ENCRYPTION_KEY must be a 64-character hex string (32 bytes). Got ${hexKey.length} characters.`, + ); + } + + this.key = Buffer.from(hexKey, 'hex'); + } + + /** + * Encrypts plaintext using AES-256-GCM. + * @returns `iv:authTag:ciphertext` (all hex-encoded, colon-separated) + */ + encrypt(plaintext: string): string { + const iv = randomBytes(12); // 96-bit IV for GCM + const cipher = createCipheriv('aes-256-gcm', this.key, iv); + + let encrypted = cipher.update(plaintext, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + + const authTag = cipher.getAuthTag().toString('hex'); + + return `${iv.toString('hex')}:${authTag}:${encrypted}`; + } + + /** + * Decrypts a stored `iv:authTag:ciphertext` value. + * @returns The original plaintext + */ + decrypt(stored: string): string { + const parts = stored.split(':'); + if (parts.length !== 3) { + throw new Error('Invalid encrypted value format. Expected iv:authTag:ciphertext'); + } + + const [ivHex, authTagHex, ciphertext] = parts; + const iv = Buffer.from(ivHex, 'hex'); + const authTag = Buffer.from(authTagHex, 'hex'); + + const decipher = createDecipheriv('aes-256-gcm', this.key, iv); + decipher.setAuthTag(authTag); + + let decrypted = decipher.update(ciphertext, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; + } +} diff --git a/apps/api/src/calendar/dto/calendar-events-query.dto.ts b/apps/api/src/calendar/dto/calendar-events-query.dto.ts new file mode 100644 index 0000000..7412ac1 --- /dev/null +++ b/apps/api/src/calendar/dto/calendar-events-query.dto.ts @@ -0,0 +1,16 @@ +import { IsDateString, IsOptional } from 'class-validator'; + +/** + * DTO for querying aggregated calendar events. + * + * Default window: now to now + 30 days (applied in CalendarService). + */ +export class CalendarEventsQueryDto { + @IsOptional() + @IsDateString() + from?: string; + + @IsOptional() + @IsDateString() + to?: string; +} diff --git a/apps/api/src/calendar/dto/create-calendar-source.dto.ts b/apps/api/src/calendar/dto/create-calendar-source.dto.ts new file mode 100644 index 0000000..1690282 --- /dev/null +++ b/apps/api/src/calendar/dto/create-calendar-source.dto.ts @@ -0,0 +1,47 @@ +import { + IsBoolean, + IsHexColor, + IsIn, + IsNotEmpty, + IsOptional, + IsString, + IsUrl, +} from 'class-validator'; + +/** + * DTO for creating a new calendar source. + * + * Security: + * - T-05-11: URL restricted to https only (SSRF mitigation) + * - T-05-10: password is plaintext in transit (over TLS), encrypted at rest by CryptoService + */ +export class CreateCalendarSourceDto { + @IsString() + @IsNotEmpty() + name!: string; + + @IsIn(['caldav', 'ics', 'exchange']) + type!: string; + + @IsUrl( + { protocols: ['https'], require_protocol: true }, + { message: 'URL must use HTTPS protocol' }, + ) + url!: string; + + @IsOptional() + @IsString() + username?: string; + + @IsOptional() + @IsString() + password?: string; + + @IsOptional() + @IsIn(['ews', 'graph']) + exchangeMode?: string; + + @IsOptional() + @IsHexColor() + color?: string; +} diff --git a/apps/api/src/calendar/dto/update-calendar-source.dto.ts b/apps/api/src/calendar/dto/update-calendar-source.dto.ts new file mode 100644 index 0000000..75873a4 --- /dev/null +++ b/apps/api/src/calendar/dto/update-calendar-source.dto.ts @@ -0,0 +1,55 @@ +import { + IsBoolean, + IsHexColor, + IsIn, + IsNotEmpty, + IsOptional, + IsString, + IsUrl, +} from 'class-validator'; + +/** + * DTO for updating an existing calendar source. + * All fields are optional — only provided fields are updated. + * + * Security: + * - T-05-11: URL restricted to https only if provided + * - CAL-02: isVisible toggle for widget source visibility + */ +export class UpdateCalendarSourceDto { + @IsOptional() + @IsString() + @IsNotEmpty() + name?: string; + + @IsOptional() + @IsIn(['caldav', 'ics', 'exchange']) + type?: string; + + @IsOptional() + @IsUrl( + { protocols: ['https'], require_protocol: true }, + { message: 'URL must use HTTPS protocol' }, + ) + url?: string; + + @IsOptional() + @IsString() + username?: string; + + @IsOptional() + @IsString() + password?: string; + + @IsOptional() + @IsIn(['ews', 'graph']) + exchangeMode?: string; + + @IsOptional() + @IsHexColor() + color?: string; + + @IsOptional() + @IsBoolean() + isVisible?: boolean; +} diff --git a/apps/api/src/calendar/providers/caldav.provider.ts b/apps/api/src/calendar/providers/caldav.provider.ts new file mode 100644 index 0000000..96e1fae --- /dev/null +++ b/apps/api/src/calendar/providers/caldav.provider.ts @@ -0,0 +1,27 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { CalendarEvent, CalendarProvider } from '../calendar.service'; + +/** + * CalDAV calendar provider — uses tsdav for protocol handling. + * Full implementation in Task 2. + */ +@Injectable() +export class CalDAVProvider implements CalendarProvider { + private readonly logger = new Logger(CalDAVProvider.name); + + async fetchEvents( + source: { url: string; username?: string; password?: string; id: string; color?: string | null }, + from: Date, + to: Date, + ): Promise { + // Stub — implemented in Task 2 + return []; + } + + async testConnection( + source: { url: string; username?: string; password?: string; id: string }, + ): Promise { + // Stub — implemented in Task 2 + return false; + } +} diff --git a/apps/api/src/calendar/providers/exchange.provider.ts b/apps/api/src/calendar/providers/exchange.provider.ts new file mode 100644 index 0000000..a1a359e --- /dev/null +++ b/apps/api/src/calendar/providers/exchange.provider.ts @@ -0,0 +1,28 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { CalendarEvent, CalendarProvider } from '../calendar.service'; + +/** + * Exchange calendar provider — dispatches on exchangeMode ('graph' vs 'ews'). + * Uses @microsoft/microsoft-graph-client for Graph and ews-javascript-api for EWS. + * Full implementation in Task 2. + */ +@Injectable() +export class ExchangeProvider implements CalendarProvider { + private readonly logger = new Logger(ExchangeProvider.name); + + async fetchEvents( + source: { url: string; username?: string; password?: string; exchangeMode?: string | null; id: string; color?: string | null }, + from: Date, + to: Date, + ): Promise { + // Stub — implemented in Task 2 + return []; + } + + async testConnection( + source: { url: string; username?: string; password?: string; exchangeMode?: string | null; id: string }, + ): Promise { + // Stub — implemented in Task 2 + return false; + } +} diff --git a/apps/api/src/calendar/providers/ics.provider.ts b/apps/api/src/calendar/providers/ics.provider.ts new file mode 100644 index 0000000..37406de --- /dev/null +++ b/apps/api/src/calendar/providers/ics.provider.ts @@ -0,0 +1,27 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { CalendarEvent, CalendarProvider } from '../calendar.service'; + +/** + * ICS calendar provider — fetches and parses .ics files via HTTPS. + * Uses node-ical for parsing. Full implementation in Task 2. + */ +@Injectable() +export class ICSProvider implements CalendarProvider { + private readonly logger = new Logger(ICSProvider.name); + + async fetchEvents( + source: { url: string; id: string; color?: string | null }, + from: Date, + to: Date, + ): Promise { + // Stub — implemented in Task 2 + return []; + } + + async testConnection( + source: { url: string; id: string }, + ): Promise { + // Stub — implemented in Task 2 + return false; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a85f2d4..e262a3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: apps/api: dependencies: + '@microsoft/microsoft-graph-client': + specifier: 3.0.7 + version: 3.0.7 '@nestjs-modules/mailer': specifier: ^2.3.7 version: 2.3.7(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(chokidar@4.0.3)(nodemailer@9.0.1)(svgo@4.0.1)(terser@5.48.0)(typescript@5.9.3) @@ -65,9 +68,15 @@ importers: cookie-parser: specifier: ^1.4.7 version: 1.4.7 + ews-javascript-api: + specifier: 0.15.3 + version: 0.15.3 ldapts: specifier: ^8.1.8 version: 8.1.8 + node-ical: + specifier: 0.26.1 + version: 0.26.1 nodemailer: specifier: ^9.0.1 version: 9.0.1 @@ -86,6 +95,9 @@ importers: rxjs: specifier: ^7.0.0 version: 7.8.2 + tsdav: + specifier: 2.2.2 + version: 2.2.2 devDependencies: '@nestjs/cli': specifier: ^11.0.0 @@ -261,6 +273,14 @@ packages: '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@azure/msal-common@14.16.1': + resolution: {integrity: sha512-nyxsA6NA4SVKh5YyRpbSXiMr7oQbwark7JU9LMeg6tJYTSPyAGkdx61wPT4gyxZfxlSxMMEyAsWaubBlNyIa1w==} + engines: {node: '>=0.8.0'} + + '@azure/msal-node@2.16.3': + resolution: {integrity: sha512-CO+SE4weOsfJf+C5LM8argzvotrXw252/ZU6SM2Tz63fEblhH1uuVaaO4ISYFuN4Q6BhTo7I3qIdi8ydUQCqhw==} + engines: {node: '>=16'} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -474,6 +494,13 @@ packages: '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + '@ewsjs/ntlm-client@3.0.1': + resolution: {integrity: sha512-eJEyxiR0Eb1kRU0N96ISubBgyhZJ8PXhw+gj5m5My+M5CqDEn8Ae2xPHiSyo0fUWiSj1A2tQ+kk3PZmursR5kQ==} + engines: {node: '>=4.0.0'} + + '@ewsjs/xhr@3.1.3': + resolution: {integrity: sha512-vUMv1XwUvoGmlhLl0b/zXDB7qIUz4WCQqy70vsdl71RhnWXxnlUA/hIL0VkOBJSLBPEBkhsmERkbhBpnGvpkng==} + '@exodus/bytes@1.15.1': resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -802,10 +829,32 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@js-temporal/polyfill@0.5.1': + resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==} + engines: {node: '>=12'} + '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + '@microsoft/microsoft-graph-client@3.0.7': + resolution: {integrity: sha512-/AazAV/F+HK4LIywF9C+NYHcJo038zEnWkteilcxC1FM/uK/4NVGDKGrxx7nNq1ybspAroRKT4I1FHfxQzxkUw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@azure/identity': '*' + '@azure/msal-browser': '*' + buffer: '*' + stream-browserify: '*' + peerDependenciesMeta: + '@azure/identity': + optional: true + '@azure/msal-browser': + optional: true + buffer: + optional: true + stream-browserify: + optional: true + '@napi-rs/wasm-runtime@1.1.5': resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} peerDependencies: @@ -1687,6 +1736,10 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + '@xmldom/xmldom@0.8.13': + resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} + engines: {node: '>=10.0.0'} + '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -1723,6 +1776,14 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1820,6 +1881,12 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.18.1: + resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} + babel-walk@3.0.0-canary-5: resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} engines: {node: '>= 10.0.0'} @@ -1834,6 +1901,9 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base-64@1.0.0: + resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -2018,6 +2088,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -2237,6 +2311,10 @@ packages: defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -2245,6 +2323,9 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + des.js@1.1.0: + resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} + destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -2421,6 +2502,10 @@ packages: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2484,6 +2569,9 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + ews-javascript-api@0.15.3: + resolution: {integrity: sha512-7re0Q/FSHyM9MgcPxjcNZlQuyzHmU7wHOdODRmQYMgxlGU8HB73cZdysyia4rE+CQCFyqhZqV8vMddYJXDwA9Q==} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -2545,6 +2633,15 @@ packages: resolution: {integrity: sha512-5SM1+H2CcuJ3gGEwTiVo/+nd/hYpNj9Ch3iMDOQ58ndY+VGQ2QdvaUTkd3otjZvYnd/8LF/HkJ5cx7PBq0orCQ==} hasBin: true + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -2556,6 +2653,10 @@ packages: typescript: '>3.6.0' webpack: ^5.11.0 + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -2754,10 +2855,27 @@ packages: htmlparser2@9.1.0: resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + http-cookie-agent@5.0.4: + resolution: {integrity: sha512-OtvikW69RvfyP6Lsequ0fN5R49S+8QcS9zwd58k6VSr6r57T8G29BkPdyrBcSwLq6ExLs9V+rBlfxu7gDstJag==} + engines: {node: '>=14.18.0 <15.0.0 || >=16.0.0'} + peerDependencies: + deasync: ^0.1.26 + tough-cookie: ^4.0.0 + undici: ^5.11.0 + peerDependenciesMeta: + deasync: + optional: true + undici: + optional: true + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -2908,6 +3026,9 @@ packages: js-cookie@3.0.8: resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} + js-md4@0.3.2: + resolution: {integrity: sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==} + js-stringify@1.0.2: resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==} @@ -2918,6 +3039,9 @@ packages: resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true + jsbi@4.3.2: + resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==} + jsdom@29.1.1: resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} @@ -3335,6 +3459,9 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -3448,6 +3575,12 @@ packages: resolution: {integrity: sha512-cOrV1zC8SekUpZ2YGT174iWADRnuttYMtrAypQ1UvYXOUCOUc1DvwB8Dx8DH9+51+WyA0fNVOmUDx1Bc8qztqQ==} hasBin: true + moment-timezone@0.5.48: + resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3531,6 +3664,10 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true + node-ical@0.26.1: + resolution: {integrity: sha512-KoYLpsz7Ga9lPDpt9vy0iKcgcb/9Ix7ICRZd0csLXMl2lZOSONGj7HrcktJFR7Jid1l44Zu1H4k/1nB04rWPgQ==} + engines: {node: '>=20'} + node-releases@2.0.48: resolution: {integrity: sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==} engines: {node: '>=18'} @@ -3949,6 +4086,13 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + pug-attrs@3.0.0: resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==} @@ -4000,6 +4144,9 @@ packages: resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -4138,6 +4285,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -4163,6 +4313,9 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + rrule-temporal@1.5.3: + resolution: {integrity: sha512-qALnXyu4MKNUeykkkO0r6Xxl5or3rM8Cf6ibKIe/29sgmq3tGm1oNq4G1Ddp8Ku3mnKmvC3+3yFAJ3OgOu6OJw==} + run-applescript@5.0.0: resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} engines: {node: '>=12'} @@ -4391,6 +4544,12 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + temporal-polyfill@0.3.2: + resolution: {integrity: sha512-TzHthD/heRK947GNiSu3Y5gSPpeUDH34+LESnfsq8bqpFhsB79HFBX8+Z834IVX68P3EUyRPZK5bL/1fh437Eg==} + + temporal-spec@0.3.1: + resolution: {integrity: sha512-B4TUhezh9knfSIMwt7RVggApDRJZo73uZdj8AacL2mZ8RP5KtLianh2MXxL06GN9ESYiIsiuoLQhgVfwe55Yhw==} + terser-webpack-plugin@5.6.1: resolution: {integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==} engines: {node: '>= 10.13.0'} @@ -4476,6 +4635,10 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + tough-cookie@6.0.1: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} @@ -4498,6 +4661,10 @@ packages: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} + tsdav@2.2.2: + resolution: {integrity: sha512-/uC/RItcdYVxUj8b2gT4CorAkUmDFUuzhgS4XVT+OVzGRE80hH5ldubcWH13dHa2T9rNaXu6UIEWHlQ1UL7H1Q==} + engines: {node: '>=18'} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -4572,6 +4739,10 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -4589,6 +4760,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use-intl@4.13.0: resolution: {integrity: sha512-fAFDrWaASxlhXOipcOyb5VDD+YONqj6+8O8EcG/J7RBoOUF3A8YahRWLN+mBxYMrlMQB8N6Voqk5X+YC+HSL0A==} peerDependencies: @@ -4601,6 +4775,11 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). @@ -4804,6 +4983,10 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -4931,6 +5114,14 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} + '@azure/msal-common@14.16.1': {} + + '@azure/msal-node@2.16.3': + dependencies: + '@azure/msal-common': 14.16.1 + jsonwebtoken: 9.0.3 + uuid: 8.3.2 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -5096,6 +5287,25 @@ snapshots: '@epic-web/invariant@1.0.0': {} + '@ewsjs/ntlm-client@3.0.1': + dependencies: + des.js: 1.1.0 + js-md4: 0.3.2 + optionalDependencies: + extend: 3.0.2 + + '@ewsjs/xhr@3.1.3': + dependencies: + '@ewsjs/ntlm-client': 3.0.1 + axios: 1.18.1 + http-cookie-agent: 5.0.4(tough-cookie@4.1.4) + tough-cookie: 4.1.4 + transitivePeerDependencies: + - deasync + - debug + - supports-color + - undici + '@exodus/bytes@1.15.1': {} '@formatjs/fast-memoize@3.1.6': {} @@ -5384,8 +5594,17 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-temporal/polyfill@0.5.1': + dependencies: + jsbi: 4.3.2 + '@lukeed/csprng@1.1.0': {} + '@microsoft/microsoft-graph-client@3.0.7': + dependencies: + '@babel/runtime': 7.29.7 + tslib: 2.8.1 + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -6252,6 +6471,8 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 + '@xmldom/xmldom@0.8.13': {} + '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} @@ -6283,6 +6504,14 @@ snapshots: acorn@8.17.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + ajv-formats@2.1.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -6372,6 +6601,18 @@ snapshots: assertion-error@2.0.1: {} + asynckit@0.4.0: {} + + axios@1.18.1: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.6 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + babel-walk@3.0.0-canary-5: dependencies: '@babel/types': 7.29.7 @@ -6383,6 +6624,8 @@ snapshots: balanced-match@4.0.4: {} + base-64@1.0.0: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.10.38: {} @@ -6600,6 +6843,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} commander@10.0.1: @@ -6838,10 +7085,17 @@ snapshots: defu@6.1.7: {} + delayed-stream@1.0.0: {} + depd@2.0.0: {} dequal@2.0.3: {} + des.js@1.1.0: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + destr@2.0.5: {} detect-indent@6.1.0: @@ -7017,6 +7271,13 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + escalade@3.2.0: {} escape-goat@3.0.0: @@ -7060,6 +7321,21 @@ snapshots: events@3.3.0: {} + ews-javascript-api@0.15.3: + dependencies: + '@azure/msal-node': 2.16.3 + '@ewsjs/xhr': 3.1.3 + '@xmldom/xmldom': 0.8.13 + base64-js: 1.5.1 + moment: 2.30.1 + moment-timezone: 0.5.48 + uuid: 9.0.1 + transitivePeerDependencies: + - deasync + - debug + - supports-color + - undici + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -7163,6 +7439,8 @@ snapshots: rc: 1.2.8 optional: true + follow-redirects@1.16.0: {} + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -7186,6 +7464,14 @@ snapshots: typescript: 5.9.3 webpack: 5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(postcss@8.5.15) + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -7290,7 +7576,6 @@ snapshots: has-tostringtag@1.0.2: dependencies: has-symbols: 1.1.0 - optional: true hasown@2.0.4: dependencies: @@ -7493,6 +7778,11 @@ snapshots: entities: 4.5.0 optional: true + http-cookie-agent@5.0.4(tough-cookie@4.1.4): + dependencies: + agent-base: 7.1.4 + tough-cookie: 4.1.4 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -7501,6 +7791,13 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: optional: true @@ -7645,6 +7942,8 @@ snapshots: js-cookie@3.0.8: optional: true + js-md4@0.3.2: {} + js-stringify@1.0.2: optional: true @@ -7654,6 +7953,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsbi@4.3.2: {} + jsdom@29.1.1: dependencies: '@asamuzakjp/css-color': 5.1.11 @@ -8278,6 +8579,8 @@ snapshots: min-indent@1.0.1: {} + minimalistic-assert@1.0.1: {} + minimatch@10.2.5: dependencies: brace-expansion: 5.0.6 @@ -8785,6 +9088,12 @@ snapshots: - uncss optional: true + moment-timezone@0.5.48: + dependencies: + moment: 2.30.1 + + moment@2.30.1: {} + ms@2.1.3: {} multer@2.1.1: @@ -8863,6 +9172,11 @@ snapshots: node-gyp-build@4.8.4: {} + node-ical@0.26.1: + dependencies: + rrule-temporal: 1.5.3 + temporal-polyfill: 0.3.2 + node-releases@2.0.48: {} nodemailer@8.0.11: @@ -9337,6 +9651,12 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-from-env@2.1.0: {} + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + pug-attrs@3.0.0: dependencies: constantinople: 4.0.1 @@ -9427,6 +9747,8 @@ snapshots: dependencies: side-channel: 1.1.1 + querystringify@2.2.0: {} + range-parser@1.2.1: {} raw-body@3.0.2: @@ -9648,6 +9970,8 @@ snapshots: require-from-string@2.0.2: {} + requires-port@1.0.0: {} + resize-observer-polyfill@1.5.1: {} resolve-from@4.0.0: {} @@ -9696,6 +10020,10 @@ snapshots: transitivePeerDependencies: - supports-color + rrule-temporal@1.5.3: + dependencies: + '@js-temporal/polyfill': 0.5.1 + run-applescript@5.0.0: dependencies: execa: 5.1.1 @@ -9713,8 +10041,7 @@ snapshots: safer-buffer@2.1.2: {} - sax@1.6.0: - optional: true + sax@1.6.0: {} saxes@6.0.0: dependencies: @@ -9964,6 +10291,12 @@ snapshots: tapable@2.3.3: {} + temporal-polyfill@0.3.2: + dependencies: + temporal-spec: 0.3.1 + + temporal-spec@0.3.1: {} + terser-webpack-plugin@5.6.1(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(postcss@8.5.15)(webpack@5.106.2(@swc/core@1.15.41)(cssnano@7.1.9(postcss@8.5.15))(postcss@8.5.15)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -10014,6 +10347,13 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + tough-cookie@6.0.1: dependencies: tldts: 7.4.3 @@ -10039,6 +10379,14 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tsdav@2.2.2: + dependencies: + base-64: 1.0.0 + debug: 4.4.3 + xml-js: 1.6.11 + transitivePeerDependencies: + - supports-color + tslib@2.8.1: {} turbo@2.9.18: @@ -10129,6 +10477,8 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + universalify@0.2.0: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -10143,6 +10493,11 @@ snapshots: dependencies: punycode: 2.3.1 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + use-intl@4.13.0(react@19.2.7): dependencies: '@formatjs/fast-memoize': 3.1.6 @@ -10155,8 +10510,9 @@ snapshots: utils-merge@1.0.1: {} - uuid@9.0.1: - optional: true + uuid@8.3.2: {} + + uuid@9.0.1: {} valid-data-url@3.0.1: optional: true @@ -10353,6 +10709,10 @@ snapshots: wrappy@1.0.2: {} + xml-js@1.6.11: + dependencies: + sax: 1.6.0 + xml-name-validator@5.0.0: {} xmlchars@2.2.0: {}