@k-msg/channel
Version:
AlimTalk channel and sender number management
1,457 lines (1,452 loc) • 83.7 kB
JavaScript
// src/types/channel.types.ts
import { z } from "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 = z.object({
name: z.string().min(1).max(100),
type: z.nativeEnum(ChannelType),
provider: z.string().min(1),
profileKey: z.string().min(1),
businessInfo: z.object({
name: z.string().min(1),
registrationNumber: z.string().min(1),
category: z.string().min(1),
contactPerson: z.string().min(1),
contactEmail: z.string().email(),
contactPhone: z.string().regex(/^[0-9-+\s()]+$/)
}).optional(),
kakaoInfo: z.object({
plusFriendId: z.string().min(1),
brandName: z.string().min(1),
logoUrl: z.string().url().optional(),
description: z.string().max(500).optional()
}).optional()
});
var SenderNumberCreateRequestSchema = z.object({
phoneNumber: z.string().regex(/^[0-9]{10,11}$/),
category: z.nativeEnum(SenderNumberCategory),
businessInfo: z.object({
businessName: z.string().min(1),
businessRegistrationNumber: z.string().min(1),
contactPerson: z.string().min(1),
contactEmail: z.string().email()
}).optional()
});
var ChannelFiltersSchema = z.object({
provider: z.string().optional(),
type: z.nativeEnum(ChannelType).optional(),
status: z.nativeEnum(ChannelStatus).optional(),
verified: z.boolean().optional(),
createdAfter: z.date().optional(),
createdBefore: z.date().optional()
});
var SenderNumberFiltersSchema = z.object({
channelId: z.string().optional(),
status: z.nativeEnum(SenderNumberStatus).optional(),
category: z.nativeEnum(SenderNumberCategory).optional(),
verified: 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
import { EventEmitter } from "events";
var ChannelCRUD = class extends 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
import { EventEmitter as EventEmitter2 } from "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 EventEmitter2 {
// 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 */
},
{
id: "viewer-sender-read",
resource: "senderNumber" /* SENDER_NUMBER */,
action: "read" /* READ */,
scope: "organization" /* ORGANIZATION */
},
{
id: "viewer-analytics-read",
resource: "analytics" /* ANALYTICS */,
action: "read" /* READ */,
scope: "organization" /* ORGANIZATION */
}
],
createdAt: /* @__PURE__ */ new Date(),
updatedAt: /* @__PURE__ */ new Date()
};
this.roles.set(superAdminRole.id, superAdminRole);
this.roles.set(channelAdminRole.id, channelAdminRole);
this.roles.set(messageSenderRole.id, messageSenderRole);
this.roles.set(viewerRole.id, viewerRole);
}
updateUserRoleCache(userId, roleIds) {
this.userRoleCache.set(userId, new Set(roleIds));
}
clearUserPermissionCache(userId) {
const keysToDelete = [];
for (const key of this.permissionCache.keys()) {
if (key.startsWith(`${userId}:`)) {
keysToDelete.push(key);
}
}
keysToDelete.forEach((key) => {
this.permissionCache.delete(key);
this.cacheExpiry.delete(key);
});
}
clearRolePermissionCache(roleId) {
for (const [userId, roleIds] of this.userRoleCache) {
if (roleIds.has(roleId)) {
this.clearUserPermissionCache(userId);
}
}
}
getCacheKey(check) {
const contextKey = check.context ? JSON.stringify(check.context) : "";
return `${check.userId}:${check.resource}:${check.action}:${check.resourceId || ""}:${contextKey}`;
}
getFromCache(key) {
const expiry = this.cacheExpiry.get(key);
if (!expiry || expiry < Date.now()) {
this.permissionCache.delete(key);
this.cacheExpiry.delete(key);
return null;
}
return this.permissionCache.get(key) || null;
}
setCache(key, result) {
this.permissionCache.set(key, result);
this.cacheExpiry.set(key, Date.now() + this.CACHE_DURATION);
}
generateUserId() {
return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
generateRoleId() {
return `role_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
};
// src/verification/business.verify.ts
import { EventEmitter as EventEmitter3 } from "events";
var BusinessVerifier = class extends EventEmitter3 {
constructor(options = {}) {
super();
this.options = options;
this.veri