UNPKG

@k-msg/channel

Version:

AlimTalk channel and sender number management

1,430 lines (1,423 loc) 86.1 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { ActionType: () => ActionType, BusinessVerifier: () => BusinessVerifier, ChannelCRUD: () => ChannelCRUD, ChannelCreateRequestSchema: () => ChannelCreateRequestSchema, ChannelFiltersSchema: () => ChannelFiltersSchema, ChannelService: () => ChannelService, ChannelStatus: () => ChannelStatus, ChannelType: () => ChannelType, DocumentStatus: () => DocumentStatus, DocumentType: () => DocumentType, KakaoChannelManager: () => KakaoChannelManager, KakaoSenderNumberManager: () => KakaoSenderNumberManager, NumberVerifier: () => NumberVerifier, PermissionManager: () => PermissionManager, PermissionScope: () => PermissionScope, ResourceType: () => ResourceType, SenderNumberCategory: () => SenderNumberCategory, SenderNumberCreateRequestSchema: () => SenderNumberCreateRequestSchema, SenderNumberFiltersSchema: () => SenderNumberFiltersSchema, SenderNumberStatus: () => SenderNumberStatus, VerificationMethod: () => VerificationMethod, VerificationStatus: () => VerificationStatus, VerificationType: () => VerificationType }); module.exports = __toCommonJS(index_exports); // src/types/channel.types.ts var import_zod = require("zod"); var ChannelType = /* @__PURE__ */ ((ChannelType2) => { ChannelType2["KAKAO_ALIMTALK"] = "KAKAO_ALIMTALK"; ChannelType2["KAKAO_FRIENDTALK"] = "KAKAO_FRIENDTALK"; ChannelType2["SMS"] = "SMS"; ChannelType2["LMS"] = "LMS"; ChannelType2["MMS"] = "MMS"; return ChannelType2; })(ChannelType || {}); var ChannelStatus = /* @__PURE__ */ ((ChannelStatus2) => { ChannelStatus2["PENDING"] = "PENDING"; ChannelStatus2["VERIFYING"] = "VERIFYING"; ChannelStatus2["ACTIVE"] = "ACTIVE"; ChannelStatus2["SUSPENDED"] = "SUSPENDED"; ChannelStatus2["BLOCKED"] = "BLOCKED"; ChannelStatus2["DELETED"] = "DELETED"; return ChannelStatus2; })(ChannelStatus || {}); var SenderNumberStatus = /* @__PURE__ */ ((SenderNumberStatus2) => { SenderNumberStatus2["PENDING"] = "PENDING"; SenderNumberStatus2["VERIFYING"] = "VERIFYING"; SenderNumberStatus2["VERIFIED"] = "VERIFIED"; SenderNumberStatus2["REJECTED"] = "REJECTED"; SenderNumberStatus2["BLOCKED"] = "BLOCKED"; return SenderNumberStatus2; })(SenderNumberStatus || {}); var SenderNumberCategory = /* @__PURE__ */ ((SenderNumberCategory3) => { SenderNumberCategory3["BUSINESS"] = "BUSINESS"; SenderNumberCategory3["PERSONAL"] = "PERSONAL"; SenderNumberCategory3["GOVERNMENT"] = "GOVERNMENT"; SenderNumberCategory3["NON_PROFIT"] = "NON_PROFIT"; return SenderNumberCategory3; })(SenderNumberCategory || {}); var VerificationStatus = /* @__PURE__ */ ((VerificationStatus2) => { VerificationStatus2["NOT_REQUIRED"] = "NOT_REQUIRED"; VerificationStatus2["PENDING"] = "PENDING"; VerificationStatus2["UNDER_REVIEW"] = "UNDER_REVIEW"; VerificationStatus2["VERIFIED"] = "VERIFIED"; VerificationStatus2["REJECTED"] = "REJECTED"; return VerificationStatus2; })(VerificationStatus || {}); var DocumentType = /* @__PURE__ */ ((DocumentType2) => { DocumentType2["BUSINESS_REGISTRATION"] = "BUSINESS_REGISTRATION"; DocumentType2["BUSINESS_LICENSE"] = "BUSINESS_LICENSE"; DocumentType2["ID_CARD"] = "ID_CARD"; DocumentType2["AUTHORIZATION_LETTER"] = "AUTHORIZATION_LETTER"; DocumentType2["OTHER"] = "OTHER"; return DocumentType2; })(DocumentType || {}); var DocumentStatus = /* @__PURE__ */ ((DocumentStatus2) => { DocumentStatus2["UPLOADED"] = "UPLOADED"; DocumentStatus2["VERIFIED"] = "VERIFIED"; DocumentStatus2["REJECTED"] = "REJECTED"; return DocumentStatus2; })(DocumentStatus || {}); var ChannelCreateRequestSchema = import_zod.z.object({ name: import_zod.z.string().min(1).max(100), type: import_zod.z.nativeEnum(ChannelType), provider: import_zod.z.string().min(1), profileKey: import_zod.z.string().min(1), businessInfo: import_zod.z.object({ name: import_zod.z.string().min(1), registrationNumber: import_zod.z.string().min(1), category: import_zod.z.string().min(1), contactPerson: import_zod.z.string().min(1), contactEmail: import_zod.z.string().email(), contactPhone: import_zod.z.string().regex(/^[0-9-+\s()]+$/) }).optional(), kakaoInfo: import_zod.z.object({ plusFriendId: import_zod.z.string().min(1), brandName: import_zod.z.string().min(1), logoUrl: import_zod.z.string().url().optional(), description: import_zod.z.string().max(500).optional() }).optional() }); var SenderNumberCreateRequestSchema = import_zod.z.object({ phoneNumber: import_zod.z.string().regex(/^[0-9]{10,11}$/), category: import_zod.z.nativeEnum(SenderNumberCategory), businessInfo: import_zod.z.object({ businessName: import_zod.z.string().min(1), businessRegistrationNumber: import_zod.z.string().min(1), contactPerson: import_zod.z.string().min(1), contactEmail: import_zod.z.string().email() }).optional() }); var ChannelFiltersSchema = import_zod.z.object({ provider: import_zod.z.string().optional(), type: import_zod.z.nativeEnum(ChannelType).optional(), status: import_zod.z.nativeEnum(ChannelStatus).optional(), verified: import_zod.z.boolean().optional(), createdAfter: import_zod.z.date().optional(), createdBefore: import_zod.z.date().optional() }); var SenderNumberFiltersSchema = import_zod.z.object({ channelId: import_zod.z.string().optional(), status: import_zod.z.nativeEnum(SenderNumberStatus).optional(), category: import_zod.z.nativeEnum(SenderNumberCategory).optional(), verified: import_zod.z.boolean().optional() }); // src/kakao/channel.ts var KakaoChannelManager = class { constructor() { this.channels = /* @__PURE__ */ new Map(); } async createChannel(request) { this.validateKakaoChannelRequest(request); const channelId = this.generateChannelId(); const channel = { id: channelId, name: request.name, provider: request.provider, type: request.type, status: "PENDING" /* PENDING */, profileKey: request.profileKey, senderNumbers: [], metadata: { businessInfo: request.businessInfo, kakaoInfo: request.kakaoInfo, limits: { dailyMessageLimit: 1e4, monthlyMessageLimit: 3e5, rateLimit: 10 // 10 messages per second }, features: { supportsBulkSending: true, supportsScheduling: true, supportsButtons: true, maxButtonCount: 5 } }, verification: { status: request.businessInfo ? "PENDING" /* PENDING */ : "NOT_REQUIRED" /* NOT_REQUIRED */, documents: [] }, createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }; this.channels.set(channelId, channel); if (request.businessInfo) { await this.initiateBusinessVerification(channel); } return channel; } validateKakaoChannelRequest(request) { if (request.type !== "KAKAO_ALIMTALK" /* KAKAO_ALIMTALK */ && request.type !== "KAKAO_FRIENDTALK" /* KAKAO_FRIENDTALK */) { throw new Error("Invalid channel type for Kakao channel"); } if (!request.kakaoInfo?.plusFriendId) { throw new Error("Plus Friend ID is required for Kakao channels"); } if (!request.kakaoInfo?.brandName) { throw new Error("Brand name is required for Kakao channels"); } if (!this.isValidPlusFriendId(request.kakaoInfo.plusFriendId)) { throw new Error("Invalid Plus Friend ID format"); } } isValidPlusFriendId(plusFriendId) { const regex = /^@[a-zA-Z0-9_-]{3,30}$/; return regex.test(plusFriendId); } async initiateBusinessVerification(channel) { channel.verification.status = "UNDER_REVIEW" /* UNDER_REVIEW */; channel.status = "VERIFYING" /* VERIFYING */; channel.updatedAt = /* @__PURE__ */ new Date(); setTimeout(() => { this.completeVerification(channel.id, true); }, 5e3); } async completeVerification(channelId, approved, rejectionReason) { const channel = this.channels.get(channelId); if (!channel) { throw new Error("Channel not found"); } if (approved) { channel.verification.status = "VERIFIED" /* VERIFIED */; channel.verification.verifiedAt = /* @__PURE__ */ new Date(); channel.status = "ACTIVE" /* ACTIVE */; } else { channel.verification.status = "REJECTED" /* REJECTED */; channel.verification.rejectedAt = /* @__PURE__ */ new Date(); channel.verification.rejectionReason = rejectionReason || "Verification failed"; channel.status = "SUSPENDED" /* SUSPENDED */; } channel.updatedAt = /* @__PURE__ */ new Date(); } async getChannel(channelId) { return this.channels.get(channelId) || null; } async updateChannel(channelId, updates) { const channel = this.channels.get(channelId); if (!channel) { throw new Error("Channel not found"); } if (updates.metadata?.kakaoInfo?.plusFriendId && !this.isValidPlusFriendId(updates.metadata.kakaoInfo.plusFriendId)) { throw new Error("Invalid Plus Friend ID format"); } Object.assign(channel, updates, { updatedAt: /* @__PURE__ */ new Date() }); return channel; } async deleteChannel(channelId) { const channel = this.channels.get(channelId); if (!channel) { return false; } channel.status = "DELETED" /* DELETED */; channel.updatedAt = /* @__PURE__ */ new Date(); return true; } async listChannels(filters) { let channels = Array.from(this.channels.values()); if (filters) { if (filters.status) { channels = channels.filter((c) => c.status === filters.status); } if (filters.type) { channels = channels.filter((c) => c.type === filters.type); } if (filters.verified !== void 0) { const verifiedStatus = filters.verified ? "VERIFIED" /* VERIFIED */ : "PENDING" /* PENDING */; channels = channels.filter((c) => c.verification.status === verifiedStatus); } } return channels.filter((c) => c.status !== "DELETED" /* DELETED */); } async suspendChannel(channelId, reason) { const channel = this.channels.get(channelId); if (!channel) { throw new Error("Channel not found"); } channel.status = "SUSPENDED" /* SUSPENDED */; channel.updatedAt = /* @__PURE__ */ new Date(); console.log(`Channel ${channelId} suspended: ${reason}`); } async reactivateChannel(channelId) { const channel = this.channels.get(channelId); if (!channel) { throw new Error("Channel not found"); } if (channel.verification.status !== "VERIFIED" /* VERIFIED */) { throw new Error("Channel must be verified before reactivation"); } channel.status = "ACTIVE" /* ACTIVE */; channel.updatedAt = /* @__PURE__ */ new Date(); } async checkChannelHealth(channelId) { const channel = this.channels.get(channelId); if (!channel) { throw new Error("Channel not found"); } const issues = []; const recommendations = []; if (channel.status !== "ACTIVE" /* ACTIVE */) { issues.push(`Channel status is ${channel.status}`); } if (channel.verification.status !== "VERIFIED" /* VERIFIED */ && channel.verification.status !== "NOT_REQUIRED" /* NOT_REQUIRED */) { issues.push(`Channel verification is ${channel.verification.status}`); } if (channel.senderNumbers.length === 0) { recommendations.push("Add at least one verified sender number"); } if (!channel.metadata.businessInfo) { recommendations.push("Complete business information for better deliverability"); } return { isHealthy: issues.length === 0, issues, recommendations }; } generateChannelId() { return `kakao_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } }; // src/kakao/sender-number.ts var KakaoSenderNumberManager = class { constructor() { this.senderNumbers = /* @__PURE__ */ new Map(); this.verificationCodes = /* @__PURE__ */ new Map(); } async addSenderNumber(channelId, request) { this.validatePhoneNumber(request.phoneNumber); const existingNumber = this.findSenderNumberByPhone(request.phoneNumber); if (existingNumber) { throw new Error("Phone number is already registered"); } const senderNumberId = this.generateSenderNumberId(); const senderNumber = { id: senderNumberId, phoneNumber: request.phoneNumber, status: "PENDING" /* PENDING */, category: request.category, metadata: { businessName: request.businessInfo?.businessName, businessRegistrationNumber: request.businessInfo?.businessRegistrationNumber, contactPerson: request.businessInfo?.contactPerson, contactEmail: request.businessInfo?.contactEmail }, createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }; this.senderNumbers.set(senderNumberId, senderNumber); await this.initiateVerification(senderNumber); return senderNumber; } validatePhoneNumber(phoneNumber) { const regex = /^(010|011|016|017|018|019)[0-9]{7,8}$/; if (!regex.test(phoneNumber)) { throw new Error("Invalid Korean phone number format"); } } findSenderNumberByPhone(phoneNumber) { return Array.from(this.senderNumbers.values()).find((sn) => sn.phoneNumber === phoneNumber); } async initiateVerification(senderNumber) { const verificationCode = this.generateVerificationCode(); const expiresAt = new Date(Date.now() + 5 * 60 * 1e3); this.verificationCodes.set(senderNumber.id, { code: verificationCode, expiresAt }); senderNumber.status = "VERIFYING" /* VERIFYING */; senderNumber.verificationCode = verificationCode; senderNumber.updatedAt = /* @__PURE__ */ new Date(); console.log(`Verification code for ${senderNumber.phoneNumber}: ${verificationCode}`); await this.sendVerificationSMS(senderNumber.phoneNumber, verificationCode); } async sendVerificationSMS(phoneNumber, code) { console.log(`Sending SMS to ${phoneNumber}: Your verification code is ${code}`); } async verifySenderNumber(senderNumberId, code) { const senderNumber = this.senderNumbers.get(senderNumberId); if (!senderNumber) { throw new Error("Sender number not found"); } const verification = this.verificationCodes.get(senderNumberId); if (!verification) { throw new Error("No verification code found"); } if (/* @__PURE__ */ new Date() > verification.expiresAt) { throw new Error("Verification code has expired"); } if (verification.code !== code) { return false; } senderNumber.status = "VERIFIED" /* VERIFIED */; senderNumber.verifiedAt = /* @__PURE__ */ new Date(); senderNumber.updatedAt = /* @__PURE__ */ new Date(); delete senderNumber.verificationCode; this.verificationCodes.delete(senderNumberId); return true; } async resendVerificationCode(senderNumberId) { const senderNumber = this.senderNumbers.get(senderNumberId); if (!senderNumber) { throw new Error("Sender number not found"); } if (senderNumber.status !== "VERIFYING" /* VERIFYING */) { throw new Error("Sender number is not in verifying status"); } const lastVerification = this.verificationCodes.get(senderNumberId); if (lastVerification) { const timeSinceLastCode = Date.now() - (lastVerification.expiresAt.getTime() - 5 * 60 * 1e3); if (timeSinceLastCode < 60 * 1e3) { throw new Error("Please wait before requesting a new verification code"); } } await this.initiateVerification(senderNumber); } async getSenderNumber(senderNumberId) { return this.senderNumbers.get(senderNumberId) || null; } async listSenderNumbers(filters) { let senderNumbers = Array.from(this.senderNumbers.values()); if (filters) { if (filters.status) { senderNumbers = senderNumbers.filter((sn) => sn.status === filters.status); } if (filters.category) { senderNumbers = senderNumbers.filter((sn) => sn.category === filters.category); } if (filters.verified !== void 0) { if (filters.verified) { senderNumbers = senderNumbers.filter((sn) => sn.status === "VERIFIED" /* VERIFIED */); } else { senderNumbers = senderNumbers.filter((sn) => sn.status !== "VERIFIED" /* VERIFIED */); } } } return senderNumbers; } async updateSenderNumber(senderNumberId, updates) { const senderNumber = this.senderNumbers.get(senderNumberId); if (!senderNumber) { throw new Error("Sender number not found"); } const allowedUpdates = { ...updates }; delete allowedUpdates.id; delete allowedUpdates.phoneNumber; delete allowedUpdates.verifiedAt; delete allowedUpdates.createdAt; Object.assign(senderNumber, allowedUpdates, { updatedAt: /* @__PURE__ */ new Date() }); return senderNumber; } async deleteSenderNumber(senderNumberId) { const senderNumber = this.senderNumbers.get(senderNumberId); if (!senderNumber) { return false; } if (await this.isSenderNumberInUse(senderNumberId)) { throw new Error("Cannot delete sender number that is currently in use"); } this.senderNumbers.delete(senderNumberId); this.verificationCodes.delete(senderNumberId); return true; } async isSenderNumberInUse(senderNumberId) { return false; } async blockSenderNumber(senderNumberId, reason) { const senderNumber = this.senderNumbers.get(senderNumberId); if (!senderNumber) { throw new Error("Sender number not found"); } senderNumber.status = "BLOCKED" /* BLOCKED */; senderNumber.updatedAt = /* @__PURE__ */ new Date(); console.log(`Sender number ${senderNumberId} blocked: ${reason}`); } async unblockSenderNumber(senderNumberId) { const senderNumber = this.senderNumbers.get(senderNumberId); if (!senderNumber) { throw new Error("Sender number not found"); } if (senderNumber.status !== "BLOCKED" /* BLOCKED */) { throw new Error("Sender number is not blocked"); } senderNumber.status = senderNumber.verifiedAt ? "VERIFIED" /* VERIFIED */ : "PENDING" /* PENDING */; senderNumber.updatedAt = /* @__PURE__ */ new Date(); } async validateSenderNumberForSending(senderNumberId) { const senderNumber = this.senderNumbers.get(senderNumberId); const errors = []; if (!senderNumber) { errors.push("Sender number not found"); return { isValid: false, errors }; } if (senderNumber.status !== "VERIFIED" /* VERIFIED */) { errors.push(`Sender number status is ${senderNumber.status}, must be verified`); } if (!senderNumber.verifiedAt) { errors.push("Sender number has not been verified"); } if (senderNumber.verifiedAt) { const oneYearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1e3); if (senderNumber.verifiedAt < oneYearAgo) { errors.push("Sender number verification has expired"); } } return { isValid: errors.length === 0, errors }; } generateSenderNumberId() { return `sn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } generateVerificationCode() { return Math.floor(1e5 + Math.random() * 9e5).toString(); } // Cleanup expired verification codes cleanup() { const now = /* @__PURE__ */ new Date(); for (const [id, verification] of this.verificationCodes) { if (now > verification.expiresAt) { this.verificationCodes.delete(id); const senderNumber = this.senderNumbers.get(id); if (senderNumber && senderNumber.status === "VERIFYING" /* VERIFYING */) { senderNumber.status = "PENDING" /* PENDING */; delete senderNumber.verificationCode; senderNumber.updatedAt = /* @__PURE__ */ new Date(); } } } } }; // src/management/crud.ts var import_events = require("events"); var ChannelCRUD = class extends import_events.EventEmitter { constructor(options = {}) { super(); this.options = options; this.channels = /* @__PURE__ */ new Map(); this.senderNumbers = /* @__PURE__ */ new Map(); this.auditLogs = []; this.defaultOptions = { enableAuditLog: true, enableEventEmission: true, defaultPageSize: 20, maxPageSize: 100, enableSoftDelete: true, autoCleanup: true, cleanupInterval: 36e5 // 1 hour }; this.options = { ...this.defaultOptions, ...options }; if (this.options.autoCleanup) { this.startAutoCleanup(); } } // Channel CRUD Operations async createChannel(request, userId) { const channelId = this.generateChannelId(); const channel = { id: channelId, name: request.name, provider: request.provider, type: request.type, status: "PENDING" /* PENDING */, profileKey: request.profileKey, senderNumbers: [], metadata: { businessInfo: request.businessInfo, kakaoInfo: request.kakaoInfo, limits: this.getDefaultLimits(request.type), features: this.getDefaultFeatures(request.type) }, verification: { status: request.businessInfo ? "PENDING" /* PENDING */ : "NOT_REQUIRED" /* NOT_REQUIRED */, documents: [] }, createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }; this.channels.set(channelId, channel); if (this.options.enableAuditLog) { this.addAuditLog("channel", channelId, "create", userId, void 0, channel); } if (this.options.enableEventEmission) { this.emit("channel:created", { channel, userId }); } return channel; } async getChannel(channelId, userId) { const channel = this.channels.get(channelId); if (channel && this.options.enableAuditLog) { this.addAuditLog("channel", channelId, "read", userId); } return channel || null; } async updateChannel(channelId, updates, userId) { const channel = this.channels.get(channelId); if (!channel) { throw new Error(`Channel ${channelId} not found`); } const before = this.options.enableAuditLog ? { ...channel } : void 0; const updatedChannel = { ...channel, ...updates, id: channelId, // Ensure ID doesn't change updatedAt: /* @__PURE__ */ new Date() }; this.channels.set(channelId, updatedChannel); if (this.options.enableAuditLog) { this.addAuditLog("channel", channelId, "update", userId, before, updatedChannel); } if (this.options.enableEventEmission) { this.emit("channel:updated", { channel: updatedChannel, previousChannel: channel, userId }); } return updatedChannel; } async deleteChannel(channelId, userId) { const channel = this.channels.get(channelId); if (!channel) { return false; } if (this.options.enableSoftDelete) { channel.status = "DELETED" /* DELETED */; channel.updatedAt = /* @__PURE__ */ new Date(); } else { this.channels.delete(channelId); for (const [id, senderNumber] of this.senderNumbers) { } } if (this.options.enableAuditLog) { this.addAuditLog("channel", channelId, "delete", userId, channel); } if (this.options.enableEventEmission) { this.emit("channel:deleted", { channel, userId }); } return true; } async listChannels(filters = {}, pagination = { page: 1, limit: this.options.defaultPageSize }) { let channels = Array.from(this.channels.values()); if (filters.provider) { channels = channels.filter((c) => c.provider === filters.provider); } if (filters.type) { channels = channels.filter((c) => c.type === filters.type); } if (filters.status) { channels = channels.filter((c) => c.status === filters.status); } if (filters.verified !== void 0) { const targetStatus = filters.verified ? "VERIFIED" /* VERIFIED */ : "PENDING" /* PENDING */; channels = channels.filter((c) => c.verification.status === targetStatus); } if (filters.createdAfter) { channels = channels.filter((c) => c.createdAt >= filters.createdAfter); } if (filters.createdBefore) { channels = channels.filter((c) => c.createdAt <= filters.createdBefore); } if (!filters.status || filters.status !== "DELETED" /* DELETED */) { channels = channels.filter((c) => c.status !== "DELETED" /* DELETED */); } const sortBy = pagination.sortBy || "createdAt"; const sortOrder = pagination.sortOrder || "desc"; channels.sort((a, b) => { let aValue, bValue; switch (sortBy) { case "name": aValue = a.name; bValue = b.name; break; case "createdAt": aValue = a.createdAt.getTime(); bValue = b.createdAt.getTime(); break; case "updatedAt": aValue = a.updatedAt.getTime(); bValue = b.updatedAt.getTime(); break; default: aValue = a.createdAt.getTime(); bValue = b.createdAt.getTime(); } if (sortOrder === "asc") { return aValue < bValue ? -1 : aValue > bValue ? 1 : 0; } else { return aValue > bValue ? -1 : aValue < bValue ? 1 : 0; } }); const total = channels.length; const limit = Math.min(pagination.limit, this.options.maxPageSize); const page = Math.max(1, pagination.page); const offset = (page - 1) * limit; const paginatedChannels = channels.slice(offset, offset + limit); return { data: paginatedChannels, total, page, limit, totalPages: Math.ceil(total / limit), hasNext: offset + limit < total, hasPrev: page > 1 }; } // Sender Number CRUD Operations async createSenderNumber(channelId, request, userId) { const channel = this.channels.get(channelId); if (!channel) { throw new Error(`Channel ${channelId} not found`); } const senderNumberId = this.generateSenderNumberId(); const senderNumber = { id: senderNumberId, phoneNumber: request.phoneNumber, status: "PENDING" /* PENDING */, category: request.category, metadata: { businessName: request.businessInfo?.businessName, businessRegistrationNumber: request.businessInfo?.businessRegistrationNumber, contactPerson: request.businessInfo?.contactPerson, contactEmail: request.businessInfo?.contactEmail }, createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }; this.senderNumbers.set(senderNumberId, senderNumber); channel.senderNumbers.push(senderNumber); channel.updatedAt = /* @__PURE__ */ new Date(); if (this.options.enableAuditLog) { this.addAuditLog("senderNumber", senderNumberId, "create", userId, void 0, senderNumber); } if (this.options.enableEventEmission) { this.emit("senderNumber:created", { senderNumber, channelId, userId }); } return senderNumber; } async getSenderNumber(senderNumberId, userId) { const senderNumber = this.senderNumbers.get(senderNumberId); if (senderNumber && this.options.enableAuditLog) { this.addAuditLog("senderNumber", senderNumberId, "read", userId); } return senderNumber || null; } async updateSenderNumber(senderNumberId, updates, userId) { const senderNumber = this.senderNumbers.get(senderNumberId); if (!senderNumber) { throw new Error(`Sender number ${senderNumberId} not found`); } const before = this.options.enableAuditLog ? { ...senderNumber } : void 0; const updatedSenderNumber = { ...senderNumber, ...updates, id: senderNumberId, // Ensure ID doesn't change updatedAt: /* @__PURE__ */ new Date() }; this.senderNumbers.set(senderNumberId, updatedSenderNumber); for (const channel of this.channels.values()) { const index = channel.senderNumbers.findIndex((sn) => sn.id === senderNumberId); if (index !== -1) { channel.senderNumbers[index] = updatedSenderNumber; channel.updatedAt = /* @__PURE__ */ new Date(); break; } } if (this.options.enableAuditLog) { this.addAuditLog("senderNumber", senderNumberId, "update", userId, before, updatedSenderNumber); } if (this.options.enableEventEmission) { this.emit("senderNumber:updated", { senderNumber: updatedSenderNumber, previousSenderNumber: senderNumber, userId }); } return updatedSenderNumber; } async deleteSenderNumber(senderNumberId, userId) { const senderNumber = this.senderNumbers.get(senderNumberId); if (!senderNumber) { return false; } this.senderNumbers.delete(senderNumberId); for (const channel of this.channels.values()) { const index = channel.senderNumbers.findIndex((sn) => sn.id === senderNumberId); if (index !== -1) { channel.senderNumbers.splice(index, 1); channel.updatedAt = /* @__PURE__ */ new Date(); break; } } if (this.options.enableAuditLog) { this.addAuditLog("senderNumber", senderNumberId, "delete", userId, senderNumber); } if (this.options.enableEventEmission) { this.emit("senderNumber:deleted", { senderNumber, userId }); } return true; } async listSenderNumbers(filters = {}, pagination = { page: 1, limit: this.options.defaultPageSize }) { let senderNumbers = Array.from(this.senderNumbers.values()); if (filters.channelId) { const channel = this.channels.get(filters.channelId); if (channel) { senderNumbers = channel.senderNumbers; } else { senderNumbers = []; } } if (filters.status) { senderNumbers = senderNumbers.filter((sn) => sn.status === filters.status); } if (filters.category) { senderNumbers = senderNumbers.filter((sn) => sn.category === filters.category); } if (filters.verified !== void 0) { if (filters.verified) { senderNumbers = senderNumbers.filter((sn) => sn.status === "VERIFIED" /* VERIFIED */); } else { senderNumbers = senderNumbers.filter((sn) => sn.status !== "VERIFIED" /* VERIFIED */); } } const sortBy = pagination.sortBy || "createdAt"; const sortOrder = pagination.sortOrder || "desc"; senderNumbers.sort((a, b) => { let aValue, bValue; switch (sortBy) { case "phoneNumber": aValue = a.phoneNumber; bValue = b.phoneNumber; break; case "createdAt": aValue = a.createdAt.getTime(); bValue = b.createdAt.getTime(); break; case "updatedAt": aValue = a.updatedAt.getTime(); bValue = b.updatedAt.getTime(); break; default: aValue = a.createdAt.getTime(); bValue = b.createdAt.getTime(); } if (sortOrder === "asc") { return aValue < bValue ? -1 : aValue > bValue ? 1 : 0; } else { return aValue > bValue ? -1 : aValue < bValue ? 1 : 0; } }); const total = senderNumbers.length; const limit = Math.min(pagination.limit, this.options.maxPageSize); const page = Math.max(1, pagination.page); const offset = (page - 1) * limit; const paginatedSenderNumbers = senderNumbers.slice(offset, offset + limit); return { data: paginatedSenderNumbers, total, page, limit, totalPages: Math.ceil(total / limit), hasNext: offset + limit < total, hasPrev: page > 1 }; } // Audit and Analytics getAuditLogs(entityType, entityId, limit = 100) { let logs = [...this.auditLogs]; if (entityType) { logs = logs.filter((log) => log.entityType === entityType); } if (entityId) { logs = logs.filter((log) => log.entityId === entityId); } return logs.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()).slice(0, limit); } getStatistics() { const channels = Array.from(this.channels.values()); const senderNumbers = Array.from(this.senderNumbers.values()); const channelsByStatus = {}; const channelsByType = {}; const channelsByProvider = {}; channels.forEach((channel) => { channelsByStatus[channel.status] = (channelsByStatus[channel.status] || 0) + 1; channelsByType[channel.type] = (channelsByType[channel.type] || 0) + 1; channelsByProvider[channel.provider] = (channelsByProvider[channel.provider] || 0) + 1; }); const senderNumbersByStatus = {}; const senderNumbersByCategory = {}; senderNumbers.forEach((senderNumber) => { senderNumbersByStatus[senderNumber.status] = (senderNumbersByStatus[senderNumber.status] || 0) + 1; senderNumbersByCategory[senderNumber.category] = (senderNumbersByCategory[senderNumber.category] || 0) + 1; }); return { channels: { total: channels.length, byStatus: channelsByStatus, byType: channelsByType, byProvider: channelsByProvider }, senderNumbers: { total: senderNumbers.length, byStatus: senderNumbersByStatus, byCategory: senderNumbersByCategory } }; } // Cleanup and Maintenance cleanup() { let deletedChannels = 0; let expiredAuditLogs = 0; const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3); for (const [id, channel] of this.channels) { if (channel.status === "DELETED" /* DELETED */ && channel.updatedAt < thirtyDaysAgo) { this.channels.delete(id); deletedChannels++; } } const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3); const originalLogCount = this.auditLogs.length; this.auditLogs = this.auditLogs.filter((log) => log.timestamp >= ninetyDaysAgo); expiredAuditLogs = originalLogCount - this.auditLogs.length; return { deletedChannels, expiredAuditLogs }; } destroy() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = void 0; } this.removeAllListeners(); this.channels.clear(); this.senderNumbers.clear(); this.auditLogs = []; } addAuditLog(entityType, entityId, action, userId, before, after) { const auditLog = { id: this.generateAuditLogId(), entityType, entityId, action, userId, timestamp: /* @__PURE__ */ new Date(), changes: before || after ? { before, after } : void 0 }; this.auditLogs.push(auditLog); if (this.auditLogs.length > 1e4) { this.auditLogs = this.auditLogs.slice(-1e4); } } getDefaultLimits(channelType) { switch (channelType) { case "KAKAO_ALIMTALK" /* KAKAO_ALIMTALK */: return { dailyMessageLimit: 1e4, monthlyMessageLimit: 3e5, rateLimit: 10 }; case "KAKAO_FRIENDTALK" /* KAKAO_FRIENDTALK */: return { dailyMessageLimit: 1e3, monthlyMessageLimit: 3e4, rateLimit: 5 }; case "SMS" /* SMS */: case "LMS" /* LMS */: case "MMS" /* MMS */: return { dailyMessageLimit: 1e3, monthlyMessageLimit: 3e4, rateLimit: 3 }; default: return { dailyMessageLimit: 1e3, monthlyMessageLimit: 3e4, rateLimit: 1 }; } } getDefaultFeatures(channelType) { switch (channelType) { case "KAKAO_ALIMTALK" /* KAKAO_ALIMTALK */: return { supportsBulkSending: true, supportsScheduling: true, supportsButtons: true, maxButtonCount: 5 }; case "KAKAO_FRIENDTALK" /* KAKAO_FRIENDTALK */: return { supportsBulkSending: true, supportsScheduling: true, supportsButtons: false, maxButtonCount: 0 }; default: return { supportsBulkSending: false, supportsScheduling: false, supportsButtons: false, maxButtonCount: 0 }; } } startAutoCleanup() { this.cleanupTimer = setInterval(() => { this.cleanup(); }, this.options.cleanupInterval); } generateChannelId() { return `ch_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } generateSenderNumberId() { return `sn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } generateAuditLogId() { return `audit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } }; // src/management/permissions.ts var import_events2 = require("events"); var ResourceType = /* @__PURE__ */ ((ResourceType2) => { ResourceType2["CHANNEL"] = "channel"; ResourceType2["SENDER_NUMBER"] = "senderNumber"; ResourceType2["TEMPLATE"] = "template"; ResourceType2["MESSAGE"] = "message"; ResourceType2["USER"] = "user"; ResourceType2["ROLE"] = "role"; ResourceType2["AUDIT_LOG"] = "auditLog"; ResourceType2["ANALYTICS"] = "analytics"; return ResourceType2; })(ResourceType || {}); var ActionType = /* @__PURE__ */ ((ActionType2) => { ActionType2["CREATE"] = "create"; ActionType2["READ"] = "read"; ActionType2["UPDATE"] = "update"; ActionType2["DELETE"] = "delete"; ActionType2["VERIFY"] = "verify"; ActionType2["SUSPEND"] = "suspend"; ActionType2["ACTIVATE"] = "activate"; ActionType2["SEND"] = "send"; ActionType2["MANAGE"] = "manage"; return ActionType2; })(ActionType || {}); var PermissionScope = /* @__PURE__ */ ((PermissionScope2) => { PermissionScope2["GLOBAL"] = "global"; PermissionScope2["ORGANIZATION"] = "organization"; PermissionScope2["TEAM"] = "team"; PermissionScope2["PERSONAL"] = "personal"; return PermissionScope2; })(PermissionScope || {}); var PermissionManager = class extends import_events2.EventEmitter { // 5 minutes constructor() { super(); this.users = /* @__PURE__ */ new Map(); this.roles = /* @__PURE__ */ new Map(); this.userRoleCache = /* @__PURE__ */ new Map(); this.permissionCache = /* @__PURE__ */ new Map(); this.cacheExpiry = /* @__PURE__ */ new Map(); this.CACHE_DURATION = 5 * 60 * 1e3; this.initializeSystemRoles(); } // User Management async createUser(userData) { const userId = this.generateUserId(); const user = { ...userData, id: userId, createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }; this.users.set(userId, user); this.updateUserRoleCache(userId, user.roles.map((r) => r.id)); this.emit("user:created", { user }); return user; } async getUser(userId) { return this.users.get(userId) || null; } async updateUser(userId, updates) { const user = this.users.get(userId); if (!user) { throw new Error("User not found"); } const updatedUser = { ...user, ...updates, id: userId, updatedAt: /* @__PURE__ */ new Date() }; this.users.set(userId, updatedUser); if (updates.roles) { this.updateUserRoleCache(userId, updates.roles.map((r) => r.id)); } this.clearUserPermissionCache(userId); this.emit("user:updated", { user: updatedUser, previousUser: user }); return updatedUser; } async deleteUser(userId) { const user = this.users.get(userId); if (!user) { return false; } this.users.delete(userId); this.userRoleCache.delete(userId); this.clearUserPermissionCache(userId); this.emit("user:deleted", { user }); return true; } // Role Management async createRole(roleData) { const roleId = this.generateRoleId(); const role = { ...roleData, id: roleId, createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }; this.roles.set(roleId, role); this.emit("role:created", { role }); return role; } async getRole(roleId) { return this.roles.get(roleId) || null; } async updateRole(roleId, updates) { const role = this.roles.get(roleId); if (!role) { throw new Error("Role not found"); } if (role.isSystem && updates.permissions) { throw new Error("Cannot modify permissions of system roles"); } const updatedRole = { ...role, ...updates, id: roleId, updatedAt: /* @__PURE__ */ new Date() }; this.roles.set(roleId, updatedRole); this.clearRolePermissionCache(roleId); this.emit("role:updated", { role: updatedRole, previousRole: role }); return updatedRole; } async deleteRole(roleId) { const role = this.roles.get(roleId); if (!role) { return false; } if (role.isSystem) { throw new Error("Cannot delete system roles"); } const usersWithRole = Array.from(this.users.values()).filter((user) => user.roles.some((r) => r.id === roleId)); if (usersWithRole.length > 0) { throw new Error("Cannot delete role that is assigned to users"); } this.roles.delete(roleId); this.emit("role:deleted", { role }); return true; } // Permission Management async assignRoleToUser(userId, roleId) { const user = this.users.get(userId); const role = this.roles.get(roleId); if (!user) { throw new Error("User not found"); } if (!role) { throw new Error("Role not found"); } if (user.roles.some((r) => r.id === roleId)) { return; } user.roles.push(role); user.updatedAt = /* @__PURE__ */ new Date(); this.updateUserRoleCache(userId, user.roles.map((r) => r.id)); this.clearUserPermissionCache(userId); this.emit("role:assigned", { userId, roleId }); } async removeRoleFromUser(userId, roleId) { const user = this.users.get(userId); if (!user) { throw new Error("User not found"); } const roleIndex = user.roles.findIndex((r) => r.id === roleId); if (roleIndex === -1) { return; } user.roles.splice(roleIndex, 1); user.updatedAt = /* @__PURE__ */ new Date(); this.updateUserRoleCache(userId, user.roles.map((r) => r.id)); this.clearUserPermissionCache(userId); this.emit("role:removed", { userId, roleId }); } // Permission Checking async checkPermission(check) { const cacheKey = this.getCacheKey(check); const cached = this.getFromCache(cacheKey); if (cached) { return cached; } const result = await this.performPermissionCheck(check); this.setCache(cacheKey, result); return result; } async hasPermission(userId, resource, action, resourceId, context) { const result = await this.checkPermission({ userId, resource, action, resourceId, context }); return result.granted; } async requirePermission(userId, resource, action, resourceId, context) { const hasAccess = await this.hasPermission(userId, resource, action, resourceId, context); if (!hasAccess) { throw new Error(`Access denied: ${action} on ${resource}`); } } // Utility Methods async getUserPermissions(userId) { const user = this.users.get(userId); if (!user) { return []; } const permissions = []; for (const role of user.roles) { permissions.push(...role.permissions); } const uniquePermissions = permissions.filter( (permission, index, self) => index === self.findIndex((p) => p.id === permission.id) ); return uniquePermissions; } async getUserRoles(userId) { const user = this.users.get(userId); return user ? user.roles : []; } listUsers(filters) { let users = Array.from(this.users.values()); if (filters?.isActive !== void 0) { users = users.filter((u) => u.isActive === filters.isActive); } if (filters?.roleId) { users = users.filter((u) => u.roles.some((r) => r.id === filters.roleId)); } return users; } listRoles() { return Array.from(this.roles.values()); } // Private Methods async performPermissionCheck(check) { const user = this.users.get(check.userId); if (!user) { return { granted: false, reason: "User not found", matchedPermissions: [], deniedReasons: ["User not found"] }; } if (!user.isActive) { return { granted: false, reason: "User is inactive", matchedPermissions: [], deniedReasons: ["User is inactive"] }; } const matchedPermissions = []; const deniedReasons = []; for (const role of user.roles) { for (const permission of role.permissions) { if (this.doesPermissionMatch(permission, check)) { if (await this.checkConditions(permission, check)) { matchedPermissions.push(permission); } else { deniedReasons.push(`Conditions not met for permission ${permission.id}`); } } } } const granted = matchedPermissions.length > 0; return { granted, reason: granted ? void 0 : "No matching permissions found", matchedPermissions, deniedReasons: granted ? [] : deniedReasons }; } doesPermissionMatch(permission, check) { return permission.resource === check.resource && permission.action === check.action; } async checkConditions(permission, check) { if (!permission.conditions || permission.conditions.length === 0) { return true; } for (const condition of permission.conditions) { if (!await this.evaluateCondition(condition, check)) { return false; } } return true; } async evaluateCondition(condition, check) { let actualValue; switch (condition.field) { case "userId": actualValue = check.userId; break; case "organizationId": actualValue = check.context?.organizationId; break; case "teamId": actualValue = check.context?.teamId; break; case "resourceOwnerId": actualValue = check.context?.resourceOwnerId; break; default: actualValue = check.context?.metadata?.[condition.field]; } switch (condition.operator) { case "equals": return actualValue === condition.value; case "not_equals": return actualValue !== condition.value; case "in": return Array.isArray(condition.value) && condition.value.includes(actualValue); case "not_in": return Array.isArray(condition.value) && !condition.value.includes(actualValue); case "contains": return String(actualValue).includes(String(condition.value)); case "starts_with": return String(actualValue).startsWith(String(condition.value)); default: return false; } } initializeSystemRoles() { const superAdminRole = { id: "super-admin", name: "Super Admin", description: "Full system access", isSystem: true, permissions: [ // Global permissions for all resources and actions ...Object.values(ResourceType).flatMap( (resource) => Object.values(ActionType).map((action) => ({ id: `super-admin-${resource}-${action}`, resource, action, scope: "global" /* GLOBAL */ })) ) ], createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }; const channelAdminRole = { id: "channel-admin", name: "Channel Admin", description: "Manage channels and sender numbers", isSystem: true, permissions: [ { id: "channel-admin-channel-manage", resource: "channel" /* CHANNEL */, action: "manage" /* MANAGE */, scope: "organization" /* ORGANIZATION */ }, { id: "channel-admin-sender-manage", resource: "senderNumber" /* SENDER_NUMBER */, action: "manage" /* MANAGE */, scope: "organization" /* ORGANIZATION */ } ], createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }; const messageSenderRole = { id: "message-sender", name: "Message Sender", description: "Send messages using configured channels", isSystem: true, permissions: [ { id: "message-sender-channel-read", resource: "channel" /* CHANNEL */, action: "read" /* READ */, scope: "organization" /* ORGANIZATION */ }, { id: "message-sender-message-send", resource: "message" /* MESSAGE */, action: "send" /* SEND */, scope: "organization" /* ORGANIZATION */ } ], createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }; const viewerRole = { id: "viewer", name: "Viewer", description: "Read-only access", isSystem: true, permissions: [ { id: "viewer-channel-read", resource: "channel" /* CHANNEL */, action: "read" /* READ */, scope: "organization" /* ORGANIZATION */