UNPKG

@k-msg/provider

Version:

Complete provider system with adapters and implementations for K-Message platform

1,662 lines (1,651 loc) 59.2 kB
// src/registry/plugin-registry.ts import { EventEmitter } from "events"; var PluginRegistry = class { plugins = /* @__PURE__ */ new Map(); instances = /* @__PURE__ */ new Map(); register(plugin) { const id = plugin.metadata.name.toLowerCase(); if (this.plugins.has(id)) { throw new Error(`Plugin ${id} is already registered`); } this.plugins.set(id, plugin); } async create(pluginId, config, options = {}) { const plugin = this.plugins.get(pluginId.toLowerCase()); if (!plugin) { throw new Error(`Plugin ${pluginId} not found`); } const PluginClass = plugin.constructor; const instance = new PluginClass(); const context = { config, logger: options.logger || new ConsoleLogger(), metrics: options.metrics || new NoOpMetricsCollector(), storage: options.storage || new MemoryStorage(), eventBus: new EventEmitter() }; await instance.initialize(context); const instanceKey = `${pluginId}-${Date.now()}`; this.instances.set(instanceKey, instance); return instance; } async loadAndCreate(pluginId, config, options) { return this.create(pluginId, config, options); } getSupportedTypes() { return Array.from(this.plugins.keys()); } validateProviderConfig(type, config) { const plugin = this.plugins.get(type.toLowerCase()); if (!plugin) return false; return !!(config.apiUrl && config.apiKey); } async destroyAll() { const destroyPromises = Array.from(this.instances.values()).map( (instance) => instance.destroy() ); await Promise.all(destroyPromises); this.instances.clear(); } }; var ConsoleLogger = class { info(message, ...args) { console.log(`[INFO] ${message}`, ...args); } error(message, error) { console.error(`[ERROR] ${message}`, error); } debug(message, ...args) { console.debug(`[DEBUG] ${message}`, ...args); } warn(message, ...args) { console.warn(`[WARN] ${message}`, ...args); } }; var NoOpMetricsCollector = class { increment(_metric, _labels) { } histogram(_metric, _value, _labels) { } gauge(_metric, _value, _labels) { } }; var MemoryStorage = class { store = /* @__PURE__ */ new Map(); async get(key) { const item = this.store.get(key); if (!item) return void 0; if (item.expiry && Date.now() > item.expiry) { this.store.delete(key); return void 0; } return item.value; } async set(key, value, ttl) { const expiry = ttl ? Date.now() + ttl * 1e3 : void 0; this.store.set(key, { value, expiry }); } async delete(key) { this.store.delete(key); } }; // src/middleware/index.ts function createRetryMiddleware(options) { return { name: "retry", error: async (error, context) => { const retries = context.metadata.retries || 0; if (retries >= options.maxRetries) { throw error; } const isRetryable = options.retryableErrors?.includes(error.code) || options.retryableStatusCodes?.includes(error.status) || error.code === "ETIMEDOUT" || error.code === "ECONNRESET"; if (!isRetryable) { throw error; } await new Promise( (resolve) => setTimeout(resolve, options.retryDelay * (retries + 1)) ); context.metadata.retries = retries + 1; throw { ...error, shouldRetry: true }; } }; } function createRateLimitMiddleware(options) { const windows = /* @__PURE__ */ new Map(); return { name: "rate-limit", pre: async (context) => { const now = Date.now(); const key = "global"; if (!windows.has(key)) { windows.set(key, []); } const timestamps = windows.get(key); if (options.messagesPerSecond) { const recentCount = timestamps.filter((t) => now - t < 1e3).length; if (recentCount >= options.messagesPerSecond) { throw new Error("Rate limit exceeded: messages per second"); } } if (options.messagesPerMinute) { const recentCount = timestamps.filter((t) => now - t < 6e4).length; if (recentCount >= options.messagesPerMinute) { throw new Error("Rate limit exceeded: messages per minute"); } } timestamps.push(now); const cutoff = now - 36e5; const filtered = timestamps.filter((t) => t > cutoff); windows.set(key, filtered); } }; } function createLoggingMiddleware(options) { return { name: "logging", pre: async (context) => { if (options.logLevel === "debug") { options.logger.debug("Request started", { metadata: context.metadata, timestamp: context.startTime }); } }, post: async (context) => { const duration = Date.now() - context.startTime; options.logger.info("Request completed", { duration, success: true }); }, error: async (error, context) => { const duration = Date.now() - context.startTime; options.logger.error("Request failed", { error: error.message, duration, stack: error.stack }); } }; } function createMetricsMiddleware(options) { return { name: "metrics", pre: async (context) => { options.collector.increment("requests_total", options.labels); }, post: async (context) => { const duration = Date.now() - context.startTime; options.collector.histogram("request_duration_ms", duration, options.labels); options.collector.increment("requests_success_total", options.labels); }, error: async (error, context) => { const duration = Date.now() - context.startTime; options.collector.histogram("request_duration_ms", duration, options.labels); options.collector.increment("requests_error_total", { ...options.labels, error_type: error.constructor.name }); } }; } function createCircuitBreakerMiddleware(options) { let state = "CLOSED"; let failures = 0; let nextAttempt = 0; return { name: "circuit-breaker", pre: async (context) => { const now = Date.now(); if (state === "OPEN") { if (now < nextAttempt) { throw new Error("Circuit breaker is OPEN"); } state = "HALF_OPEN"; } }, post: async (context) => { if (state === "HALF_OPEN") { state = "CLOSED"; failures = 0; } }, error: async (error, context) => { failures++; if (failures >= options.threshold) { state = "OPEN"; nextAttempt = Date.now() + options.resetTimeout; } throw error; } }; } // src/utils/base-plugin.ts var BasePlugin = class { context; middleware = []; async initialize(context) { this.context = context; this.context.logger.info(`Initializing plugin: ${this.metadata.name}`); } async destroy() { this.context.logger.info(`Destroying plugin: ${this.metadata.name}`); } async executeMiddleware(phase, context, error) { for (const middleware of this.middleware) { try { if (phase === "pre" && middleware.pre) { await middleware.pre(context); } else if (phase === "post" && middleware.post) { await middleware.post(context); } else if (phase === "error" && middleware.error && error) { await middleware.error(error, context); } } catch (err) { this.context.logger.error(`Middleware ${middleware.name} failed`, err); throw err; } } } createMiddlewareContext(request, metadata = {}) { return { request, response: void 0, metadata: { ...metadata, pluginName: this.metadata.name, pluginVersion: this.metadata.version }, startTime: Date.now() }; } validateConfig(config, required) { for (const field of required) { if (!config[field]) { throw new Error(`${this.metadata.name}: Missing required config field: ${field}`); } } } async makeRequest(url, options, metadata = {}) { const context = this.createMiddlewareContext({ url, options }, metadata); try { await this.executeMiddleware("pre", context); const response = await fetch(url, { ...options, headers: { "User-Agent": `K-OTP-${this.metadata.name}/${this.metadata.version}`, ...this.context.config.headers, ...options.headers }, signal: AbortSignal.timeout(this.context.config.timeout || 3e4) }); context.response = response; await this.executeMiddleware("post", context); return response; } catch (error) { await this.executeMiddleware("error", context, error); throw error; } } /** * Make HTTP request and parse JSON response * Subclasses should use their specific response adapters to transform the result */ async makeJSONRequest(url, options, metadata = {}) { const response = await this.makeRequest(url, options, metadata); if (!response.ok) { const error = new Error(`HTTP ${response.status}: ${response.statusText}`); error.response = response; error.status = response.status; throw error; } try { return await response.json(); } catch (parseError) { const error = new Error("Failed to parse JSON response"); error.response = response; error.parseError = parseError; throw error; } } /** * Helper method for logging provider-specific operations */ logOperation(operation, data) { this.context.logger.info(`${this.metadata.name}: ${operation}`, data); } /** * Helper method for logging provider-specific errors */ logError(operation, error, data) { this.context.logger.error(`${this.metadata.name}: ${operation} failed`, { error, data }); } }; // src/utils/index.ts function normalizePhoneNumber(phone) { const cleaned = phone.replace(/[^\d]/g, ""); if (cleaned.startsWith("82")) { return "0" + cleaned.substring(2); } if (cleaned.startsWith("0")) { return cleaned; } if (cleaned.length >= 10 && cleaned.length <= 11) { return "0" + cleaned; } return cleaned; } function validatePhoneNumber(phone) { const normalized = normalizePhoneNumber(phone); return /^01[0-9]{8,9}$/.test(normalized); } function formatDateTime(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); const seconds = String(date.getSeconds()).padStart(2, "0"); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } function parseTemplate(template, variables) { let result = template; for (const [key, value] of Object.entries(variables)) { const regex = new RegExp(`#{${key}}`, "g"); result = result.replace(regex, value); } for (const [key, value] of Object.entries(variables)) { const regex = new RegExp(`{{${key}}}`, "g"); result = result.replace(regex, value); } return result; } function extractVariables(template) { const variables = /* @__PURE__ */ new Set(); const hashMatches = template.match(/#\{([^}]+)\}/g); if (hashMatches) { hashMatches.forEach((match) => { const variable = match.slice(2, -1); variables.add(variable); }); } const braceMatches = template.match(/\{\{([^}]+)\}\}/g); if (braceMatches) { braceMatches.forEach((match) => { const variable = match.slice(2, -2); variables.add(variable); }); } return Array.from(variables); } function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function retry(fn, options) { return new Promise(async (resolve, reject) => { let lastError; for (let attempt = 0; attempt <= options.maxRetries; attempt++) { try { if (attempt > 0) { const delayMs = options.backoff === "exponential" ? options.delay * Math.pow(2, attempt - 1) : options.delay * attempt; await delay(delayMs); } const result = await fn(); resolve(result); return; } catch (error) { lastError = error; if (attempt === options.maxRetries) { reject(lastError); return; } } } }); } // src/abstract/provider.base.ts var BaseAlimTalkProvider = class { config = {}; isConfigured = false; constructor(config) { if (config) { this.configure(config); } } /** * Configure the provider with necessary credentials and settings */ configure(config) { this.validateConfiguration(config); this.config = { ...config }; this.isConfigured = true; this.onConfigured(); } /** * Validate the provided configuration */ validateConfiguration(config) { const schema = this.getConfigurationSchema(); for (const field of schema.required) { if (!(field.key in config)) { throw new Error(`Required configuration field '${field.key}' is missing`); } this.validateFieldValue(field, config[field.key]); } for (const field of schema.optional) { if (field.key in config) { this.validateFieldValue(field, config[field.key]); } } } validateFieldValue(field, value) { switch (field.type) { case "string": if (typeof value !== "string") { throw new Error(`Field '${field.key}' must be a string`); } break; case "number": if (typeof value !== "number") { throw new Error(`Field '${field.key}' must be a number`); } break; case "boolean": if (typeof value !== "boolean") { throw new Error(`Field '${field.key}' must be a boolean`); } break; case "url": try { new URL(String(value)); } catch { throw new Error(`Field '${field.key}' must be a valid URL`); } break; } if (field.validation) { if (field.validation.pattern) { const regex = new RegExp(field.validation.pattern); if (!regex.test(String(value))) { throw new Error(`Field '${field.key}' does not match required pattern`); } } if (field.validation.min !== void 0 && Number(value) < field.validation.min) { throw new Error(`Field '${field.key}' must be at least ${field.validation.min}`); } if (field.validation.max !== void 0 && Number(value) > field.validation.max) { throw new Error(`Field '${field.key}' must be at most ${field.validation.max}`); } } } /** * Called after configuration is set */ onConfigured() { } /** * Check if the provider is properly configured */ isReady() { return this.isConfigured; } /** * Get configuration value */ getConfig(key) { if (!this.isConfigured) { throw new Error("Provider is not configured"); } return this.config[key]; } /** * Check if a configuration key exists */ hasConfig(key) { return key in this.config; } /** * Perform health check on the provider */ async healthCheck() { const issues = []; const startTime = Date.now(); try { if (!this.isReady()) { issues.push("Provider is not configured"); return { healthy: false, issues }; } await this.testConnectivity(); await this.testAuthentication(); const latency = Date.now() - startTime; return { healthy: issues.length === 0, issues, latency }; } catch (error) { issues.push(`Health check failed: ${error instanceof Error ? error.message : "Unknown error"}`); return { healthy: false, issues }; } } /** * Get provider information */ getInfo() { return { id: this.id, name: this.name, version: this.getVersion(), capabilities: this.capabilities, configured: this.isConfigured }; } /** * Cleanup resources when provider is destroyed */ destroy() { this.config = {}; this.isConfigured = false; this.onDestroy(); } /** * Called when provider is being destroyed */ onDestroy() { } /** * Create standardized error */ createError(code, message, details) { const error = new Error(message); error.code = code; error.provider = this.id; error.details = details; return error; } /** * Log provider activity */ log(level, message, data) { const logData = { provider: this.id, level, message, timestamp: (/* @__PURE__ */ new Date()).toISOString() }; if (data) { logData.data = data; } console.log(JSON.stringify(logData)); } /** * Handle rate limiting */ async handleRateLimit(operation) { const rateLimit = this.capabilities.messaging.maxRequestsPerSecond; if (rateLimit > 0) { const delay2 = 1e3 / rateLimit; await new Promise((resolve) => setTimeout(resolve, delay2)); } } /** * Retry mechanism for failed operations */ async withRetry(operation, options = {}) { const { maxRetries = 3, initialDelay = 1e3, maxDelay = 1e4, backoffFactor = 2 } = options; let lastError; let delay2 = initialDelay; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error; if (attempt === maxRetries) { break; } this.log("warn", `Operation failed, retrying in ${delay2}ms`, { attempt: attempt + 1, maxRetries, error: lastError.message }); await new Promise((resolve) => setTimeout(resolve, delay2)); delay2 = Math.min(delay2 * backoffFactor, maxDelay); } } throw lastError; } }; // src/adapters/request.adapter.ts var BaseRequestAdapter = class { /** * Common transformation utilities */ formatPhoneNumber(phoneNumber, countryCode = "KR") { const digits = phoneNumber.replace(/\D/g, ""); if (countryCode === "KR") { if (digits.startsWith("82")) { return digits.substring(2); } if (digits.startsWith("0")) { return digits; } return "0" + digits; } return phoneNumber; } formatVariables(variables) { const formatted = {}; for (const [key, value] of Object.entries(variables)) { if (value instanceof Date) { formatted[key] = value.toISOString(); } else if (typeof value === "object") { formatted[key] = JSON.stringify(value); } else { formatted[key] = String(value); } } return formatted; } validateRequiredFields(data, requiredFields) { const obj = data; for (const field of requiredFields) { if (!(field in obj) || obj[field] === void 0 || obj[field] === null) { throw new Error(`Required field '${field}' is missing`); } } } }; var IWINVRequestAdapter = class extends BaseRequestAdapter { transformMessageRequest(request) { this.validateRequiredFields(request, ["templateCode", "phoneNumber"]); return { profile_key: this.getProfileKey(), template_code: request.templateCode, phone_number: this.formatPhoneNumber(request.phoneNumber), message_variables: this.formatVariables(request.variables), sender_number: request.senderNumber, reserve_time: request.options?.scheduledAt ? Math.floor(new Date(request.options.scheduledAt).getTime() / 1e3) : void 0 }; } transformTemplateRequest(request) { this.validateRequiredFields(request, ["name", "content"]); return { profile_key: this.getProfileKey(), template_name: request.name, template_content: request.content, template_category: this.mapCategory(request.category), template_variables: request.variables?.map((v) => ({ name: v.name, type: v.type, required: v.required ? "Y" : "N", max_length: v.maxLength })), template_buttons: request.buttons?.map((b) => ({ type: b.type, name: b.name, url_mobile: b.linkMobile, url_pc: b.linkPc, scheme_ios: b.schemeIos, scheme_android: b.schemeAndroid })) }; } getProfileKey() { return process.env.IWINV_PROFILE_KEY || ""; } mapCategory(category) { const categoryMap = { "AUTHENTICATION": "A", "NOTIFICATION": "N", "PROMOTION": "P", "INFORMATION": "I" }; return categoryMap[category] || "I"; } }; var AligoRequestAdapter = class extends BaseRequestAdapter { transformMessageRequest(request) { this.validateRequiredFields(request, ["templateCode", "phoneNumber"]); return { apikey: this.getApiKey(), userid: this.getUserId(), senderkey: this.getSenderKey(), template_code: request.templateCode, receiver: this.formatPhoneNumber(request.phoneNumber), subject: "AlimTalk", message: this.buildMessage(request), button: request.variables.buttons ? JSON.stringify(request.variables.buttons) : void 0, reservation: request.options?.scheduledAt ? this.formatDateTime(new Date(request.options.scheduledAt)) : void 0 }; } transformTemplateRequest(request) { this.validateRequiredFields(request, ["name", "content"]); return { apikey: this.getApiKey(), userid: this.getUserId(), senderkey: this.getSenderKey(), template_name: request.name, template_content: request.content, template_emphasis: this.extractEmphasis(request.content), template_extra: this.buildTemplateExtra(request), template_ad: this.isPromotional(request.category) ? "Y" : "N" }; } getApiKey() { return process.env.ALIGO_API_KEY || ""; } getUserId() { return process.env.ALIGO_USER_ID || ""; } getSenderKey() { return process.env.ALIGO_SENDER_KEY || ""; } buildMessage(request) { return request.templateCode; } formatDateTime(date) { return date.toISOString().replace(/[-:]/g, "").replace("T", "").substring(0, 12); } extractEmphasis(content) { const emphasisMatch = content.match(/\*\*(.*?)\*\*/); return emphasisMatch ? emphasisMatch[1] : ""; } buildTemplateExtra(request) { const extra = {}; if (request.buttons) { extra.buttons = request.buttons; } if (request.variables) { extra.variables = request.variables; } return JSON.stringify(extra); } isPromotional(category) { return category === "PROMOTION"; } }; var KakaoRequestAdapter = class extends BaseRequestAdapter { transformMessageRequest(request) { this.validateRequiredFields(request, ["templateCode", "phoneNumber"]); return { template_object: { object_type: "text", text: this.buildTemplateText(request), link: this.buildTemplateLink(request), button_title: request.variables.buttonTitle || "" }, user_ids: [this.formatPhoneNumber(request.phoneNumber)] }; } transformTemplateRequest(request) { this.validateRequiredFields(request, ["name", "content"]); return { template: { name: request.name, content: request.content, category_code: this.mapCategoryCode(request.category), template_message_type: "BA", // Basic AlimTalk template_emphasis_type: this.extractEmphasisType(request.content), template_title: request.name, template_subtitle: "", template_imageurl: "", template_header: "", template_item_highlight: { title: "", description: "" }, template_item: { list: [] }, template_button: this.buildTemplateButtons(request.buttons || []) } }; } buildTemplateText(request) { let text = request.templateCode; for (const [key, value] of Object.entries(request.variables)) { text = text.replace(new RegExp(`#{${key}}`, "g"), String(value)); } return text; } buildTemplateLink(request) { if (request.variables.linkUrl) { return { web_url: request.variables.linkUrl, mobile_web_url: request.variables.linkUrl }; } return {}; } mapCategoryCode(category) { const categoryMap = { "AUTHENTICATION": "999999", "NOTIFICATION": "999998", "PROMOTION": "999997", "INFORMATION": "999996" }; return categoryMap[category] || "999999"; } extractEmphasisType(content) { if (content.includes("**")) return "BOLD"; if (content.includes("__")) return "UNDERLINE"; return "NONE"; } buildTemplateButtons(buttons) { return buttons.map((button) => { const btn = button; return { name: btn.name, type: btn.type, url_mobile: btn.linkMobile, url_pc: btn.linkPc, scheme_ios: btn.schemeIos, scheme_android: btn.schemeAndroid }; }); } }; var RequestAdapterFactory = class _RequestAdapterFactory { static adapters = /* @__PURE__ */ new Map(); static { _RequestAdapterFactory.adapters.set("iwinv", IWINVRequestAdapter); _RequestAdapterFactory.adapters.set("aligo", AligoRequestAdapter); _RequestAdapterFactory.adapters.set("kakao", KakaoRequestAdapter); } static create(providerId) { const AdapterClass = this.adapters.get(providerId.toLowerCase()); if (!AdapterClass) { throw new Error(`No request adapter found for provider: ${providerId}`); } return new AdapterClass(); } static register(providerId, adapterClass) { this.adapters.set(providerId.toLowerCase(), adapterClass); } }; // src/adapters/response.adapter.ts var BaseResponseAdapter = class { /** * Common error transformation */ transformError(providerError) { return { code: this.extractErrorCode(providerError), message: this.extractErrorMessage(providerError), details: this.extractErrorDetails(providerError) }; } /** * Common status mapping utilities */ mapMessageStatus(providerStatus) { const statusMap = { "queued": "QUEUED" /* QUEUED */, "sending": "SENDING" /* SENDING */, "sent": "SENT" /* SENT */, "delivered": "DELIVERED" /* DELIVERED */, "failed": "FAILED" /* FAILED */, "cancelled": "CANCELLED" /* CANCELLED */ }; return statusMap[providerStatus.toLowerCase()] || "FAILED" /* FAILED */; } mapTemplateStatus(providerStatus) { const statusMap = { "draft": "DRAFT" /* DRAFT */, "pending": "PENDING" /* PENDING */, "approved": "APPROVED" /* APPROVED */, "rejected": "REJECTED" /* REJECTED */, "disabled": "DISABLED" /* DISABLED */ }; return statusMap[providerStatus.toLowerCase()] || "DRAFT" /* DRAFT */; } parseDate(dateString) { if (!dateString) return void 0; try { return new Date(dateString); } catch { return void 0; } } }; var IWINVResponseAdapter = class extends BaseResponseAdapter { transformMessageResponse(providerResponse) { const response = providerResponse; return { messageId: response.msg_id || response.msgid, status: this.mapIWINVMessageStatus(response.result_code), sentAt: this.parseDate(response.send_time), error: response.result_code !== "1" ? this.transformError(providerResponse) : void 0 }; } transformTemplateResponse(providerResponse) { const response = providerResponse; return { templateId: response.template_id, providerTemplateCode: response.template_code, status: this.mapIWINVTemplateStatus(response.status), message: response.message || response.comment }; } mapIWINVMessageStatus(resultCode) { const statusMap = { "1": "SENT" /* SENT */, "0": "FAILED" /* FAILED */, "-1": "FAILED" /* FAILED */, "-2": "FAILED" /* FAILED */, "-3": "FAILED" /* FAILED */, "-4": "FAILED" /* FAILED */ }; return statusMap[resultCode] || "FAILED" /* FAILED */; } mapIWINVTemplateStatus(status) { const statusMap = { "R": "PENDING" /* PENDING */, // Request "A": "APPROVED" /* APPROVED */, // Approved "C": "REJECTED" /* REJECTED */, // Cancelled/Rejected "S": "PENDING" /* PENDING */ // Standby }; return statusMap[status] || "DRAFT" /* DRAFT */; } extractErrorCode(providerError) { const error = providerError; return error.result_code || error.error_code || "UNKNOWN_ERROR"; } extractErrorMessage(providerError) { const error = providerError; return error.message || error.error_message || "Unknown error occurred"; } extractErrorDetails(providerError) { const error = providerError; return { resultCode: error.result_code, originalResponse: providerError }; } }; var AligoResponseAdapter = class extends BaseResponseAdapter { transformMessageResponse(providerResponse) { const response = providerResponse; return { messageId: response.msg_id || response.mid, status: this.mapAligoMessageStatus(response.result_code), sentAt: this.parseDate(response.send_time), error: response.result_code !== "1" ? this.transformError(providerResponse) : void 0 }; } transformTemplateResponse(providerResponse) { const response = providerResponse; return { templateId: response.template_code, providerTemplateCode: response.template_code, status: this.mapAligoTemplateStatus(response.inspect_status), message: response.comment }; } mapAligoMessageStatus(resultCode) { const statusMap = { "1": "SENT" /* SENT */, "0": "FAILED" /* FAILED */, "-1": "FAILED" /* FAILED */, "-101": "FAILED" /* FAILED */, "-102": "FAILED" /* FAILED */ }; return statusMap[resultCode] || "FAILED" /* FAILED */; } mapAligoTemplateStatus(inspectStatus) { const statusMap = { "REG": "PENDING" /* PENDING */, // Registered "REQ": "PENDING" /* PENDING */, // Request "APR": "APPROVED" /* APPROVED */, // Approved "REJ": "REJECTED" /* REJECTED */, // Rejected "STOP": "DISABLED" /* DISABLED */ // Stopped }; return statusMap[inspectStatus] || "DRAFT" /* DRAFT */; } extractErrorCode(providerError) { const error = providerError; return error.result_code || error.code || "UNKNOWN_ERROR"; } extractErrorMessage(providerError) { const error = providerError; return error.message || error.error || "Unknown error occurred"; } extractErrorDetails(providerError) { const error = providerError; return { resultCode: error.result_code, inspectStatus: error.inspect_status, originalResponse: providerError }; } }; var KakaoResponseAdapter = class extends BaseResponseAdapter { transformMessageResponse(providerResponse) { const response = providerResponse; return { messageId: response.message_id, status: this.mapKakaoMessageStatus(response.result_code), sentAt: this.parseDate(response.sent_time), error: response.result_code !== 0 ? this.transformError(providerResponse) : void 0 }; } transformTemplateResponse(providerResponse) { const response = providerResponse; return { templateId: response.template_id, providerTemplateCode: response.template_code, status: this.mapKakaoTemplateStatus(response.status), message: response.comments }; } mapKakaoMessageStatus(resultCode) { const statusMap = { 0: "SENT" /* SENT */, [-1]: "FAILED" /* FAILED */, [-2]: "FAILED" /* FAILED */, [-3]: "FAILED" /* FAILED */, [-999]: "FAILED" /* FAILED */ }; return statusMap[resultCode] || "FAILED" /* FAILED */; } mapKakaoTemplateStatus(status) { const statusMap = { "TSC01": "PENDING" /* PENDING */, // Under review "TSC02": "APPROVED" /* APPROVED */, // Approved "TSC03": "REJECTED" /* REJECTED */, // Rejected "TSC04": "DISABLED" /* DISABLED */ // Disabled }; return statusMap[status] || "DRAFT" /* DRAFT */; } extractErrorCode(providerError) { const error = providerError; return String(error.result_code || error.error_code || "UNKNOWN_ERROR"); } extractErrorMessage(providerError) { const error = providerError; return error.message || error.error_message || "Unknown error occurred"; } extractErrorDetails(providerError) { const error = providerError; return { resultCode: error.result_code, originalResponse: providerError }; } }; var NHNResponseAdapter = class extends BaseResponseAdapter { transformMessageResponse(providerResponse) { const response = providerResponse; return { messageId: response.requestId, status: this.mapNHNMessageStatus(response.statusCode), sentAt: this.parseDate(response.statusDateTime), error: response.statusCode !== "SSS" ? this.transformError(providerResponse) : void 0 }; } transformTemplateResponse(providerResponse) { const response = providerResponse; return { templateId: response.templateId, providerTemplateCode: response.templateId, status: this.mapNHNTemplateStatus(response.templateStatus), message: response.templateStatusName }; } mapNHNMessageStatus(statusCode) { const statusMap = { "SSS": "SENT" /* SENT */, // Success "RDY": "QUEUED" /* QUEUED */, // Ready "PRG": "SENDING" /* SENDING */, // Progress "CPL": "DELIVERED" /* DELIVERED */, // Complete "FAL": "FAILED" /* FAILED */, // Failed "CAL": "CANCELLED" /* CANCELLED */ // Cancelled }; return statusMap[statusCode] || "FAILED" /* FAILED */; } mapNHNTemplateStatus(templateStatus) { const statusMap = { "WAITING": "PENDING" /* PENDING */, "APPROVED": "APPROVED" /* APPROVED */, "REJECTED": "REJECTED" /* REJECTED */, "DISABLED": "DISABLED" /* DISABLED */ }; return statusMap[templateStatus] || "DRAFT" /* DRAFT */; } extractErrorCode(providerError) { const error = providerError; return error.statusCode || error.errorCode || "UNKNOWN_ERROR"; } extractErrorMessage(providerError) { const error = providerError; return error.statusMessage || error.errorMessage || "Unknown error occurred"; } extractErrorDetails(providerError) { const error = providerError; return { statusCode: error.statusCode, statusMessage: error.statusMessage, originalResponse: providerError }; } }; var ResponseAdapterFactory = class { static adapters = /* @__PURE__ */ new Map([ ["iwinv", IWINVResponseAdapter], ["aligo", AligoResponseAdapter], ["kakao", KakaoResponseAdapter], ["nhn", NHNResponseAdapter] ]); static create(providerId) { const AdapterClass = this.adapters.get(providerId.toLowerCase()); if (!AdapterClass) { throw new Error(`No response adapter found for provider: ${providerId}`); } return new AdapterClass(); } static register(providerId, adapterClass) { this.adapters.set(providerId.toLowerCase(), adapterClass); } }; // src/services/provider.manager.ts var ProviderManager = class { providers = /* @__PURE__ */ new Map(); defaultProvider; registerProvider(provider) { this.providers.set(provider.id, provider); if (!this.defaultProvider) { this.defaultProvider = provider.id; } } unregisterProvider(providerId) { this.providers.delete(providerId); if (this.defaultProvider === providerId) { const remaining = Array.from(this.providers.keys()); this.defaultProvider = remaining.length > 0 ? remaining[0] : void 0; } } getProvider(providerId) { const id = providerId || this.defaultProvider; return id ? this.providers.get(id) || null : null; } listProviders() { return Array.from(this.providers.values()); } setDefaultProvider(providerId) { if (!this.providers.has(providerId)) { throw new Error(`Provider ${providerId} not found`); } this.defaultProvider = providerId; } async healthCheckAll() { const results = {}; for (const [id, provider] of this.providers.entries()) { try { const health = await provider.healthCheck(); results[id] = health.healthy; } catch (error) { results[id] = false; } } return results; } getProvidersForChannel(channel) { return Array.from(this.providers.values()).filter((provider) => { const providerWithChannels = provider; return providerWithChannels.supportedChannels?.includes(channel); }); } }; // src/iwinv/contracts/messaging.contract.ts var IWINVMessagingContract = class { constructor(config) { this.config = config; } async send(message) { try { const response = await fetch(`${this.config.baseUrl}/send`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.config.apiKey}` }, body: JSON.stringify({ templateCode: message.templateCode, phone: message.phoneNumber, variables: message.variables, senderNumber: message.senderNumber, ...message.options }) }); const result = await response.json(); if (!response.ok) { return { messageId: `failed_${Date.now()}`, status: "FAILED" /* FAILED */, error: { code: result.code || "SEND_FAILED", message: result.message || "Failed to send message" } }; } return { messageId: result.messageId || `msg_${Date.now()}`, status: "SENT" /* SENT */, sentAt: /* @__PURE__ */ new Date() }; } catch (error) { return { messageId: `error_${Date.now()}`, status: "FAILED" /* FAILED */, error: { code: "NETWORK_ERROR", message: error instanceof Error ? error.message : "Network error occurred" } }; } } async sendBulk(messages) { const results = []; for (const message of messages) { const result = await this.send(message); results.push(result); } const sent = results.filter((r) => r.status === "SENT" /* SENT */).length; const failed = results.filter((r) => r.status === "FAILED" /* FAILED */).length; return { requestId: `bulk_${Date.now()}`, results, summary: { total: messages.length, sent, failed } }; } async schedule(message, scheduledAt) { return { scheduleId: `schedule_${Date.now()}`, messageId: `msg_${Date.now()}`, scheduledAt, status: "scheduled" }; } async cancel(messageId) { const response = await fetch(`${this.config.baseUrl}/cancel`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.config.apiKey}` }, body: JSON.stringify({ messageId }) }); if (!response.ok) { const result = await response.json(); throw new Error(`Failed to cancel message: ${result.message}`); } } async getStatus(messageId) { try { const response = await fetch(`${this.config.baseUrl}/status`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.config.apiKey}` }, body: JSON.stringify({ messageId }) }); const result = await response.json(); if (!response.ok) { return "FAILED" /* FAILED */; } switch (result.statusCode) { case "OK": return "DELIVERED" /* DELIVERED */; case "PENDING": return "SENDING" /* SENDING */; case "FAILED": return "FAILED" /* FAILED */; default: return "SENT" /* SENT */; } } catch (error) { return "FAILED" /* FAILED */; } } }; // src/iwinv/contracts/template.contract.ts var IWINVTemplateContract = class { constructor(config) { this.config = config; } async create(template) { try { const response = await fetch(`${this.config.baseUrl}/template/create`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.config.apiKey}` }, body: JSON.stringify({ templateName: template.name, templateContent: template.content, templateCategory: template.category, templateVariables: template.variables, templateButtons: template.buttons }) }); const result = await response.json(); if (!response.ok) { throw new Error(`Template creation failed: ${result.message}`); } return { templateId: result.templateId || `tpl_${Date.now()}`, providerTemplateCode: result.templateCode || template.name, status: "PENDING" /* PENDING */, message: result.message || "Template created successfully" }; } catch (error) { throw new Error(`Failed to create template: ${error instanceof Error ? error.message : "Unknown error"}`); } } async update(templateId, template) { try { const response = await fetch(`${this.config.baseUrl}/template/modify`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.config.apiKey}` }, body: JSON.stringify({ templateCode: templateId, templateName: template.name, templateContent: template.content, templateButtons: template.buttons }) }); const result = await response.json(); if (!response.ok) { throw new Error(`Template update failed: ${result.message}`); } return { templateId, status: "PENDING" /* PENDING */, message: result.message || "Template updated successfully" }; } catch (error) { throw new Error(`Failed to update template: ${error instanceof Error ? error.message : "Unknown error"}`); } } async delete(templateId) { try { const response = await fetch(`${this.config.baseUrl}/template/delete`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.config.apiKey}` }, body: JSON.stringify({ templateCode: templateId }) }); const result = await response.json(); if (!response.ok) { throw new Error(`Template deletion failed: ${result.message}`); } } catch (error) { throw new Error(`Failed to delete template: ${error instanceof Error ? error.message : "Unknown error"}`); } } async get(templateId) { const templates = await this.list({ templateCode: templateId }); const template = templates.find((t) => t.code === templateId); if (!template) { throw new Error(`Template ${templateId} not found`); } return template; } async list(filters) { try { const response = await fetch(`${this.config.baseUrl}/template/list`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.config.apiKey}` }, body: JSON.stringify({ page: 1, size: 100, ...filters }) }); const result = await response.json(); if (!response.ok) { throw new Error(`Failed to list templates: ${result.message}`); } return (result.list || []).map((template) => ({ id: template.templateId || template.templateCode, code: template.templateCode, name: template.templateName, content: template.templateContent, status: this.mapIWINVStatus(template.status || template.templateStatus), createdAt: template.createDate ? new Date(template.createDate) : /* @__PURE__ */ new Date(), updatedAt: template.updateDate ? new Date(template.updateDate) : /* @__PURE__ */ new Date(), approvedAt: template.approvedAt ? new Date(template.approvedAt) : void 0, rejectedAt: template.rejectedAt ? new Date(template.rejectedAt) : void 0, rejectionReason: template.rejectionReason })); } catch (error) { throw new Error(`Failed to list templates: ${error instanceof Error ? error.message : "Unknown error"}`); } } async sync() { try { const templates = await this.list(); return { synced: templates.length, created: 0, updated: 0, deleted: 0, errors: [] }; } catch (error) { return { synced: 0, created: 0, updated: 0, deleted: 0, errors: [{ templateId: "unknown", error: error instanceof Error ? error.message : "Sync failed" }] }; } } mapIWINVStatus(status) { switch (status) { case "Y": return "APPROVED" /* APPROVED */; case "I": return "PENDING" /* PENDING */; case "R": return "REJECTED" /* REJECTED */; case "D": return "DISABLED" /* DISABLED */; default: return "DRAFT" /* DRAFT */; } } }; // src/iwinv/contracts/channel.contract.ts var IWINVChannelContract = class { constructor(config) { this.config = config; } async register(channel) { return { id: `channel_${Date.now()}`, name: channel.name, profileKey: channel.profileKey, status: "pending", createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }; } async list() { return [ { id: "iwinv-default", name: "IWINV Default Channel", profileKey: "default", status: "active", createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() } ]; } async addSenderNumber(channelId, number) { return { id: `sender_${Date.now()}`, channelId, phoneNumber: number, isVerified: false, createdAt: /* @__PURE__ */ new Date() }; } async verifySenderNumber(number, verificationCode) { return true; } }; // src/iwinv/contracts/analytics.contract.ts var IWINVAnalyticsContract = class { constructor(config) { this.config = config; } async getUsage(period) { try { const response = await fetch(`${this.config.baseUrl}/history/list`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.config.apiKey}` }, body: JSON.stringify({ startDate: period.from.toISOString(), endDate: period.to.toISOString(), page: 1, size: 1e3 }) }); const result = await response.json(); if (!response.ok) { throw new Error(`Failed to get usage stats: ${result.message}`); } const messages = result.list || []; const totalMessages = messages.length; const deliveredMessages = messages.filter((msg) => msg.statusCode === "OK").length; const failedMessages = messages.filter((msg) => msg.statusCode === "FAILED").length; const sentMessages = totalMessages - failedMessages; return { period, totalMessages, sentMessages, deliveredMessages, failedMessages, deliveryRate: totalMessages > 0 ? deliveredMessages / totalMessages * 100 : 0, failureRate: totalMessages > 0 ? failedMessages / totalMessages * 100 : 0, breakdown: { byTemplate: this.groupByTemplate(messages), byDay: this.groupByDay(messages, period), byHour: this.groupByHour(messages) } }; } catch (error) { return { period, totalMessages: 0, sentMessages: 0, deliveredMessages: 0, failedMessages: 0, deliveryRate: 0, failureRate: 0, breakdown: { byTemplate: {}, byDay: {}, byHour: {} } }; } } async getTemplateStats(templateId, period) { try { const usage = await this.getUsage(period); const templateMessages = usage.breakdown.byTemplate[templateId] || 0; return { templateId, period, totalSent: templateMessages, delivered: Math.round(templateMessages * (usage.deliveryRate / 100)), failed: Math.round(templateMessages * (usage.failureRate / 100)), deliveryRate: usage.deliveryRate, averageDeliveryTime: 30 // Mock average delivery time in seconds }; } catch (error) { return { templateId, period, totalSent: 0, delivered: 0, failed: 0, deliveryRate: 0, averageDeliveryTime: 0 }; } } async getDeliveryReport(messageId) { try { const response = await fetch(`${this.config.baseUrl}/history/detail`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.config.apiKey}` }, body: JSON.stringify({ messageId: parseInt(messageId) || 0 }) }); const result = await response.json(); if (!response.ok) { throw new Error(`Failed to get delivery report: ${result.message}`); } return { messageId, phoneNumber: result.phone || "unknown", templateCode: result.templateCode || "unknown", status: this.mapStatus(result.statusCode), sentAt: result.sendDate ? new Date(result.sendDate)