@k-msg/provider
Version:
Complete provider system with adapters and implementations for K-Message platform
1,641 lines (1,628 loc) • 61.6 kB
JavaScript
"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, {
AligoRequestAdapter: () => AligoRequestAdapter,
AligoResponseAdapter: () => AligoResponseAdapter,
BaseAlimTalkProvider: () => BaseAlimTalkProvider,
BasePlugin: () => BasePlugin,
BaseRequestAdapter: () => BaseRequestAdapter,
BaseResponseAdapter: () => BaseResponseAdapter,
IWINVProvider: () => IWINVProvider,
IWINVRequestAdapter: () => IWINVRequestAdapter,
IWINVResponseAdapter: () => IWINVResponseAdapter,
KakaoRequestAdapter: () => KakaoRequestAdapter,
KakaoResponseAdapter: () => KakaoResponseAdapter,
NHNResponseAdapter: () => NHNResponseAdapter,
PluginRegistry: () => PluginRegistry,
ProviderManager: () => ProviderManager,
RequestAdapterFactory: () => RequestAdapterFactory,
ResponseAdapterFactory: () => ResponseAdapterFactory,
createCircuitBreakerMiddleware: () => createCircuitBreakerMiddleware,
createLoggingMiddleware: () => createLoggingMiddleware,
createMetricsMiddleware: () => createMetricsMiddleware,
createRateLimitMiddleware: () => createRateLimitMiddleware,
createRetryMiddleware: () => createRetryMiddleware,
delay: () => delay,
extractVariables: () => extractVariables,
formatDateTime: () => formatDateTime,
normalizePhoneNumber: () => normalizePhoneNumber,
parseTemplate: () => parseTemplate,
retry: () => retry,
validatePhoneNumber: () => validatePhoneNumber
});
module.exports = __toCommonJS(index_exports);
// src/registry/plugin-registry.ts
var import_events = require("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 import_events.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,
deliveredMessage