UNPKG

@k-msg/channel

Version:

AlimTalk channel and sender number management

1 lines 164 kB
{"version":3,"sources":["../src/index.ts","../src/types/channel.types.ts","../src/kakao/channel.ts","../src/kakao/sender-number.ts","../src/management/crud.ts","../src/management/permissions.ts","../src/verification/business.verify.ts","../src/verification/number.verify.ts","../src/services/channel.service.ts"],"sourcesContent":["// Types\nexport * from './types/channel.types';\n\n// Kakao Channel Management\nexport { KakaoChannelManager } from './kakao/channel';\nexport { KakaoSenderNumberManager } from './kakao/sender-number';\n\n// Channel Management\nexport { \n ChannelCRUD,\n type PaginationOptions,\n type PaginatedResult,\n type ChannelCRUDOptions,\n type AuditLogEntry\n} from './management/crud';\n\nexport { \n PermissionManager,\n type User,\n type Role,\n type Permission,\n type AccessContext,\n type PermissionCheck,\n type PermissionResult,\n ResourceType,\n ActionType,\n PermissionScope\n} from './management/permissions';\n\n// Verification Systems\nexport { \n BusinessVerifier,\n type BusinessInfo,\n type VerificationRequest,\n type AutoVerificationResult,\n type DocumentValidationResult,\n type BusinessVerifierOptions\n} from './verification/business.verify';\n\nexport { \n NumberVerifier,\n type PhoneVerificationRequest,\n type VerificationAttempt,\n type PhoneVerificationStatus,\n type NumberVerifierOptions,\n type SMSProvider,\n type VoiceProvider,\n type PhoneNumberInfo,\n VerificationType,\n VerificationMethod\n} from './verification/number.verify';\n\n// Legacy Channel Service (maintained for backward compatibility)\nexport { ChannelService } from './services/channel.service';","import { z } from 'zod';\n\nexport interface Channel {\n id: string;\n name: string;\n provider: string;\n type: ChannelType;\n status: ChannelStatus;\n profileKey: string;\n senderNumbers: SenderNumber[];\n metadata: ChannelMetadata;\n verification: ChannelVerification;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport enum ChannelType {\n KAKAO_ALIMTALK = 'KAKAO_ALIMTALK',\n KAKAO_FRIENDTALK = 'KAKAO_FRIENDTALK',\n SMS = 'SMS',\n LMS = 'LMS',\n MMS = 'MMS'\n}\n\nexport enum ChannelStatus {\n PENDING = 'PENDING', // 등록 대기\n VERIFYING = 'VERIFYING', // 검증 중\n ACTIVE = 'ACTIVE', // 활성화\n SUSPENDED = 'SUSPENDED', // 일시 정지\n BLOCKED = 'BLOCKED', // 차단됨\n DELETED = 'DELETED' // 삭제됨\n}\n\nexport interface SenderNumber {\n id: string;\n phoneNumber: string;\n status: SenderNumberStatus;\n verificationCode?: string;\n verifiedAt?: Date;\n category: SenderNumberCategory;\n metadata: {\n businessName?: string;\n businessRegistrationNumber?: string;\n contactPerson?: string;\n contactEmail?: string;\n };\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport enum SenderNumberStatus {\n PENDING = 'PENDING', // 등록 대기\n VERIFYING = 'VERIFYING', // 인증 중\n VERIFIED = 'VERIFIED', // 인증 완료\n REJECTED = 'REJECTED', // 반려됨\n BLOCKED = 'BLOCKED' // 차단됨\n}\n\nexport enum SenderNumberCategory {\n BUSINESS = 'BUSINESS', // 사업자\n PERSONAL = 'PERSONAL', // 개인\n GOVERNMENT = 'GOVERNMENT', // 관공서\n NON_PROFIT = 'NON_PROFIT' // 비영리단체\n}\n\nexport interface ChannelMetadata {\n businessInfo?: {\n name: string;\n registrationNumber: string;\n category: string;\n contactPerson: string;\n contactEmail: string;\n contactPhone: string;\n };\n kakaoInfo?: {\n plusFriendId: string;\n brandName: string;\n logoUrl?: string;\n description?: string;\n };\n limits: {\n dailyMessageLimit: number;\n monthlyMessageLimit: number;\n rateLimit: number; // messages per second\n };\n features: {\n supportsBulkSending: boolean;\n supportsScheduling: boolean;\n supportsButtons: boolean;\n maxButtonCount: number;\n };\n}\n\nexport interface ChannelVerification {\n status: VerificationStatus;\n documents: VerificationDocument[];\n verifiedAt?: Date;\n rejectedAt?: Date;\n rejectionReason?: string;\n verifiedBy?: string;\n}\n\nexport enum VerificationStatus {\n NOT_REQUIRED = 'NOT_REQUIRED',\n PENDING = 'PENDING',\n UNDER_REVIEW = 'UNDER_REVIEW',\n VERIFIED = 'VERIFIED',\n REJECTED = 'REJECTED'\n}\n\nexport interface VerificationDocument {\n id: string;\n type: DocumentType;\n fileName: string;\n fileUrl: string;\n uploadedAt: Date;\n status: DocumentStatus;\n}\n\nexport enum DocumentType {\n BUSINESS_REGISTRATION = 'BUSINESS_REGISTRATION',\n BUSINESS_LICENSE = 'BUSINESS_LICENSE',\n ID_CARD = 'ID_CARD',\n AUTHORIZATION_LETTER = 'AUTHORIZATION_LETTER',\n OTHER = 'OTHER'\n}\n\nexport enum DocumentStatus {\n UPLOADED = 'UPLOADED',\n VERIFIED = 'VERIFIED',\n REJECTED = 'REJECTED'\n}\n\n// Request/Response types\nexport interface ChannelCreateRequest {\n name: string;\n type: ChannelType;\n provider: string;\n profileKey: string;\n businessInfo?: {\n name: string;\n registrationNumber: string;\n category: string;\n contactPerson: string;\n contactEmail: string;\n contactPhone: string;\n };\n kakaoInfo?: {\n plusFriendId: string;\n brandName: string;\n logoUrl?: string;\n description?: string;\n };\n}\n\nexport interface SenderNumberCreateRequest {\n phoneNumber: string;\n category: SenderNumberCategory;\n businessInfo?: {\n businessName: string;\n businessRegistrationNumber: string;\n contactPerson: string;\n contactEmail: string;\n };\n}\n\nexport interface ChannelFilters {\n provider?: string;\n type?: ChannelType;\n status?: ChannelStatus;\n verified?: boolean;\n createdAfter?: Date;\n createdBefore?: Date;\n}\n\nexport interface SenderNumberFilters {\n channelId?: string;\n status?: SenderNumberStatus;\n category?: SenderNumberCategory;\n verified?: boolean;\n}\n\n// Zod schemas\nexport const ChannelCreateRequestSchema = z.object({\n name: z.string().min(1).max(100),\n type: z.nativeEnum(ChannelType),\n provider: z.string().min(1),\n profileKey: z.string().min(1),\n businessInfo: z.object({\n name: z.string().min(1),\n registrationNumber: z.string().min(1),\n category: z.string().min(1),\n contactPerson: z.string().min(1),\n contactEmail: z.string().email(),\n contactPhone: z.string().regex(/^[0-9-+\\s()]+$/),\n }).optional(),\n kakaoInfo: z.object({\n plusFriendId: z.string().min(1),\n brandName: z.string().min(1),\n logoUrl: z.string().url().optional(),\n description: z.string().max(500).optional(),\n }).optional(),\n});\n\nexport const SenderNumberCreateRequestSchema = z.object({\n phoneNumber: z.string().regex(/^[0-9]{10,11}$/),\n category: z.nativeEnum(SenderNumberCategory),\n businessInfo: z.object({\n businessName: z.string().min(1),\n businessRegistrationNumber: z.string().min(1),\n contactPerson: z.string().min(1),\n contactEmail: z.string().email(),\n }).optional(),\n});\n\nexport const ChannelFiltersSchema = z.object({\n provider: z.string().optional(),\n type: z.nativeEnum(ChannelType).optional(),\n status: z.nativeEnum(ChannelStatus).optional(),\n verified: z.boolean().optional(),\n createdAfter: z.date().optional(),\n createdBefore: z.date().optional(),\n});\n\nexport const SenderNumberFiltersSchema = z.object({\n channelId: z.string().optional(),\n status: z.nativeEnum(SenderNumberStatus).optional(),\n category: z.nativeEnum(SenderNumberCategory).optional(),\n verified: z.boolean().optional(),\n});\n\nexport type ChannelCreateRequestType = z.infer<typeof ChannelCreateRequestSchema>;\nexport type SenderNumberCreateRequestType = z.infer<typeof SenderNumberCreateRequestSchema>;\nexport type ChannelFiltersType = z.infer<typeof ChannelFiltersSchema>;\nexport type SenderNumberFiltersType = z.infer<typeof SenderNumberFiltersSchema>;\n\n// Additional types for service compatibility\nexport interface ChannelConfig {\n id: string;\n name: string;\n type: 'alimtalk' | 'sms' | 'lms' | 'friendtalk';\n providerId: string;\n active: boolean;\n settings: Record<string, any>;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface ChannelVerificationResult {\n success: boolean;\n status: string;\n verificationCode?: string;\n error?: string;\n}","import { \n Channel, \n ChannelCreateRequest, \n ChannelStatus, \n ChannelType,\n VerificationStatus \n} from '../types/channel.types';\n\nexport class KakaoChannelManager {\n private channels: Map<string, Channel> = new Map();\n\n async createChannel(request: ChannelCreateRequest): Promise<Channel> {\n // Validate Kakao-specific requirements\n this.validateKakaoChannelRequest(request);\n\n const channelId = this.generateChannelId();\n \n const channel: Channel = {\n id: channelId,\n name: request.name,\n provider: request.provider,\n type: request.type,\n status: ChannelStatus.PENDING,\n profileKey: request.profileKey,\n senderNumbers: [],\n metadata: {\n businessInfo: request.businessInfo,\n kakaoInfo: request.kakaoInfo,\n limits: {\n dailyMessageLimit: 10000,\n monthlyMessageLimit: 300000,\n rateLimit: 10 // 10 messages per second\n },\n features: {\n supportsBulkSending: true,\n supportsScheduling: true,\n supportsButtons: true,\n maxButtonCount: 5\n }\n },\n verification: {\n status: request.businessInfo ? VerificationStatus.PENDING : VerificationStatus.NOT_REQUIRED,\n documents: []\n },\n createdAt: new Date(),\n updatedAt: new Date()\n };\n\n this.channels.set(channelId, channel);\n\n // Start verification process if business info is provided\n if (request.businessInfo) {\n await this.initiateBusinessVerification(channel);\n }\n\n return channel;\n }\n\n private validateKakaoChannelRequest(request: ChannelCreateRequest): void {\n if (request.type !== ChannelType.KAKAO_ALIMTALK && request.type !== ChannelType.KAKAO_FRIENDTALK) {\n throw new Error('Invalid channel type for Kakao channel');\n }\n\n if (!request.kakaoInfo?.plusFriendId) {\n throw new Error('Plus Friend ID is required for Kakao channels');\n }\n\n if (!request.kakaoInfo?.brandName) {\n throw new Error('Brand name is required for Kakao channels');\n }\n\n // Validate Plus Friend ID format\n if (!this.isValidPlusFriendId(request.kakaoInfo.plusFriendId)) {\n throw new Error('Invalid Plus Friend ID format');\n }\n }\n\n private isValidPlusFriendId(plusFriendId: string): boolean {\n // Plus Friend ID should start with @ and contain only allowed characters\n const regex = /^@[a-zA-Z0-9_-]{3,30}$/;\n return regex.test(plusFriendId);\n }\n\n private async initiateBusinessVerification(channel: Channel): Promise<void> {\n // In a real implementation, this would integrate with Kakao's verification API\n // For now, we'll simulate the process\n \n channel.verification.status = VerificationStatus.UNDER_REVIEW;\n channel.status = ChannelStatus.VERIFYING;\n channel.updatedAt = new Date();\n\n // Simulate verification process (in real scenario, this would be handled by webhooks)\n setTimeout(() => {\n this.completeVerification(channel.id, true);\n }, 5000); // 5 seconds for demo\n }\n\n async completeVerification(channelId: string, approved: boolean, rejectionReason?: string): Promise<void> {\n const channel = this.channels.get(channelId);\n if (!channel) {\n throw new Error('Channel not found');\n }\n\n if (approved) {\n channel.verification.status = VerificationStatus.VERIFIED;\n channel.verification.verifiedAt = new Date();\n channel.status = ChannelStatus.ACTIVE;\n } else {\n channel.verification.status = VerificationStatus.REJECTED;\n channel.verification.rejectedAt = new Date();\n channel.verification.rejectionReason = rejectionReason || 'Verification failed';\n channel.status = ChannelStatus.SUSPENDED;\n }\n\n channel.updatedAt = new Date();\n }\n\n async getChannel(channelId: string): Promise<Channel | null> {\n return this.channels.get(channelId) || null;\n }\n\n async updateChannel(channelId: string, updates: Partial<Channel>): Promise<Channel> {\n const channel = this.channels.get(channelId);\n if (!channel) {\n throw new Error('Channel not found');\n }\n\n // Validate updates\n if (updates.metadata?.kakaoInfo?.plusFriendId && !this.isValidPlusFriendId(updates.metadata.kakaoInfo.plusFriendId)) {\n throw new Error('Invalid Plus Friend ID format');\n }\n\n // Apply updates\n Object.assign(channel, updates, { updatedAt: new Date() });\n\n return channel;\n }\n\n async deleteChannel(channelId: string): Promise<boolean> {\n const channel = this.channels.get(channelId);\n if (!channel) {\n return false;\n }\n\n // Soft delete - mark as deleted instead of removing\n channel.status = ChannelStatus.DELETED;\n channel.updatedAt = new Date();\n\n return true;\n }\n\n async listChannels(filters?: {\n status?: ChannelStatus;\n type?: ChannelType;\n verified?: boolean;\n }): Promise<Channel[]> {\n let channels = Array.from(this.channels.values());\n\n if (filters) {\n if (filters.status) {\n channels = channels.filter(c => c.status === filters.status);\n }\n if (filters.type) {\n channels = channels.filter(c => c.type === filters.type);\n }\n if (filters.verified !== undefined) {\n const verifiedStatus = filters.verified ? VerificationStatus.VERIFIED : VerificationStatus.PENDING;\n channels = channels.filter(c => c.verification.status === verifiedStatus);\n }\n }\n\n // Exclude deleted channels unless specifically requested\n return channels.filter(c => c.status !== ChannelStatus.DELETED);\n }\n\n async suspendChannel(channelId: string, reason: string): Promise<void> {\n const channel = this.channels.get(channelId);\n if (!channel) {\n throw new Error('Channel not found');\n }\n\n channel.status = ChannelStatus.SUSPENDED;\n channel.updatedAt = new Date();\n \n // Log suspension reason (in real implementation, save to audit log)\n console.log(`Channel ${channelId} suspended: ${reason}`);\n }\n\n async reactivateChannel(channelId: string): Promise<void> {\n const channel = this.channels.get(channelId);\n if (!channel) {\n throw new Error('Channel not found');\n }\n\n if (channel.verification.status !== VerificationStatus.VERIFIED) {\n throw new Error('Channel must be verified before reactivation');\n }\n\n channel.status = ChannelStatus.ACTIVE;\n channel.updatedAt = new Date();\n }\n\n async checkChannelHealth(channelId: string): Promise<{\n isHealthy: boolean;\n issues: string[];\n recommendations: string[];\n }> {\n const channel = this.channels.get(channelId);\n if (!channel) {\n throw new Error('Channel not found');\n }\n\n const issues: string[] = [];\n const recommendations: string[] = [];\n\n // Check channel status\n if (channel.status !== ChannelStatus.ACTIVE) {\n issues.push(`Channel status is ${channel.status}`);\n }\n\n // Check verification status\n if (channel.verification.status !== VerificationStatus.VERIFIED && \n channel.verification.status !== VerificationStatus.NOT_REQUIRED) {\n issues.push(`Channel verification is ${channel.verification.status}`);\n }\n\n // Check if channel has sender numbers\n if (channel.senderNumbers.length === 0) {\n recommendations.push('Add at least one verified sender number');\n }\n\n // Check business info completeness\n if (!channel.metadata.businessInfo) {\n recommendations.push('Complete business information for better deliverability');\n }\n\n return {\n isHealthy: issues.length === 0,\n issues,\n recommendations\n };\n }\n\n private generateChannelId(): string {\n return `kakao_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n }\n}","import { \n SenderNumber, \n SenderNumberCreateRequest, \n SenderNumberStatus, \n SenderNumberCategory \n} from '../types/channel.types';\n\nexport class KakaoSenderNumberManager {\n private senderNumbers: Map<string, SenderNumber> = new Map();\n private verificationCodes: Map<string, { code: string; expiresAt: Date }> = new Map();\n\n async addSenderNumber(\n channelId: string, \n request: SenderNumberCreateRequest\n ): Promise<SenderNumber> {\n // Validate phone number format\n this.validatePhoneNumber(request.phoneNumber);\n\n // Check if number is already registered\n const existingNumber = this.findSenderNumberByPhone(request.phoneNumber);\n if (existingNumber) {\n throw new Error('Phone number is already registered');\n }\n\n const senderNumberId = this.generateSenderNumberId();\n \n const senderNumber: SenderNumber = {\n id: senderNumberId,\n phoneNumber: request.phoneNumber,\n status: SenderNumberStatus.PENDING,\n category: request.category,\n metadata: {\n businessName: request.businessInfo?.businessName,\n businessRegistrationNumber: request.businessInfo?.businessRegistrationNumber,\n contactPerson: request.businessInfo?.contactPerson,\n contactEmail: request.businessInfo?.contactEmail,\n },\n createdAt: new Date(),\n updatedAt: new Date()\n };\n\n this.senderNumbers.set(senderNumberId, senderNumber);\n\n // Initiate verification process\n await this.initiateVerification(senderNumber);\n\n return senderNumber;\n }\n\n private validatePhoneNumber(phoneNumber: string): void {\n // Korean phone number validation\n const regex = /^(010|011|016|017|018|019)[0-9]{7,8}$/;\n if (!regex.test(phoneNumber)) {\n throw new Error('Invalid Korean phone number format');\n }\n }\n\n private findSenderNumberByPhone(phoneNumber: string): SenderNumber | undefined {\n return Array.from(this.senderNumbers.values())\n .find(sn => sn.phoneNumber === phoneNumber);\n }\n\n private async initiateVerification(senderNumber: SenderNumber): Promise<void> {\n // Generate verification code\n const verificationCode = this.generateVerificationCode();\n const expiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes\n\n // Store verification code\n this.verificationCodes.set(senderNumber.id, {\n code: verificationCode,\n expiresAt\n });\n\n // Update sender number status\n senderNumber.status = SenderNumberStatus.VERIFYING;\n senderNumber.verificationCode = verificationCode;\n senderNumber.updatedAt = new Date();\n\n // In a real implementation, send SMS to the phone number\n console.log(`Verification code for ${senderNumber.phoneNumber}: ${verificationCode}`);\n \n // Simulate SMS sending\n await this.sendVerificationSMS(senderNumber.phoneNumber, verificationCode);\n }\n\n private async sendVerificationSMS(phoneNumber: string, code: string): Promise<void> {\n // In a real implementation, this would use an SMS provider\n console.log(`Sending SMS to ${phoneNumber}: Your verification code is ${code}`);\n }\n\n async verifySenderNumber(senderNumberId: string, code: string): Promise<boolean> {\n const senderNumber = this.senderNumbers.get(senderNumberId);\n if (!senderNumber) {\n throw new Error('Sender number not found');\n }\n\n const verification = this.verificationCodes.get(senderNumberId);\n if (!verification) {\n throw new Error('No verification code found');\n }\n\n // Check if code is expired\n if (new Date() > verification.expiresAt) {\n throw new Error('Verification code has expired');\n }\n\n // Check if code matches\n if (verification.code !== code) {\n return false;\n }\n\n // Mark as verified\n senderNumber.status = SenderNumberStatus.VERIFIED;\n senderNumber.verifiedAt = new Date();\n senderNumber.updatedAt = new Date();\n delete senderNumber.verificationCode;\n\n // Clean up verification code\n this.verificationCodes.delete(senderNumberId);\n\n return true;\n }\n\n async resendVerificationCode(senderNumberId: string): Promise<void> {\n const senderNumber = this.senderNumbers.get(senderNumberId);\n if (!senderNumber) {\n throw new Error('Sender number not found');\n }\n\n if (senderNumber.status !== SenderNumberStatus.VERIFYING) {\n throw new Error('Sender number is not in verifying status');\n }\n\n // Check rate limiting (prevent spam)\n const lastVerification = this.verificationCodes.get(senderNumberId);\n if (lastVerification) {\n const timeSinceLastCode = Date.now() - (lastVerification.expiresAt.getTime() - 5 * 60 * 1000);\n if (timeSinceLastCode < 60 * 1000) { // 1 minute cooldown\n throw new Error('Please wait before requesting a new verification code');\n }\n }\n\n // Generate new verification code\n await this.initiateVerification(senderNumber);\n }\n\n async getSenderNumber(senderNumberId: string): Promise<SenderNumber | null> {\n return this.senderNumbers.get(senderNumberId) || null;\n }\n\n async listSenderNumbers(filters?: {\n channelId?: string;\n status?: SenderNumberStatus;\n category?: SenderNumberCategory;\n verified?: boolean;\n }): Promise<SenderNumber[]> {\n let senderNumbers = Array.from(this.senderNumbers.values());\n\n if (filters) {\n if (filters.status) {\n senderNumbers = senderNumbers.filter(sn => sn.status === filters.status);\n }\n if (filters.category) {\n senderNumbers = senderNumbers.filter(sn => sn.category === filters.category);\n }\n if (filters.verified !== undefined) {\n if (filters.verified) {\n senderNumbers = senderNumbers.filter(sn => sn.status === SenderNumberStatus.VERIFIED);\n } else {\n senderNumbers = senderNumbers.filter(sn => sn.status !== SenderNumberStatus.VERIFIED);\n }\n }\n }\n\n return senderNumbers;\n }\n\n async updateSenderNumber(\n senderNumberId: string, \n updates: Partial<SenderNumber>\n ): Promise<SenderNumber> {\n const senderNumber = this.senderNumbers.get(senderNumberId);\n if (!senderNumber) {\n throw new Error('Sender number not found');\n }\n\n // Prevent updating certain fields\n const allowedUpdates = { ...updates };\n delete allowedUpdates.id;\n delete allowedUpdates.phoneNumber;\n delete allowedUpdates.verifiedAt;\n delete allowedUpdates.createdAt;\n\n Object.assign(senderNumber, allowedUpdates, { updatedAt: new Date() });\n\n return senderNumber;\n }\n\n async deleteSenderNumber(senderNumberId: string): Promise<boolean> {\n const senderNumber = this.senderNumbers.get(senderNumberId);\n if (!senderNumber) {\n return false;\n }\n\n // Check if sender number is being used\n if (await this.isSenderNumberInUse(senderNumberId)) {\n throw new Error('Cannot delete sender number that is currently in use');\n }\n\n this.senderNumbers.delete(senderNumberId);\n this.verificationCodes.delete(senderNumberId);\n\n return true;\n }\n\n private async isSenderNumberInUse(senderNumberId: string): Promise<boolean> {\n // In a real implementation, check if any templates or active campaigns use this sender number\n return false;\n }\n\n async blockSenderNumber(senderNumberId: string, reason: string): Promise<void> {\n const senderNumber = this.senderNumbers.get(senderNumberId);\n if (!senderNumber) {\n throw new Error('Sender number not found');\n }\n\n senderNumber.status = SenderNumberStatus.BLOCKED;\n senderNumber.updatedAt = new Date();\n\n // Log blocking reason (in real implementation, save to audit log)\n console.log(`Sender number ${senderNumberId} blocked: ${reason}`);\n }\n\n async unblockSenderNumber(senderNumberId: string): Promise<void> {\n const senderNumber = this.senderNumbers.get(senderNumberId);\n if (!senderNumber) {\n throw new Error('Sender number not found');\n }\n\n if (senderNumber.status !== SenderNumberStatus.BLOCKED) {\n throw new Error('Sender number is not blocked');\n }\n\n // Restore to previous status (assume verified if was blocked)\n senderNumber.status = senderNumber.verifiedAt ? \n SenderNumberStatus.VERIFIED : \n SenderNumberStatus.PENDING;\n senderNumber.updatedAt = new Date();\n }\n\n async validateSenderNumberForSending(senderNumberId: string): Promise<{\n isValid: boolean;\n errors: string[];\n }> {\n const senderNumber = this.senderNumbers.get(senderNumberId);\n const errors: string[] = [];\n\n if (!senderNumber) {\n errors.push('Sender number not found');\n return { isValid: false, errors };\n }\n\n if (senderNumber.status !== SenderNumberStatus.VERIFIED) {\n errors.push(`Sender number status is ${senderNumber.status}, must be verified`);\n }\n\n if (!senderNumber.verifiedAt) {\n errors.push('Sender number has not been verified');\n }\n\n // Check if verification is recent (within 1 year)\n if (senderNumber.verifiedAt) {\n const oneYearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000);\n if (senderNumber.verifiedAt < oneYearAgo) {\n errors.push('Sender number verification has expired');\n }\n }\n\n return {\n isValid: errors.length === 0,\n errors\n };\n }\n\n private generateSenderNumberId(): string {\n return `sn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n }\n\n private generateVerificationCode(): string {\n return Math.floor(100000 + Math.random() * 900000).toString();\n }\n\n // Cleanup expired verification codes\n cleanup(): void {\n const now = new Date();\n for (const [id, verification] of this.verificationCodes) {\n if (now > verification.expiresAt) {\n this.verificationCodes.delete(id);\n \n // Reset sender number status if verification expired\n const senderNumber = this.senderNumbers.get(id);\n if (senderNumber && senderNumber.status === SenderNumberStatus.VERIFYING) {\n senderNumber.status = SenderNumberStatus.PENDING;\n delete senderNumber.verificationCode;\n senderNumber.updatedAt = new Date();\n }\n }\n }\n }\n}","/**\n * Channel CRUD Operations\n * 채널 생성, 조회, 수정, 삭제 통합 관리\n */\n\nimport { EventEmitter } from 'events';\nimport {\n Channel,\n SenderNumber,\n ChannelCreateRequest,\n SenderNumberCreateRequest,\n ChannelFilters,\n SenderNumberFilters,\n ChannelStatus,\n SenderNumberStatus,\n ChannelType,\n VerificationStatus\n} from '../types/channel.types';\n\nexport interface PaginationOptions {\n page: number;\n limit: number;\n sortBy?: string;\n sortOrder?: 'asc' | 'desc';\n}\n\nexport interface PaginatedResult<T> {\n data: T[];\n total: number;\n page: number;\n limit: number;\n totalPages: number;\n hasNext: boolean;\n hasPrev: boolean;\n}\n\nexport interface ChannelCRUDOptions {\n enableAuditLog: boolean;\n enableEventEmission: boolean;\n defaultPageSize: number;\n maxPageSize: number;\n enableSoftDelete: boolean;\n autoCleanup: boolean;\n cleanupInterval: number; // in milliseconds\n}\n\nexport interface AuditLogEntry {\n id: string;\n entityType: 'channel' | 'senderNumber';\n entityId: string;\n action: 'create' | 'read' | 'update' | 'delete' | 'verify' | 'suspend' | 'activate';\n userId?: string;\n timestamp: Date;\n changes?: {\n before: any;\n after: any;\n };\n metadata?: Record<string, any>;\n}\n\nexport class ChannelCRUD extends EventEmitter {\n private channels = new Map<string, Channel>();\n private senderNumbers = new Map<string, SenderNumber>();\n private auditLogs: AuditLogEntry[] = [];\n private cleanupTimer?: NodeJS.Timeout;\n\n private defaultOptions: ChannelCRUDOptions = {\n enableAuditLog: true,\n enableEventEmission: true,\n defaultPageSize: 20,\n maxPageSize: 100,\n enableSoftDelete: true,\n autoCleanup: true,\n cleanupInterval: 3600000 // 1 hour\n };\n\n constructor(private options: Partial<ChannelCRUDOptions> = {}) {\n super();\n this.options = { ...this.defaultOptions, ...options };\n\n if (this.options.autoCleanup) {\n this.startAutoCleanup();\n }\n }\n\n // Channel CRUD Operations\n async createChannel(request: ChannelCreateRequest, userId?: string): Promise<Channel> {\n const channelId = this.generateChannelId();\n \n const channel: Channel = {\n id: channelId,\n name: request.name,\n provider: request.provider,\n type: request.type,\n status: ChannelStatus.PENDING,\n profileKey: request.profileKey,\n senderNumbers: [],\n metadata: {\n businessInfo: request.businessInfo,\n kakaoInfo: request.kakaoInfo,\n limits: this.getDefaultLimits(request.type),\n features: this.getDefaultFeatures(request.type)\n },\n verification: {\n status: request.businessInfo ? VerificationStatus.PENDING : VerificationStatus.NOT_REQUIRED,\n documents: []\n },\n createdAt: new Date(),\n updatedAt: new Date()\n };\n\n this.channels.set(channelId, channel);\n\n // Audit log\n if (this.options.enableAuditLog) {\n this.addAuditLog('channel', channelId, 'create', userId, undefined, channel);\n }\n\n // Event emission\n if (this.options.enableEventEmission) {\n this.emit('channel:created', { channel, userId });\n }\n\n return channel;\n }\n\n async getChannel(channelId: string, userId?: string): Promise<Channel | null> {\n const channel = this.channels.get(channelId);\n \n if (channel && this.options.enableAuditLog) {\n this.addAuditLog('channel', channelId, 'read', userId);\n }\n\n return channel || null;\n }\n\n async updateChannel(\n channelId: string, \n updates: Partial<Omit<Channel, 'id' | 'createdAt' | 'updatedAt'>>,\n userId?: string\n ): Promise<Channel> {\n const channel = this.channels.get(channelId);\n if (!channel) {\n throw new Error(`Channel ${channelId} not found`);\n }\n\n const before = this.options.enableAuditLog ? { ...channel } : undefined;\n \n // Apply updates\n const updatedChannel = {\n ...channel,\n ...updates,\n id: channelId, // Ensure ID doesn't change\n updatedAt: new Date()\n };\n\n this.channels.set(channelId, updatedChannel);\n\n // Audit log\n if (this.options.enableAuditLog) {\n this.addAuditLog('channel', channelId, 'update', userId, before, updatedChannel);\n }\n\n // Event emission\n if (this.options.enableEventEmission) {\n this.emit('channel:updated', { \n channel: updatedChannel, \n previousChannel: channel,\n userId \n });\n }\n\n return updatedChannel;\n }\n\n async deleteChannel(channelId: string, userId?: string): Promise<boolean> {\n const channel = this.channels.get(channelId);\n if (!channel) {\n return false;\n }\n\n if (this.options.enableSoftDelete) {\n // Soft delete - mark as deleted\n channel.status = ChannelStatus.DELETED;\n channel.updatedAt = new Date();\n } else {\n // Hard delete - remove from memory\n this.channels.delete(channelId);\n \n // Also delete associated sender numbers\n for (const [id, senderNumber] of this.senderNumbers) {\n // In a real implementation, we'd have a channelId field in SenderNumber\n // For now, we'll skip this cleanup\n }\n }\n\n // Audit log\n if (this.options.enableAuditLog) {\n this.addAuditLog('channel', channelId, 'delete', userId, channel);\n }\n\n // Event emission\n if (this.options.enableEventEmission) {\n this.emit('channel:deleted', { channel, userId });\n }\n\n return true;\n }\n\n async listChannels(\n filters: ChannelFilters = {}, \n pagination: PaginationOptions = { page: 1, limit: this.options.defaultPageSize! }\n ): Promise<PaginatedResult<Channel>> {\n let channels = Array.from(this.channels.values());\n\n // Apply filters\n if (filters.provider) {\n channels = channels.filter(c => c.provider === filters.provider);\n }\n if (filters.type) {\n channels = channels.filter(c => c.type === filters.type);\n }\n if (filters.status) {\n channels = channels.filter(c => c.status === filters.status);\n }\n if (filters.verified !== undefined) {\n const targetStatus = filters.verified ? VerificationStatus.VERIFIED : VerificationStatus.PENDING;\n channels = channels.filter(c => c.verification.status === targetStatus);\n }\n if (filters.createdAfter) {\n channels = channels.filter(c => c.createdAt >= filters.createdAfter!);\n }\n if (filters.createdBefore) {\n channels = channels.filter(c => c.createdAt <= filters.createdBefore!);\n }\n\n // Exclude soft deleted channels unless specifically requested\n if (!filters.status || filters.status !== ChannelStatus.DELETED) {\n channels = channels.filter(c => c.status !== ChannelStatus.DELETED);\n }\n\n // Apply sorting\n const sortBy = pagination.sortBy || 'createdAt';\n const sortOrder = pagination.sortOrder || 'desc';\n \n channels.sort((a, b) => {\n let aValue: any, bValue: any;\n \n switch (sortBy) {\n case 'name':\n aValue = a.name;\n bValue = b.name;\n break;\n case 'createdAt':\n aValue = a.createdAt.getTime();\n bValue = b.createdAt.getTime();\n break;\n case 'updatedAt':\n aValue = a.updatedAt.getTime();\n bValue = b.updatedAt.getTime();\n break;\n default:\n aValue = a.createdAt.getTime();\n bValue = b.createdAt.getTime();\n }\n\n if (sortOrder === 'asc') {\n return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;\n } else {\n return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;\n }\n });\n\n // Apply pagination\n const total = channels.length;\n const limit = Math.min(pagination.limit, this.options.maxPageSize!);\n const page = Math.max(1, pagination.page);\n const offset = (page - 1) * limit;\n const paginatedChannels = channels.slice(offset, offset + limit);\n\n return {\n data: paginatedChannels,\n total,\n page,\n limit,\n totalPages: Math.ceil(total / limit),\n hasNext: offset + limit < total,\n hasPrev: page > 1\n };\n }\n\n // Sender Number CRUD Operations\n async createSenderNumber(\n channelId: string,\n request: SenderNumberCreateRequest,\n userId?: string\n ): Promise<SenderNumber> {\n const channel = this.channels.get(channelId);\n if (!channel) {\n throw new Error(`Channel ${channelId} not found`);\n }\n\n const senderNumberId = this.generateSenderNumberId();\n \n const senderNumber: SenderNumber = {\n id: senderNumberId,\n phoneNumber: request.phoneNumber,\n status: SenderNumberStatus.PENDING,\n category: request.category,\n metadata: {\n businessName: request.businessInfo?.businessName,\n businessRegistrationNumber: request.businessInfo?.businessRegistrationNumber,\n contactPerson: request.businessInfo?.contactPerson,\n contactEmail: request.businessInfo?.contactEmail,\n },\n createdAt: new Date(),\n updatedAt: new Date()\n };\n\n this.senderNumbers.set(senderNumberId, senderNumber);\n\n // Add to channel's sender numbers\n channel.senderNumbers.push(senderNumber);\n channel.updatedAt = new Date();\n\n // Audit log\n if (this.options.enableAuditLog) {\n this.addAuditLog('senderNumber', senderNumberId, 'create', userId, undefined, senderNumber);\n }\n\n // Event emission\n if (this.options.enableEventEmission) {\n this.emit('senderNumber:created', { senderNumber, channelId, userId });\n }\n\n return senderNumber;\n }\n\n async getSenderNumber(senderNumberId: string, userId?: string): Promise<SenderNumber | null> {\n const senderNumber = this.senderNumbers.get(senderNumberId);\n \n if (senderNumber && this.options.enableAuditLog) {\n this.addAuditLog('senderNumber', senderNumberId, 'read', userId);\n }\n\n return senderNumber || null;\n }\n\n async updateSenderNumber(\n senderNumberId: string,\n updates: Partial<Omit<SenderNumber, 'id' | 'phoneNumber' | 'createdAt' | 'updatedAt'>>,\n userId?: string\n ): Promise<SenderNumber> {\n const senderNumber = this.senderNumbers.get(senderNumberId);\n if (!senderNumber) {\n throw new Error(`Sender number ${senderNumberId} not found`);\n }\n\n const before = this.options.enableAuditLog ? { ...senderNumber } : undefined;\n \n // Apply updates\n const updatedSenderNumber = {\n ...senderNumber,\n ...updates,\n id: senderNumberId, // Ensure ID doesn't change\n updatedAt: new Date()\n };\n\n this.senderNumbers.set(senderNumberId, updatedSenderNumber);\n\n // Update in channel's sender numbers array\n for (const channel of this.channels.values()) {\n const index = channel.senderNumbers.findIndex(sn => sn.id === senderNumberId);\n if (index !== -1) {\n channel.senderNumbers[index] = updatedSenderNumber;\n channel.updatedAt = new Date();\n break;\n }\n }\n\n // Audit log\n if (this.options.enableAuditLog) {\n this.addAuditLog('senderNumber', senderNumberId, 'update', userId, before, updatedSenderNumber);\n }\n\n // Event emission\n if (this.options.enableEventEmission) {\n this.emit('senderNumber:updated', { \n senderNumber: updatedSenderNumber, \n previousSenderNumber: senderNumber,\n userId \n });\n }\n\n return updatedSenderNumber;\n }\n\n async deleteSenderNumber(senderNumberId: string, userId?: string): Promise<boolean> {\n const senderNumber = this.senderNumbers.get(senderNumberId);\n if (!senderNumber) {\n return false;\n }\n\n // Remove from memory\n this.senderNumbers.delete(senderNumberId);\n\n // Remove from channel's sender numbers array\n for (const channel of this.channels.values()) {\n const index = channel.senderNumbers.findIndex(sn => sn.id === senderNumberId);\n if (index !== -1) {\n channel.senderNumbers.splice(index, 1);\n channel.updatedAt = new Date();\n break;\n }\n }\n\n // Audit log\n if (this.options.enableAuditLog) {\n this.addAuditLog('senderNumber', senderNumberId, 'delete', userId, senderNumber);\n }\n\n // Event emission\n if (this.options.enableEventEmission) {\n this.emit('senderNumber:deleted', { senderNumber, userId });\n }\n\n return true;\n }\n\n async listSenderNumbers(\n filters: SenderNumberFilters = {},\n pagination: PaginationOptions = { page: 1, limit: this.options.defaultPageSize! }\n ): Promise<PaginatedResult<SenderNumber>> {\n let senderNumbers = Array.from(this.senderNumbers.values());\n\n // Apply filters\n if (filters.channelId) {\n const channel = this.channels.get(filters.channelId);\n if (channel) {\n senderNumbers = channel.senderNumbers;\n } else {\n senderNumbers = [];\n }\n }\n if (filters.status) {\n senderNumbers = senderNumbers.filter(sn => sn.status === filters.status);\n }\n if (filters.category) {\n senderNumbers = senderNumbers.filter(sn => sn.category === filters.category);\n }\n if (filters.verified !== undefined) {\n if (filters.verified) {\n senderNumbers = senderNumbers.filter(sn => sn.status === SenderNumberStatus.VERIFIED);\n } else {\n senderNumbers = senderNumbers.filter(sn => sn.status !== SenderNumberStatus.VERIFIED);\n }\n }\n\n // Apply sorting\n const sortBy = pagination.sortBy || 'createdAt';\n const sortOrder = pagination.sortOrder || 'desc';\n \n senderNumbers.sort((a, b) => {\n let aValue: any, bValue: any;\n \n switch (sortBy) {\n case 'phoneNumber':\n aValue = a.phoneNumber;\n bValue = b.phoneNumber;\n break;\n case 'createdAt':\n aValue = a.createdAt.getTime();\n bValue = b.createdAt.getTime();\n break;\n case 'updatedAt':\n aValue = a.updatedAt.getTime();\n bValue = b.updatedAt.getTime();\n break;\n default:\n aValue = a.createdAt.getTime();\n bValue = b.createdAt.getTime();\n }\n\n if (sortOrder === 'asc') {\n return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;\n } else {\n return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;\n }\n });\n\n // Apply pagination\n const total = senderNumbers.length;\n const limit = Math.min(pagination.limit, this.options.maxPageSize!);\n const page = Math.max(1, pagination.page);\n const offset = (page - 1) * limit;\n const paginatedSenderNumbers = senderNumbers.slice(offset, offset + limit);\n\n return {\n data: paginatedSenderNumbers,\n total,\n page,\n limit,\n totalPages: Math.ceil(total / limit),\n hasNext: offset + limit < total,\n hasPrev: page > 1\n };\n }\n\n // Audit and Analytics\n getAuditLogs(\n entityType?: 'channel' | 'senderNumber',\n entityId?: string,\n limit: number = 100\n ): AuditLogEntry[] {\n let logs = [...this.auditLogs];\n\n if (entityType) {\n logs = logs.filter(log => log.entityType === entityType);\n }\n if (entityId) {\n logs = logs.filter(log => log.entityId === entityId);\n }\n\n return logs\n .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())\n .slice(0, limit);\n }\n\n getStatistics(): {\n channels: {\n total: number;\n byStatus: Record<string, number>;\n byType: Record<string, number>;\n byProvider: Record<string, number>;\n };\n senderNumbers: {\n total: number;\n byStatus: Record<string, number>;\n byCategory: Record<string, number>;\n };\n } {\n const channels = Array.from(this.channels.values());\n const senderNumbers = Array.from(this.senderNumbers.values());\n\n const channelsByStatus: Record<string, number> = {};\n const channelsByType: Record<string, number> = {};\n const channelsByProvider: Record<string, number> = {};\n\n channels.forEach(channel => {\n channelsByStatus[channel.status] = (channelsByStatus[channel.status] || 0) + 1;\n channelsByType[channel.type] = (channelsByType[channel.type] || 0) + 1;\n channelsByProvider[channel.provider] = (channelsByProvider[channel.provider] || 0) + 1;\n });\n\n const senderNumbersByStatus: Record<string, number> = {};\n const senderNumbersByCategory: Record<string, number> = {};\n\n senderNumbers.forEach(senderNumber => {\n senderNumbersByStatus[senderNumber.status] = (senderNumbersByStatus[senderNumber.status] || 0) + 1;\n senderNumbersByCategory[senderNumber.category] = (senderNumbersByCategory[senderNumber.category] || 0) + 1;\n });\n\n return {\n channels: {\n total: channels.length,\n byStatus: channelsByStatus,\n byType: channelsByType,\n byProvider: channelsByProvider\n },\n senderNumbers: {\n total: senderNumbers.length,\n byStatus: senderNumbersByStatus,\n byCategory: senderNumbersByCategory\n }\n };\n }\n\n // Cleanup and Maintenance\n cleanup(): {\n deletedChannels: number;\n expiredAuditLogs: number;\n } {\n let deletedChannels = 0;\n let expiredAuditLogs = 0;\n\n // Clean up soft-deleted channels older than 30 days\n const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);\n \n for (const [id, channel] of this.channels) {\n if (channel.status === ChannelStatus.DELETED && channel.updatedAt < thirtyDaysAgo) {\n this.channels.delete(id);\n deletedChannels++;\n }\n }\n\n // Clean up audit logs older than 90 days\n const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);\n const originalLogCount = this.auditLogs.length;\n this.auditLogs = this.auditLogs.filter(log => log.timestamp >= ninetyDaysAgo);\n expiredAuditLogs = originalLogCount - this.auditLogs.length;\n\n return { deletedChannels, expiredAuditLogs };\n }\n\n destroy(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = undefined;\n }\n \n this.removeAllListeners();\n this.channels.clear();\n this.senderNumbers.clear();\n this.auditLogs = [];\n }\n\n private addAuditLog(\n entityType: 'channel' | 'senderNumber',\n entityId: string,\n action: AuditLogEntry['action'],\n userId?: string,\n before?: any,\n after?: any\n ): void {\n const auditLog: AuditLogEntry = {\n id: this.generateAuditLogId(),\n entityType,\n entityId,\n action,\n userId,\n timestamp: new Date(),\n changes: before || after ? { before, after } : undefined\n };\n\n this.auditLogs.push(auditLog);\n\n // Keep only last 10000 audit logs to prevent memory issues\n if (this.auditLogs.length > 10000) {\n this.auditLogs = this.auditLogs.slice(-10000);\n }\n }\n\n private getDefaultLimits(channelType: ChannelType) {\n switch (channelType) {\n case ChannelType.KAKAO_ALIMTALK:\n return {\n dailyMessageLimit: 10000,\n monthlyMessageLimit: 300000,\n rateLimit: 10\n };\n case ChannelType.KAKAO_FRIENDTALK:\n return {\n dailyMessageLimit: 1000,\n monthlyMessageLimit: 30000,\n rateLimit: 5\n };\n case ChannelType.SMS:\n case ChannelType.LMS:\n case ChannelType.MMS:\n return {\n dailyMessageLimit: 1000,\n monthlyMessageLimit: 30000,\n rateLimit: 3\n };\n default:\n return {\n dailyMessageLimit: 1000,\n monthlyMessageLimit: 30000,\n rateLimit: 1\n };\n }\n }\n\n private getDefaultFeatures(channelType: ChannelType) {\n switch (channelType) {\n case ChannelType.KAKAO_ALIMTALK:\n return {\n supportsBulkSending: true,\n supportsScheduling: true,\n supportsButtons: true,\n maxButtonCount: 5\n };\n case ChannelType.KAKAO_FRIENDTALK:\n return {\n supportsBulkSending: true,\n supportsScheduling: true,\n supportsButtons: false,\n maxButtonCount: 0\n };\n default:\n return {\n supportsBulkSending: false,\n supportsScheduling: false,\n supportsButtons: false,\n maxButtonCount: 0\n };\n }\n }\n\n private startAutoCleanup(): void {\n this.cleanupTimer = setInterval(() => {\n this.cleanup();\n }, this.options.cleanupInterval!);\n }\n\n private generateChannelId(): string {\n return `ch_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n }\n\n private generateSenderNumberId(): string {\n return `sn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n }\n\n private generateAuditLogId(): string {\n return `audit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n }\n}","/**\n * Permission Management System\n * 채널 및 발신번호 액세스 권한 관리\n */\n\nimport { EventEmitter } from 'events';\n\nexport interface User {\n id: string;\n email: string;\n name: string;\n roles: Role[];\n isActive: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface Role {\n id: string;\n name: string;\n permissions: Permission[];\n description?: string;\n isSystem: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface Permission {\n id: string;\n resource: ResourceType;\n action: ActionType;\n scope: PermissionScope;\n conditions?: PermissionCondition[];\n}\n\nexport enum ResourceType {\n CHANNEL = 'channel',\n SENDER_NUMBER = 'senderNumber',\n TEMPLATE = 'template',\n MESSAGE = 'message',\n USER = 'user',\n ROLE = 'role',\n AUDIT_LOG = 'auditLog',\n ANALYTICS = 'analytics'\n}\n\nexport enum ActionType {\n CREATE = 'create',\n READ = 'read',\n UPDATE = 'update',\n DELETE = 'delete',\n VERIFY = 'verify',\n SUSPEND = 'suspend',\n ACTIVATE = 'activate',\n SEND = 'send',\n MANAGE = 'manage'\n}\n\nexport enum PermissionScope {\n GLOBAL = 'global',\n ORGANIZATION = 'organization',\n TEAM = 'team',\n PERSONAL = 'personal'\n}\n\nexport interface PermissionCondition {\n field: string;\n operator: 'equals' | 'not_equals' | 'in' | 'not_in' | 'contains' | 'starts_with';\n value: any;\n}\n\nexport interface AccessContext {\n userId: string;\n organizationId?: string;\n teamId?: string;\n resourceOwnerId?: string;\n metadata?: Record<string, any>;\n}\n\nexport interface PermissionCheck {\n userId: string;\n resource: ResourceType;\n action: ActionType;\n resourceId?: string;\n context?: AccessContext;\n}\n\nexport interface PermissionResult {\n granted: boolean;\n reason?: string;\n matchedPermissions: Permission[];\n deniedReasons: string[];\n}\n\nexport class PermissionManager extends EventEmitter {\n private users = new Map<string, User>();\n private roles = new Map<string, Role>();\n private userRoleCache = new Map<string, Set<string>>();\n private permissionCache = new Map<string, PermissionResult>();\n private cacheExpiry = new Map<string, number>();\n\n private readonly CACHE_DURATION = 5 * 60 * 1000; // 5 minutes\n\n constructor()