@k-msg/provider 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2007 @@
1
+ // src/registry/plugin-registry.ts
2
+ import { EventEmitter } from "events";
3
+ var PluginRegistry = class {
4
+ plugins = /* @__PURE__ */ new Map();
5
+ instances = /* @__PURE__ */ new Map();
6
+ register(plugin) {
7
+ const id = plugin.metadata.name.toLowerCase();
8
+ if (this.plugins.has(id)) {
9
+ throw new Error(`Plugin ${id} is already registered`);
10
+ }
11
+ this.plugins.set(id, plugin);
12
+ }
13
+ async create(pluginId, config, options = {}) {
14
+ const plugin = this.plugins.get(pluginId.toLowerCase());
15
+ if (!plugin) {
16
+ throw new Error(`Plugin ${pluginId} not found`);
17
+ }
18
+ const PluginClass = plugin.constructor;
19
+ const instance = new PluginClass();
20
+ const context = {
21
+ config,
22
+ logger: options.logger || new ConsoleLogger(),
23
+ metrics: options.metrics || new NoOpMetricsCollector(),
24
+ storage: options.storage || new MemoryStorage(),
25
+ eventBus: new EventEmitter()
26
+ };
27
+ await instance.initialize(context);
28
+ const instanceKey = `${pluginId}-${Date.now()}`;
29
+ this.instances.set(instanceKey, instance);
30
+ return instance;
31
+ }
32
+ async loadAndCreate(pluginId, config, options) {
33
+ return this.create(pluginId, config, options);
34
+ }
35
+ getSupportedTypes() {
36
+ return Array.from(this.plugins.keys());
37
+ }
38
+ validateProviderConfig(type, config) {
39
+ const plugin = this.plugins.get(type.toLowerCase());
40
+ if (!plugin) return false;
41
+ return !!(config.apiUrl && config.apiKey);
42
+ }
43
+ async destroyAll() {
44
+ const destroyPromises = Array.from(this.instances.values()).map(
45
+ (instance) => instance.destroy()
46
+ );
47
+ await Promise.all(destroyPromises);
48
+ this.instances.clear();
49
+ }
50
+ };
51
+ var ConsoleLogger = class {
52
+ info(message, ...args) {
53
+ console.log(`[INFO] ${message}`, ...args);
54
+ }
55
+ error(message, error) {
56
+ console.error(`[ERROR] ${message}`, error);
57
+ }
58
+ debug(message, ...args) {
59
+ console.debug(`[DEBUG] ${message}`, ...args);
60
+ }
61
+ warn(message, ...args) {
62
+ console.warn(`[WARN] ${message}`, ...args);
63
+ }
64
+ };
65
+ var NoOpMetricsCollector = class {
66
+ increment(_metric, _labels) {
67
+ }
68
+ histogram(_metric, _value, _labels) {
69
+ }
70
+ gauge(_metric, _value, _labels) {
71
+ }
72
+ };
73
+ var MemoryStorage = class {
74
+ store = /* @__PURE__ */ new Map();
75
+ async get(key) {
76
+ const item = this.store.get(key);
77
+ if (!item) return void 0;
78
+ if (item.expiry && Date.now() > item.expiry) {
79
+ this.store.delete(key);
80
+ return void 0;
81
+ }
82
+ return item.value;
83
+ }
84
+ async set(key, value, ttl) {
85
+ const expiry = ttl ? Date.now() + ttl * 1e3 : void 0;
86
+ this.store.set(key, { value, expiry });
87
+ }
88
+ async delete(key) {
89
+ this.store.delete(key);
90
+ }
91
+ };
92
+
93
+ // src/middleware/index.ts
94
+ function createRetryMiddleware(options) {
95
+ return {
96
+ name: "retry",
97
+ error: async (error, context) => {
98
+ const retries = context.metadata.retries || 0;
99
+ if (retries >= options.maxRetries) {
100
+ throw error;
101
+ }
102
+ const isRetryable = options.retryableErrors?.includes(error.code) || options.retryableStatusCodes?.includes(error.status) || error.code === "ETIMEDOUT" || error.code === "ECONNRESET";
103
+ if (!isRetryable) {
104
+ throw error;
105
+ }
106
+ await new Promise(
107
+ (resolve) => setTimeout(resolve, options.retryDelay * (retries + 1))
108
+ );
109
+ context.metadata.retries = retries + 1;
110
+ throw { ...error, shouldRetry: true };
111
+ }
112
+ };
113
+ }
114
+ function createRateLimitMiddleware(options) {
115
+ const windows = /* @__PURE__ */ new Map();
116
+ return {
117
+ name: "rate-limit",
118
+ pre: async (context) => {
119
+ const now = Date.now();
120
+ const key = "global";
121
+ if (!windows.has(key)) {
122
+ windows.set(key, []);
123
+ }
124
+ const timestamps = windows.get(key);
125
+ if (options.messagesPerSecond) {
126
+ const recentCount = timestamps.filter((t) => now - t < 1e3).length;
127
+ if (recentCount >= options.messagesPerSecond) {
128
+ throw new Error("Rate limit exceeded: messages per second");
129
+ }
130
+ }
131
+ if (options.messagesPerMinute) {
132
+ const recentCount = timestamps.filter((t) => now - t < 6e4).length;
133
+ if (recentCount >= options.messagesPerMinute) {
134
+ throw new Error("Rate limit exceeded: messages per minute");
135
+ }
136
+ }
137
+ timestamps.push(now);
138
+ const cutoff = now - 36e5;
139
+ const filtered = timestamps.filter((t) => t > cutoff);
140
+ windows.set(key, filtered);
141
+ }
142
+ };
143
+ }
144
+ function createLoggingMiddleware(options) {
145
+ return {
146
+ name: "logging",
147
+ pre: async (context) => {
148
+ if (options.logLevel === "debug") {
149
+ options.logger.debug("Request started", {
150
+ metadata: context.metadata,
151
+ timestamp: context.startTime
152
+ });
153
+ }
154
+ },
155
+ post: async (context) => {
156
+ const duration = Date.now() - context.startTime;
157
+ options.logger.info("Request completed", {
158
+ duration,
159
+ success: true
160
+ });
161
+ },
162
+ error: async (error, context) => {
163
+ const duration = Date.now() - context.startTime;
164
+ options.logger.error("Request failed", {
165
+ error: error.message,
166
+ duration,
167
+ stack: error.stack
168
+ });
169
+ }
170
+ };
171
+ }
172
+ function createMetricsMiddleware(options) {
173
+ return {
174
+ name: "metrics",
175
+ pre: async (context) => {
176
+ options.collector.increment("requests_total", options.labels);
177
+ },
178
+ post: async (context) => {
179
+ const duration = Date.now() - context.startTime;
180
+ options.collector.histogram("request_duration_ms", duration, options.labels);
181
+ options.collector.increment("requests_success_total", options.labels);
182
+ },
183
+ error: async (error, context) => {
184
+ const duration = Date.now() - context.startTime;
185
+ options.collector.histogram("request_duration_ms", duration, options.labels);
186
+ options.collector.increment("requests_error_total", {
187
+ ...options.labels,
188
+ error_type: error.constructor.name
189
+ });
190
+ }
191
+ };
192
+ }
193
+ function createCircuitBreakerMiddleware(options) {
194
+ let state = "CLOSED";
195
+ let failures = 0;
196
+ let nextAttempt = 0;
197
+ return {
198
+ name: "circuit-breaker",
199
+ pre: async (context) => {
200
+ const now = Date.now();
201
+ if (state === "OPEN") {
202
+ if (now < nextAttempt) {
203
+ throw new Error("Circuit breaker is OPEN");
204
+ }
205
+ state = "HALF_OPEN";
206
+ }
207
+ },
208
+ post: async (context) => {
209
+ if (state === "HALF_OPEN") {
210
+ state = "CLOSED";
211
+ failures = 0;
212
+ }
213
+ },
214
+ error: async (error, context) => {
215
+ failures++;
216
+ if (failures >= options.threshold) {
217
+ state = "OPEN";
218
+ nextAttempt = Date.now() + options.resetTimeout;
219
+ }
220
+ throw error;
221
+ }
222
+ };
223
+ }
224
+
225
+ // src/utils/base-plugin.ts
226
+ var BasePlugin = class {
227
+ context;
228
+ middleware = [];
229
+ async initialize(context) {
230
+ this.context = context;
231
+ this.context.logger.info(`Initializing plugin: ${this.metadata.name}`);
232
+ }
233
+ async destroy() {
234
+ this.context.logger.info(`Destroying plugin: ${this.metadata.name}`);
235
+ }
236
+ async executeMiddleware(phase, context, error) {
237
+ for (const middleware of this.middleware) {
238
+ try {
239
+ if (phase === "pre" && middleware.pre) {
240
+ await middleware.pre(context);
241
+ } else if (phase === "post" && middleware.post) {
242
+ await middleware.post(context);
243
+ } else if (phase === "error" && middleware.error && error) {
244
+ await middleware.error(error, context);
245
+ }
246
+ } catch (err) {
247
+ this.context.logger.error(`Middleware ${middleware.name} failed`, err);
248
+ throw err;
249
+ }
250
+ }
251
+ }
252
+ createMiddlewareContext(request, metadata = {}) {
253
+ return {
254
+ request,
255
+ response: void 0,
256
+ metadata: {
257
+ ...metadata,
258
+ pluginName: this.metadata.name,
259
+ pluginVersion: this.metadata.version
260
+ },
261
+ startTime: Date.now()
262
+ };
263
+ }
264
+ validateConfig(config, required) {
265
+ for (const field of required) {
266
+ if (!config[field]) {
267
+ throw new Error(`${this.metadata.name}: Missing required config field: ${field}`);
268
+ }
269
+ }
270
+ }
271
+ async makeRequest(url, options, metadata = {}) {
272
+ const context = this.createMiddlewareContext({ url, options }, metadata);
273
+ try {
274
+ await this.executeMiddleware("pre", context);
275
+ const response = await fetch(url, {
276
+ ...options,
277
+ headers: {
278
+ "User-Agent": `K-OTP-${this.metadata.name}/${this.metadata.version}`,
279
+ ...this.context.config.headers,
280
+ ...options.headers
281
+ },
282
+ signal: AbortSignal.timeout(this.context.config.timeout || 3e4)
283
+ });
284
+ context.response = response;
285
+ await this.executeMiddleware("post", context);
286
+ return response;
287
+ } catch (error) {
288
+ await this.executeMiddleware("error", context, error);
289
+ throw error;
290
+ }
291
+ }
292
+ /**
293
+ * Make HTTP request and parse JSON response
294
+ * Subclasses should use their specific response adapters to transform the result
295
+ */
296
+ async makeJSONRequest(url, options, metadata = {}) {
297
+ const response = await this.makeRequest(url, options, metadata);
298
+ if (!response.ok) {
299
+ const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
300
+ error.response = response;
301
+ error.status = response.status;
302
+ throw error;
303
+ }
304
+ try {
305
+ return await response.json();
306
+ } catch (parseError) {
307
+ const error = new Error("Failed to parse JSON response");
308
+ error.response = response;
309
+ error.parseError = parseError;
310
+ throw error;
311
+ }
312
+ }
313
+ /**
314
+ * Helper method for logging provider-specific operations
315
+ */
316
+ logOperation(operation, data) {
317
+ this.context.logger.info(`${this.metadata.name}: ${operation}`, data);
318
+ }
319
+ /**
320
+ * Helper method for logging provider-specific errors
321
+ */
322
+ logError(operation, error, data) {
323
+ this.context.logger.error(`${this.metadata.name}: ${operation} failed`, { error, data });
324
+ }
325
+ };
326
+
327
+ // src/utils/index.ts
328
+ function normalizePhoneNumber(phone) {
329
+ const cleaned = phone.replace(/[^\d]/g, "");
330
+ if (cleaned.startsWith("82")) {
331
+ return "0" + cleaned.substring(2);
332
+ }
333
+ if (cleaned.startsWith("0")) {
334
+ return cleaned;
335
+ }
336
+ if (cleaned.length >= 10 && cleaned.length <= 11) {
337
+ return "0" + cleaned;
338
+ }
339
+ return cleaned;
340
+ }
341
+ function validatePhoneNumber(phone) {
342
+ const normalized = normalizePhoneNumber(phone);
343
+ return /^01[0-9]{8,9}$/.test(normalized);
344
+ }
345
+ function formatDateTime(date) {
346
+ const year = date.getFullYear();
347
+ const month = String(date.getMonth() + 1).padStart(2, "0");
348
+ const day = String(date.getDate()).padStart(2, "0");
349
+ const hours = String(date.getHours()).padStart(2, "0");
350
+ const minutes = String(date.getMinutes()).padStart(2, "0");
351
+ const seconds = String(date.getSeconds()).padStart(2, "0");
352
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
353
+ }
354
+ function parseTemplate(template, variables) {
355
+ let result = template;
356
+ for (const [key, value] of Object.entries(variables)) {
357
+ const regex = new RegExp(`#{${key}}`, "g");
358
+ result = result.replace(regex, value);
359
+ }
360
+ for (const [key, value] of Object.entries(variables)) {
361
+ const regex = new RegExp(`{{${key}}}`, "g");
362
+ result = result.replace(regex, value);
363
+ }
364
+ return result;
365
+ }
366
+ function extractVariables(template) {
367
+ const variables = /* @__PURE__ */ new Set();
368
+ const hashMatches = template.match(/#\{([^}]+)\}/g);
369
+ if (hashMatches) {
370
+ hashMatches.forEach((match) => {
371
+ const variable = match.slice(2, -1);
372
+ variables.add(variable);
373
+ });
374
+ }
375
+ const braceMatches = template.match(/\{\{([^}]+)\}\}/g);
376
+ if (braceMatches) {
377
+ braceMatches.forEach((match) => {
378
+ const variable = match.slice(2, -2);
379
+ variables.add(variable);
380
+ });
381
+ }
382
+ return Array.from(variables);
383
+ }
384
+ function delay(ms) {
385
+ return new Promise((resolve) => setTimeout(resolve, ms));
386
+ }
387
+ function retry(fn, options) {
388
+ return new Promise(async (resolve, reject) => {
389
+ let lastError;
390
+ for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
391
+ try {
392
+ if (attempt > 0) {
393
+ const delayMs = options.backoff === "exponential" ? options.delay * Math.pow(2, attempt - 1) : options.delay * attempt;
394
+ await delay(delayMs);
395
+ }
396
+ const result = await fn();
397
+ resolve(result);
398
+ return;
399
+ } catch (error) {
400
+ lastError = error;
401
+ if (attempt === options.maxRetries) {
402
+ reject(lastError);
403
+ return;
404
+ }
405
+ }
406
+ }
407
+ });
408
+ }
409
+
410
+ // src/abstract/provider.base.ts
411
+ var BaseAlimTalkProvider = class {
412
+ config = {};
413
+ isConfigured = false;
414
+ constructor(config) {
415
+ if (config) {
416
+ this.configure(config);
417
+ }
418
+ }
419
+ /**
420
+ * Configure the provider with necessary credentials and settings
421
+ */
422
+ configure(config) {
423
+ this.validateConfiguration(config);
424
+ this.config = { ...config };
425
+ this.isConfigured = true;
426
+ this.onConfigured();
427
+ }
428
+ /**
429
+ * Validate the provided configuration
430
+ */
431
+ validateConfiguration(config) {
432
+ const schema = this.getConfigurationSchema();
433
+ for (const field of schema.required) {
434
+ if (!(field.key in config)) {
435
+ throw new Error(`Required configuration field '${field.key}' is missing`);
436
+ }
437
+ this.validateFieldValue(field, config[field.key]);
438
+ }
439
+ for (const field of schema.optional) {
440
+ if (field.key in config) {
441
+ this.validateFieldValue(field, config[field.key]);
442
+ }
443
+ }
444
+ }
445
+ validateFieldValue(field, value) {
446
+ switch (field.type) {
447
+ case "string":
448
+ if (typeof value !== "string") {
449
+ throw new Error(`Field '${field.key}' must be a string`);
450
+ }
451
+ break;
452
+ case "number":
453
+ if (typeof value !== "number") {
454
+ throw new Error(`Field '${field.key}' must be a number`);
455
+ }
456
+ break;
457
+ case "boolean":
458
+ if (typeof value !== "boolean") {
459
+ throw new Error(`Field '${field.key}' must be a boolean`);
460
+ }
461
+ break;
462
+ case "url":
463
+ try {
464
+ new URL(String(value));
465
+ } catch {
466
+ throw new Error(`Field '${field.key}' must be a valid URL`);
467
+ }
468
+ break;
469
+ }
470
+ if (field.validation) {
471
+ if (field.validation.pattern) {
472
+ const regex = new RegExp(field.validation.pattern);
473
+ if (!regex.test(String(value))) {
474
+ throw new Error(`Field '${field.key}' does not match required pattern`);
475
+ }
476
+ }
477
+ if (field.validation.min !== void 0 && Number(value) < field.validation.min) {
478
+ throw new Error(`Field '${field.key}' must be at least ${field.validation.min}`);
479
+ }
480
+ if (field.validation.max !== void 0 && Number(value) > field.validation.max) {
481
+ throw new Error(`Field '${field.key}' must be at most ${field.validation.max}`);
482
+ }
483
+ }
484
+ }
485
+ /**
486
+ * Called after configuration is set
487
+ */
488
+ onConfigured() {
489
+ }
490
+ /**
491
+ * Check if the provider is properly configured
492
+ */
493
+ isReady() {
494
+ return this.isConfigured;
495
+ }
496
+ /**
497
+ * Get configuration value
498
+ */
499
+ getConfig(key) {
500
+ if (!this.isConfigured) {
501
+ throw new Error("Provider is not configured");
502
+ }
503
+ return this.config[key];
504
+ }
505
+ /**
506
+ * Check if a configuration key exists
507
+ */
508
+ hasConfig(key) {
509
+ return key in this.config;
510
+ }
511
+ /**
512
+ * Perform health check on the provider
513
+ */
514
+ async healthCheck() {
515
+ const issues = [];
516
+ const startTime = Date.now();
517
+ try {
518
+ if (!this.isReady()) {
519
+ issues.push("Provider is not configured");
520
+ return { healthy: false, issues };
521
+ }
522
+ await this.testConnectivity();
523
+ await this.testAuthentication();
524
+ const latency = Date.now() - startTime;
525
+ return {
526
+ healthy: issues.length === 0,
527
+ issues,
528
+ latency
529
+ };
530
+ } catch (error) {
531
+ issues.push(`Health check failed: ${error instanceof Error ? error.message : "Unknown error"}`);
532
+ return { healthy: false, issues };
533
+ }
534
+ }
535
+ /**
536
+ * Get provider information
537
+ */
538
+ getInfo() {
539
+ return {
540
+ id: this.id,
541
+ name: this.name,
542
+ version: this.getVersion(),
543
+ capabilities: this.capabilities,
544
+ configured: this.isConfigured
545
+ };
546
+ }
547
+ /**
548
+ * Cleanup resources when provider is destroyed
549
+ */
550
+ destroy() {
551
+ this.config = {};
552
+ this.isConfigured = false;
553
+ this.onDestroy();
554
+ }
555
+ /**
556
+ * Called when provider is being destroyed
557
+ */
558
+ onDestroy() {
559
+ }
560
+ /**
561
+ * Create standardized error
562
+ */
563
+ createError(code, message, details) {
564
+ const error = new Error(message);
565
+ error.code = code;
566
+ error.provider = this.id;
567
+ error.details = details;
568
+ return error;
569
+ }
570
+ /**
571
+ * Log provider activity
572
+ */
573
+ log(level, message, data) {
574
+ const logData = {
575
+ provider: this.id,
576
+ level,
577
+ message,
578
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
579
+ };
580
+ if (data) {
581
+ logData.data = data;
582
+ }
583
+ console.log(JSON.stringify(logData));
584
+ }
585
+ /**
586
+ * Handle rate limiting
587
+ */
588
+ async handleRateLimit(operation) {
589
+ const rateLimit = this.capabilities.messaging.maxRequestsPerSecond;
590
+ if (rateLimit > 0) {
591
+ const delay2 = 1e3 / rateLimit;
592
+ await new Promise((resolve) => setTimeout(resolve, delay2));
593
+ }
594
+ }
595
+ /**
596
+ * Retry mechanism for failed operations
597
+ */
598
+ async withRetry(operation, options = {}) {
599
+ const {
600
+ maxRetries = 3,
601
+ initialDelay = 1e3,
602
+ maxDelay = 1e4,
603
+ backoffFactor = 2
604
+ } = options;
605
+ let lastError;
606
+ let delay2 = initialDelay;
607
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
608
+ try {
609
+ return await operation();
610
+ } catch (error) {
611
+ lastError = error;
612
+ if (attempt === maxRetries) {
613
+ break;
614
+ }
615
+ this.log("warn", `Operation failed, retrying in ${delay2}ms`, {
616
+ attempt: attempt + 1,
617
+ maxRetries,
618
+ error: lastError.message
619
+ });
620
+ await new Promise((resolve) => setTimeout(resolve, delay2));
621
+ delay2 = Math.min(delay2 * backoffFactor, maxDelay);
622
+ }
623
+ }
624
+ throw lastError;
625
+ }
626
+ };
627
+
628
+ // src/adapters/request.adapter.ts
629
+ var BaseRequestAdapter = class {
630
+ /**
631
+ * Common transformation utilities
632
+ */
633
+ formatPhoneNumber(phoneNumber, countryCode = "KR") {
634
+ const digits = phoneNumber.replace(/\D/g, "");
635
+ if (countryCode === "KR") {
636
+ if (digits.startsWith("82")) {
637
+ return digits.substring(2);
638
+ }
639
+ if (digits.startsWith("0")) {
640
+ return digits;
641
+ }
642
+ return "0" + digits;
643
+ }
644
+ return phoneNumber;
645
+ }
646
+ formatVariables(variables) {
647
+ const formatted = {};
648
+ for (const [key, value] of Object.entries(variables)) {
649
+ if (value instanceof Date) {
650
+ formatted[key] = value.toISOString();
651
+ } else if (typeof value === "object") {
652
+ formatted[key] = JSON.stringify(value);
653
+ } else {
654
+ formatted[key] = String(value);
655
+ }
656
+ }
657
+ return formatted;
658
+ }
659
+ validateRequiredFields(data, requiredFields) {
660
+ const obj = data;
661
+ for (const field of requiredFields) {
662
+ if (!(field in obj) || obj[field] === void 0 || obj[field] === null) {
663
+ throw new Error(`Required field '${field}' is missing`);
664
+ }
665
+ }
666
+ }
667
+ };
668
+ var IWINVRequestAdapter = class extends BaseRequestAdapter {
669
+ transformMessageRequest(request) {
670
+ this.validateRequiredFields(request, ["templateCode", "phoneNumber"]);
671
+ return {
672
+ profile_key: this.getProfileKey(),
673
+ template_code: request.templateCode,
674
+ phone_number: this.formatPhoneNumber(request.phoneNumber),
675
+ message_variables: this.formatVariables(request.variables),
676
+ sender_number: request.senderNumber,
677
+ reserve_time: request.options?.scheduledAt ? Math.floor(new Date(request.options.scheduledAt).getTime() / 1e3) : void 0
678
+ };
679
+ }
680
+ transformTemplateRequest(request) {
681
+ this.validateRequiredFields(request, ["name", "content"]);
682
+ return {
683
+ profile_key: this.getProfileKey(),
684
+ template_name: request.name,
685
+ template_content: request.content,
686
+ template_category: this.mapCategory(request.category),
687
+ template_variables: request.variables?.map((v) => ({
688
+ name: v.name,
689
+ type: v.type,
690
+ required: v.required ? "Y" : "N",
691
+ max_length: v.maxLength
692
+ })),
693
+ template_buttons: request.buttons?.map((b) => ({
694
+ type: b.type,
695
+ name: b.name,
696
+ url_mobile: b.linkMobile,
697
+ url_pc: b.linkPc,
698
+ scheme_ios: b.schemeIos,
699
+ scheme_android: b.schemeAndroid
700
+ }))
701
+ };
702
+ }
703
+ getProfileKey() {
704
+ return process.env.IWINV_PROFILE_KEY || "";
705
+ }
706
+ mapCategory(category) {
707
+ const categoryMap = {
708
+ "AUTHENTICATION": "A",
709
+ "NOTIFICATION": "N",
710
+ "PROMOTION": "P",
711
+ "INFORMATION": "I"
712
+ };
713
+ return categoryMap[category] || "I";
714
+ }
715
+ };
716
+ var AligoRequestAdapter = class extends BaseRequestAdapter {
717
+ transformMessageRequest(request) {
718
+ this.validateRequiredFields(request, ["templateCode", "phoneNumber"]);
719
+ return {
720
+ apikey: this.getApiKey(),
721
+ userid: this.getUserId(),
722
+ senderkey: this.getSenderKey(),
723
+ template_code: request.templateCode,
724
+ receiver: this.formatPhoneNumber(request.phoneNumber),
725
+ subject: "AlimTalk",
726
+ message: this.buildMessage(request),
727
+ button: request.variables.buttons ? JSON.stringify(request.variables.buttons) : void 0,
728
+ reservation: request.options?.scheduledAt ? this.formatDateTime(new Date(request.options.scheduledAt)) : void 0
729
+ };
730
+ }
731
+ transformTemplateRequest(request) {
732
+ this.validateRequiredFields(request, ["name", "content"]);
733
+ return {
734
+ apikey: this.getApiKey(),
735
+ userid: this.getUserId(),
736
+ senderkey: this.getSenderKey(),
737
+ template_name: request.name,
738
+ template_content: request.content,
739
+ template_emphasis: this.extractEmphasis(request.content),
740
+ template_extra: this.buildTemplateExtra(request),
741
+ template_ad: this.isPromotional(request.category) ? "Y" : "N"
742
+ };
743
+ }
744
+ getApiKey() {
745
+ return process.env.ALIGO_API_KEY || "";
746
+ }
747
+ getUserId() {
748
+ return process.env.ALIGO_USER_ID || "";
749
+ }
750
+ getSenderKey() {
751
+ return process.env.ALIGO_SENDER_KEY || "";
752
+ }
753
+ buildMessage(request) {
754
+ return request.templateCode;
755
+ }
756
+ formatDateTime(date) {
757
+ return date.toISOString().replace(/[-:]/g, "").replace("T", "").substring(0, 12);
758
+ }
759
+ extractEmphasis(content) {
760
+ const emphasisMatch = content.match(/\*\*(.*?)\*\*/);
761
+ return emphasisMatch ? emphasisMatch[1] : "";
762
+ }
763
+ buildTemplateExtra(request) {
764
+ const extra = {};
765
+ if (request.buttons) {
766
+ extra.buttons = request.buttons;
767
+ }
768
+ if (request.variables) {
769
+ extra.variables = request.variables;
770
+ }
771
+ return JSON.stringify(extra);
772
+ }
773
+ isPromotional(category) {
774
+ return category === "PROMOTION";
775
+ }
776
+ };
777
+ var KakaoRequestAdapter = class extends BaseRequestAdapter {
778
+ transformMessageRequest(request) {
779
+ this.validateRequiredFields(request, ["templateCode", "phoneNumber"]);
780
+ return {
781
+ template_object: {
782
+ object_type: "text",
783
+ text: this.buildTemplateText(request),
784
+ link: this.buildTemplateLink(request),
785
+ button_title: request.variables.buttonTitle || ""
786
+ },
787
+ user_ids: [this.formatPhoneNumber(request.phoneNumber)]
788
+ };
789
+ }
790
+ transformTemplateRequest(request) {
791
+ this.validateRequiredFields(request, ["name", "content"]);
792
+ return {
793
+ template: {
794
+ name: request.name,
795
+ content: request.content,
796
+ category_code: this.mapCategoryCode(request.category),
797
+ template_message_type: "BA",
798
+ // Basic AlimTalk
799
+ template_emphasis_type: this.extractEmphasisType(request.content),
800
+ template_title: request.name,
801
+ template_subtitle: "",
802
+ template_imageurl: "",
803
+ template_header: "",
804
+ template_item_highlight: {
805
+ title: "",
806
+ description: ""
807
+ },
808
+ template_item: {
809
+ list: []
810
+ },
811
+ template_button: this.buildTemplateButtons(request.buttons || [])
812
+ }
813
+ };
814
+ }
815
+ buildTemplateText(request) {
816
+ let text = request.templateCode;
817
+ for (const [key, value] of Object.entries(request.variables)) {
818
+ text = text.replace(new RegExp(`#{${key}}`, "g"), String(value));
819
+ }
820
+ return text;
821
+ }
822
+ buildTemplateLink(request) {
823
+ if (request.variables.linkUrl) {
824
+ return {
825
+ web_url: request.variables.linkUrl,
826
+ mobile_web_url: request.variables.linkUrl
827
+ };
828
+ }
829
+ return {};
830
+ }
831
+ mapCategoryCode(category) {
832
+ const categoryMap = {
833
+ "AUTHENTICATION": "999999",
834
+ "NOTIFICATION": "999998",
835
+ "PROMOTION": "999997",
836
+ "INFORMATION": "999996"
837
+ };
838
+ return categoryMap[category] || "999999";
839
+ }
840
+ extractEmphasisType(content) {
841
+ if (content.includes("**")) return "BOLD";
842
+ if (content.includes("__")) return "UNDERLINE";
843
+ return "NONE";
844
+ }
845
+ buildTemplateButtons(buttons) {
846
+ return buttons.map((button) => {
847
+ const btn = button;
848
+ return {
849
+ name: btn.name,
850
+ type: btn.type,
851
+ url_mobile: btn.linkMobile,
852
+ url_pc: btn.linkPc,
853
+ scheme_ios: btn.schemeIos,
854
+ scheme_android: btn.schemeAndroid
855
+ };
856
+ });
857
+ }
858
+ };
859
+ var RequestAdapterFactory = class _RequestAdapterFactory {
860
+ static adapters = /* @__PURE__ */ new Map();
861
+ static {
862
+ _RequestAdapterFactory.adapters.set("iwinv", IWINVRequestAdapter);
863
+ _RequestAdapterFactory.adapters.set("aligo", AligoRequestAdapter);
864
+ _RequestAdapterFactory.adapters.set("kakao", KakaoRequestAdapter);
865
+ }
866
+ static create(providerId) {
867
+ const AdapterClass = this.adapters.get(providerId.toLowerCase());
868
+ if (!AdapterClass) {
869
+ throw new Error(`No request adapter found for provider: ${providerId}`);
870
+ }
871
+ return new AdapterClass();
872
+ }
873
+ static register(providerId, adapterClass) {
874
+ this.adapters.set(providerId.toLowerCase(), adapterClass);
875
+ }
876
+ };
877
+
878
+ // src/adapters/response.adapter.ts
879
+ var BaseResponseAdapter = class {
880
+ /**
881
+ * Common error transformation
882
+ */
883
+ transformError(providerError) {
884
+ return {
885
+ code: this.extractErrorCode(providerError),
886
+ message: this.extractErrorMessage(providerError),
887
+ details: this.extractErrorDetails(providerError)
888
+ };
889
+ }
890
+ /**
891
+ * Common status mapping utilities
892
+ */
893
+ mapMessageStatus(providerStatus) {
894
+ const statusMap = {
895
+ "queued": "QUEUED" /* QUEUED */,
896
+ "sending": "SENDING" /* SENDING */,
897
+ "sent": "SENT" /* SENT */,
898
+ "delivered": "DELIVERED" /* DELIVERED */,
899
+ "failed": "FAILED" /* FAILED */,
900
+ "cancelled": "CANCELLED" /* CANCELLED */
901
+ };
902
+ return statusMap[providerStatus.toLowerCase()] || "FAILED" /* FAILED */;
903
+ }
904
+ mapTemplateStatus(providerStatus) {
905
+ const statusMap = {
906
+ "draft": "DRAFT" /* DRAFT */,
907
+ "pending": "PENDING" /* PENDING */,
908
+ "approved": "APPROVED" /* APPROVED */,
909
+ "rejected": "REJECTED" /* REJECTED */,
910
+ "disabled": "DISABLED" /* DISABLED */
911
+ };
912
+ return statusMap[providerStatus.toLowerCase()] || "DRAFT" /* DRAFT */;
913
+ }
914
+ parseDate(dateString) {
915
+ if (!dateString) return void 0;
916
+ try {
917
+ return new Date(dateString);
918
+ } catch {
919
+ return void 0;
920
+ }
921
+ }
922
+ };
923
+ var IWINVResponseAdapter = class extends BaseResponseAdapter {
924
+ transformMessageResponse(providerResponse) {
925
+ const response = providerResponse;
926
+ return {
927
+ messageId: response.msg_id || response.msgid,
928
+ status: this.mapIWINVMessageStatus(response.result_code),
929
+ sentAt: this.parseDate(response.send_time),
930
+ error: response.result_code !== "1" ? this.transformError(providerResponse) : void 0
931
+ };
932
+ }
933
+ transformTemplateResponse(providerResponse) {
934
+ const response = providerResponse;
935
+ return {
936
+ templateId: response.template_id,
937
+ providerTemplateCode: response.template_code,
938
+ status: this.mapIWINVTemplateStatus(response.status),
939
+ message: response.message || response.comment
940
+ };
941
+ }
942
+ mapIWINVMessageStatus(resultCode) {
943
+ const statusMap = {
944
+ "1": "SENT" /* SENT */,
945
+ "0": "FAILED" /* FAILED */,
946
+ "-1": "FAILED" /* FAILED */,
947
+ "-2": "FAILED" /* FAILED */,
948
+ "-3": "FAILED" /* FAILED */,
949
+ "-4": "FAILED" /* FAILED */
950
+ };
951
+ return statusMap[resultCode] || "FAILED" /* FAILED */;
952
+ }
953
+ mapIWINVTemplateStatus(status) {
954
+ const statusMap = {
955
+ "R": "PENDING" /* PENDING */,
956
+ // Request
957
+ "A": "APPROVED" /* APPROVED */,
958
+ // Approved
959
+ "C": "REJECTED" /* REJECTED */,
960
+ // Cancelled/Rejected
961
+ "S": "PENDING" /* PENDING */
962
+ // Standby
963
+ };
964
+ return statusMap[status] || "DRAFT" /* DRAFT */;
965
+ }
966
+ extractErrorCode(providerError) {
967
+ const error = providerError;
968
+ return error.result_code || error.error_code || "UNKNOWN_ERROR";
969
+ }
970
+ extractErrorMessage(providerError) {
971
+ const error = providerError;
972
+ return error.message || error.error_message || "Unknown error occurred";
973
+ }
974
+ extractErrorDetails(providerError) {
975
+ const error = providerError;
976
+ return {
977
+ resultCode: error.result_code,
978
+ originalResponse: providerError
979
+ };
980
+ }
981
+ };
982
+ var AligoResponseAdapter = class extends BaseResponseAdapter {
983
+ transformMessageResponse(providerResponse) {
984
+ const response = providerResponse;
985
+ return {
986
+ messageId: response.msg_id || response.mid,
987
+ status: this.mapAligoMessageStatus(response.result_code),
988
+ sentAt: this.parseDate(response.send_time),
989
+ error: response.result_code !== "1" ? this.transformError(providerResponse) : void 0
990
+ };
991
+ }
992
+ transformTemplateResponse(providerResponse) {
993
+ const response = providerResponse;
994
+ return {
995
+ templateId: response.template_code,
996
+ providerTemplateCode: response.template_code,
997
+ status: this.mapAligoTemplateStatus(response.inspect_status),
998
+ message: response.comment
999
+ };
1000
+ }
1001
+ mapAligoMessageStatus(resultCode) {
1002
+ const statusMap = {
1003
+ "1": "SENT" /* SENT */,
1004
+ "0": "FAILED" /* FAILED */,
1005
+ "-1": "FAILED" /* FAILED */,
1006
+ "-101": "FAILED" /* FAILED */,
1007
+ "-102": "FAILED" /* FAILED */
1008
+ };
1009
+ return statusMap[resultCode] || "FAILED" /* FAILED */;
1010
+ }
1011
+ mapAligoTemplateStatus(inspectStatus) {
1012
+ const statusMap = {
1013
+ "REG": "PENDING" /* PENDING */,
1014
+ // Registered
1015
+ "REQ": "PENDING" /* PENDING */,
1016
+ // Request
1017
+ "APR": "APPROVED" /* APPROVED */,
1018
+ // Approved
1019
+ "REJ": "REJECTED" /* REJECTED */,
1020
+ // Rejected
1021
+ "STOP": "DISABLED" /* DISABLED */
1022
+ // Stopped
1023
+ };
1024
+ return statusMap[inspectStatus] || "DRAFT" /* DRAFT */;
1025
+ }
1026
+ extractErrorCode(providerError) {
1027
+ const error = providerError;
1028
+ return error.result_code || error.code || "UNKNOWN_ERROR";
1029
+ }
1030
+ extractErrorMessage(providerError) {
1031
+ const error = providerError;
1032
+ return error.message || error.error || "Unknown error occurred";
1033
+ }
1034
+ extractErrorDetails(providerError) {
1035
+ const error = providerError;
1036
+ return {
1037
+ resultCode: error.result_code,
1038
+ inspectStatus: error.inspect_status,
1039
+ originalResponse: providerError
1040
+ };
1041
+ }
1042
+ };
1043
+ var KakaoResponseAdapter = class extends BaseResponseAdapter {
1044
+ transformMessageResponse(providerResponse) {
1045
+ const response = providerResponse;
1046
+ return {
1047
+ messageId: response.message_id,
1048
+ status: this.mapKakaoMessageStatus(response.result_code),
1049
+ sentAt: this.parseDate(response.sent_time),
1050
+ error: response.result_code !== 0 ? this.transformError(providerResponse) : void 0
1051
+ };
1052
+ }
1053
+ transformTemplateResponse(providerResponse) {
1054
+ const response = providerResponse;
1055
+ return {
1056
+ templateId: response.template_id,
1057
+ providerTemplateCode: response.template_code,
1058
+ status: this.mapKakaoTemplateStatus(response.status),
1059
+ message: response.comments
1060
+ };
1061
+ }
1062
+ mapKakaoMessageStatus(resultCode) {
1063
+ const statusMap = {
1064
+ 0: "SENT" /* SENT */,
1065
+ [-1]: "FAILED" /* FAILED */,
1066
+ [-2]: "FAILED" /* FAILED */,
1067
+ [-3]: "FAILED" /* FAILED */,
1068
+ [-999]: "FAILED" /* FAILED */
1069
+ };
1070
+ return statusMap[resultCode] || "FAILED" /* FAILED */;
1071
+ }
1072
+ mapKakaoTemplateStatus(status) {
1073
+ const statusMap = {
1074
+ "TSC01": "PENDING" /* PENDING */,
1075
+ // Under review
1076
+ "TSC02": "APPROVED" /* APPROVED */,
1077
+ // Approved
1078
+ "TSC03": "REJECTED" /* REJECTED */,
1079
+ // Rejected
1080
+ "TSC04": "DISABLED" /* DISABLED */
1081
+ // Disabled
1082
+ };
1083
+ return statusMap[status] || "DRAFT" /* DRAFT */;
1084
+ }
1085
+ extractErrorCode(providerError) {
1086
+ const error = providerError;
1087
+ return String(error.result_code || error.error_code || "UNKNOWN_ERROR");
1088
+ }
1089
+ extractErrorMessage(providerError) {
1090
+ const error = providerError;
1091
+ return error.message || error.error_message || "Unknown error occurred";
1092
+ }
1093
+ extractErrorDetails(providerError) {
1094
+ const error = providerError;
1095
+ return {
1096
+ resultCode: error.result_code,
1097
+ originalResponse: providerError
1098
+ };
1099
+ }
1100
+ };
1101
+ var NHNResponseAdapter = class extends BaseResponseAdapter {
1102
+ transformMessageResponse(providerResponse) {
1103
+ const response = providerResponse;
1104
+ return {
1105
+ messageId: response.requestId,
1106
+ status: this.mapNHNMessageStatus(response.statusCode),
1107
+ sentAt: this.parseDate(response.statusDateTime),
1108
+ error: response.statusCode !== "SSS" ? this.transformError(providerResponse) : void 0
1109
+ };
1110
+ }
1111
+ transformTemplateResponse(providerResponse) {
1112
+ const response = providerResponse;
1113
+ return {
1114
+ templateId: response.templateId,
1115
+ providerTemplateCode: response.templateId,
1116
+ status: this.mapNHNTemplateStatus(response.templateStatus),
1117
+ message: response.templateStatusName
1118
+ };
1119
+ }
1120
+ mapNHNMessageStatus(statusCode) {
1121
+ const statusMap = {
1122
+ "SSS": "SENT" /* SENT */,
1123
+ // Success
1124
+ "RDY": "QUEUED" /* QUEUED */,
1125
+ // Ready
1126
+ "PRG": "SENDING" /* SENDING */,
1127
+ // Progress
1128
+ "CPL": "DELIVERED" /* DELIVERED */,
1129
+ // Complete
1130
+ "FAL": "FAILED" /* FAILED */,
1131
+ // Failed
1132
+ "CAL": "CANCELLED" /* CANCELLED */
1133
+ // Cancelled
1134
+ };
1135
+ return statusMap[statusCode] || "FAILED" /* FAILED */;
1136
+ }
1137
+ mapNHNTemplateStatus(templateStatus) {
1138
+ const statusMap = {
1139
+ "WAITING": "PENDING" /* PENDING */,
1140
+ "APPROVED": "APPROVED" /* APPROVED */,
1141
+ "REJECTED": "REJECTED" /* REJECTED */,
1142
+ "DISABLED": "DISABLED" /* DISABLED */
1143
+ };
1144
+ return statusMap[templateStatus] || "DRAFT" /* DRAFT */;
1145
+ }
1146
+ extractErrorCode(providerError) {
1147
+ const error = providerError;
1148
+ return error.statusCode || error.errorCode || "UNKNOWN_ERROR";
1149
+ }
1150
+ extractErrorMessage(providerError) {
1151
+ const error = providerError;
1152
+ return error.statusMessage || error.errorMessage || "Unknown error occurred";
1153
+ }
1154
+ extractErrorDetails(providerError) {
1155
+ const error = providerError;
1156
+ return {
1157
+ statusCode: error.statusCode,
1158
+ statusMessage: error.statusMessage,
1159
+ originalResponse: providerError
1160
+ };
1161
+ }
1162
+ };
1163
+ var ResponseAdapterFactory = class {
1164
+ static adapters = /* @__PURE__ */ new Map([
1165
+ ["iwinv", IWINVResponseAdapter],
1166
+ ["aligo", AligoResponseAdapter],
1167
+ ["kakao", KakaoResponseAdapter],
1168
+ ["nhn", NHNResponseAdapter]
1169
+ ]);
1170
+ static create(providerId) {
1171
+ const AdapterClass = this.adapters.get(providerId.toLowerCase());
1172
+ if (!AdapterClass) {
1173
+ throw new Error(`No response adapter found for provider: ${providerId}`);
1174
+ }
1175
+ return new AdapterClass();
1176
+ }
1177
+ static register(providerId, adapterClass) {
1178
+ this.adapters.set(providerId.toLowerCase(), adapterClass);
1179
+ }
1180
+ };
1181
+
1182
+ // src/services/provider.manager.ts
1183
+ var ProviderManager = class {
1184
+ providers = /* @__PURE__ */ new Map();
1185
+ defaultProvider;
1186
+ registerProvider(provider) {
1187
+ this.providers.set(provider.id, provider);
1188
+ if (!this.defaultProvider) {
1189
+ this.defaultProvider = provider.id;
1190
+ }
1191
+ }
1192
+ unregisterProvider(providerId) {
1193
+ this.providers.delete(providerId);
1194
+ if (this.defaultProvider === providerId) {
1195
+ const remaining = Array.from(this.providers.keys());
1196
+ this.defaultProvider = remaining.length > 0 ? remaining[0] : void 0;
1197
+ }
1198
+ }
1199
+ getProvider(providerId) {
1200
+ const id = providerId || this.defaultProvider;
1201
+ return id ? this.providers.get(id) || null : null;
1202
+ }
1203
+ listProviders() {
1204
+ return Array.from(this.providers.values());
1205
+ }
1206
+ setDefaultProvider(providerId) {
1207
+ if (!this.providers.has(providerId)) {
1208
+ throw new Error(`Provider ${providerId} not found`);
1209
+ }
1210
+ this.defaultProvider = providerId;
1211
+ }
1212
+ async healthCheckAll() {
1213
+ const results = {};
1214
+ for (const [id, provider] of this.providers.entries()) {
1215
+ try {
1216
+ const health = await provider.healthCheck();
1217
+ results[id] = health.healthy;
1218
+ } catch (error) {
1219
+ results[id] = false;
1220
+ }
1221
+ }
1222
+ return results;
1223
+ }
1224
+ getProvidersForChannel(channel) {
1225
+ return Array.from(this.providers.values()).filter((provider) => {
1226
+ const providerWithChannels = provider;
1227
+ return providerWithChannels.supportedChannels?.includes(channel);
1228
+ });
1229
+ }
1230
+ };
1231
+
1232
+ // src/iwinv/contracts/messaging.contract.ts
1233
+ var IWINVMessagingContract = class {
1234
+ constructor(config) {
1235
+ this.config = config;
1236
+ }
1237
+ async send(message) {
1238
+ try {
1239
+ const response = await fetch(`${this.config.baseUrl}/send`, {
1240
+ method: "POST",
1241
+ headers: {
1242
+ "Content-Type": "application/json",
1243
+ "Authorization": `Bearer ${this.config.apiKey}`
1244
+ },
1245
+ body: JSON.stringify({
1246
+ templateCode: message.templateCode,
1247
+ phone: message.phoneNumber,
1248
+ variables: message.variables,
1249
+ senderNumber: message.senderNumber,
1250
+ ...message.options
1251
+ })
1252
+ });
1253
+ const result = await response.json();
1254
+ if (!response.ok) {
1255
+ return {
1256
+ messageId: `failed_${Date.now()}`,
1257
+ status: "FAILED" /* FAILED */,
1258
+ error: {
1259
+ code: result.code || "SEND_FAILED",
1260
+ message: result.message || "Failed to send message"
1261
+ }
1262
+ };
1263
+ }
1264
+ return {
1265
+ messageId: result.messageId || `msg_${Date.now()}`,
1266
+ status: "SENT" /* SENT */,
1267
+ sentAt: /* @__PURE__ */ new Date()
1268
+ };
1269
+ } catch (error) {
1270
+ return {
1271
+ messageId: `error_${Date.now()}`,
1272
+ status: "FAILED" /* FAILED */,
1273
+ error: {
1274
+ code: "NETWORK_ERROR",
1275
+ message: error instanceof Error ? error.message : "Network error occurred"
1276
+ }
1277
+ };
1278
+ }
1279
+ }
1280
+ async sendBulk(messages) {
1281
+ const results = [];
1282
+ for (const message of messages) {
1283
+ const result = await this.send(message);
1284
+ results.push(result);
1285
+ }
1286
+ const sent = results.filter((r) => r.status === "SENT" /* SENT */).length;
1287
+ const failed = results.filter((r) => r.status === "FAILED" /* FAILED */).length;
1288
+ return {
1289
+ requestId: `bulk_${Date.now()}`,
1290
+ results,
1291
+ summary: {
1292
+ total: messages.length,
1293
+ sent,
1294
+ failed
1295
+ }
1296
+ };
1297
+ }
1298
+ async schedule(message, scheduledAt) {
1299
+ return {
1300
+ scheduleId: `schedule_${Date.now()}`,
1301
+ messageId: `msg_${Date.now()}`,
1302
+ scheduledAt,
1303
+ status: "scheduled"
1304
+ };
1305
+ }
1306
+ async cancel(messageId) {
1307
+ const response = await fetch(`${this.config.baseUrl}/cancel`, {
1308
+ method: "POST",
1309
+ headers: {
1310
+ "Content-Type": "application/json",
1311
+ "Authorization": `Bearer ${this.config.apiKey}`
1312
+ },
1313
+ body: JSON.stringify({ messageId })
1314
+ });
1315
+ if (!response.ok) {
1316
+ const result = await response.json();
1317
+ throw new Error(`Failed to cancel message: ${result.message}`);
1318
+ }
1319
+ }
1320
+ async getStatus(messageId) {
1321
+ try {
1322
+ const response = await fetch(`${this.config.baseUrl}/status`, {
1323
+ method: "POST",
1324
+ headers: {
1325
+ "Content-Type": "application/json",
1326
+ "Authorization": `Bearer ${this.config.apiKey}`
1327
+ },
1328
+ body: JSON.stringify({ messageId })
1329
+ });
1330
+ const result = await response.json();
1331
+ if (!response.ok) {
1332
+ return "FAILED" /* FAILED */;
1333
+ }
1334
+ switch (result.statusCode) {
1335
+ case "OK":
1336
+ return "DELIVERED" /* DELIVERED */;
1337
+ case "PENDING":
1338
+ return "SENDING" /* SENDING */;
1339
+ case "FAILED":
1340
+ return "FAILED" /* FAILED */;
1341
+ default:
1342
+ return "SENT" /* SENT */;
1343
+ }
1344
+ } catch (error) {
1345
+ return "FAILED" /* FAILED */;
1346
+ }
1347
+ }
1348
+ };
1349
+
1350
+ // src/iwinv/contracts/template.contract.ts
1351
+ var IWINVTemplateContract = class {
1352
+ constructor(config) {
1353
+ this.config = config;
1354
+ }
1355
+ async create(template) {
1356
+ try {
1357
+ const response = await fetch(`${this.config.baseUrl}/template/create`, {
1358
+ method: "POST",
1359
+ headers: {
1360
+ "Content-Type": "application/json",
1361
+ "Authorization": `Bearer ${this.config.apiKey}`
1362
+ },
1363
+ body: JSON.stringify({
1364
+ templateName: template.name,
1365
+ templateContent: template.content,
1366
+ templateCategory: template.category,
1367
+ templateVariables: template.variables,
1368
+ templateButtons: template.buttons
1369
+ })
1370
+ });
1371
+ const result = await response.json();
1372
+ if (!response.ok) {
1373
+ throw new Error(`Template creation failed: ${result.message}`);
1374
+ }
1375
+ return {
1376
+ templateId: result.templateId || `tpl_${Date.now()}`,
1377
+ providerTemplateCode: result.templateCode || template.name,
1378
+ status: "PENDING" /* PENDING */,
1379
+ message: result.message || "Template created successfully"
1380
+ };
1381
+ } catch (error) {
1382
+ throw new Error(`Failed to create template: ${error instanceof Error ? error.message : "Unknown error"}`);
1383
+ }
1384
+ }
1385
+ async update(templateId, template) {
1386
+ try {
1387
+ const response = await fetch(`${this.config.baseUrl}/template/modify`, {
1388
+ method: "POST",
1389
+ headers: {
1390
+ "Content-Type": "application/json",
1391
+ "Authorization": `Bearer ${this.config.apiKey}`
1392
+ },
1393
+ body: JSON.stringify({
1394
+ templateCode: templateId,
1395
+ templateName: template.name,
1396
+ templateContent: template.content,
1397
+ templateButtons: template.buttons
1398
+ })
1399
+ });
1400
+ const result = await response.json();
1401
+ if (!response.ok) {
1402
+ throw new Error(`Template update failed: ${result.message}`);
1403
+ }
1404
+ return {
1405
+ templateId,
1406
+ status: "PENDING" /* PENDING */,
1407
+ message: result.message || "Template updated successfully"
1408
+ };
1409
+ } catch (error) {
1410
+ throw new Error(`Failed to update template: ${error instanceof Error ? error.message : "Unknown error"}`);
1411
+ }
1412
+ }
1413
+ async delete(templateId) {
1414
+ try {
1415
+ const response = await fetch(`${this.config.baseUrl}/template/delete`, {
1416
+ method: "POST",
1417
+ headers: {
1418
+ "Content-Type": "application/json",
1419
+ "Authorization": `Bearer ${this.config.apiKey}`
1420
+ },
1421
+ body: JSON.stringify({
1422
+ templateCode: templateId
1423
+ })
1424
+ });
1425
+ const result = await response.json();
1426
+ if (!response.ok) {
1427
+ throw new Error(`Template deletion failed: ${result.message}`);
1428
+ }
1429
+ } catch (error) {
1430
+ throw new Error(`Failed to delete template: ${error instanceof Error ? error.message : "Unknown error"}`);
1431
+ }
1432
+ }
1433
+ async get(templateId) {
1434
+ const templates = await this.list({ templateCode: templateId });
1435
+ const template = templates.find((t) => t.code === templateId);
1436
+ if (!template) {
1437
+ throw new Error(`Template ${templateId} not found`);
1438
+ }
1439
+ return template;
1440
+ }
1441
+ async list(filters) {
1442
+ try {
1443
+ const response = await fetch(`${this.config.baseUrl}/template/list`, {
1444
+ method: "POST",
1445
+ headers: {
1446
+ "Content-Type": "application/json",
1447
+ "Authorization": `Bearer ${this.config.apiKey}`
1448
+ },
1449
+ body: JSON.stringify({
1450
+ page: 1,
1451
+ size: 100,
1452
+ ...filters
1453
+ })
1454
+ });
1455
+ const result = await response.json();
1456
+ if (!response.ok) {
1457
+ throw new Error(`Failed to list templates: ${result.message}`);
1458
+ }
1459
+ return (result.list || []).map((template) => ({
1460
+ id: template.templateId || template.templateCode,
1461
+ code: template.templateCode,
1462
+ name: template.templateName,
1463
+ content: template.templateContent,
1464
+ status: this.mapIWINVStatus(template.status || template.templateStatus),
1465
+ createdAt: template.createDate ? new Date(template.createDate) : /* @__PURE__ */ new Date(),
1466
+ updatedAt: template.updateDate ? new Date(template.updateDate) : /* @__PURE__ */ new Date(),
1467
+ approvedAt: template.approvedAt ? new Date(template.approvedAt) : void 0,
1468
+ rejectedAt: template.rejectedAt ? new Date(template.rejectedAt) : void 0,
1469
+ rejectionReason: template.rejectionReason
1470
+ }));
1471
+ } catch (error) {
1472
+ throw new Error(`Failed to list templates: ${error instanceof Error ? error.message : "Unknown error"}`);
1473
+ }
1474
+ }
1475
+ async sync() {
1476
+ try {
1477
+ const templates = await this.list();
1478
+ return {
1479
+ synced: templates.length,
1480
+ created: 0,
1481
+ updated: 0,
1482
+ deleted: 0,
1483
+ errors: []
1484
+ };
1485
+ } catch (error) {
1486
+ return {
1487
+ synced: 0,
1488
+ created: 0,
1489
+ updated: 0,
1490
+ deleted: 0,
1491
+ errors: [{
1492
+ templateId: "unknown",
1493
+ error: error instanceof Error ? error.message : "Sync failed"
1494
+ }]
1495
+ };
1496
+ }
1497
+ }
1498
+ mapIWINVStatus(status) {
1499
+ switch (status) {
1500
+ case "Y":
1501
+ return "APPROVED" /* APPROVED */;
1502
+ case "I":
1503
+ return "PENDING" /* PENDING */;
1504
+ case "R":
1505
+ return "REJECTED" /* REJECTED */;
1506
+ case "D":
1507
+ return "DISABLED" /* DISABLED */;
1508
+ default:
1509
+ return "DRAFT" /* DRAFT */;
1510
+ }
1511
+ }
1512
+ };
1513
+
1514
+ // src/iwinv/contracts/channel.contract.ts
1515
+ var IWINVChannelContract = class {
1516
+ constructor(config) {
1517
+ this.config = config;
1518
+ }
1519
+ async register(channel) {
1520
+ return {
1521
+ id: `channel_${Date.now()}`,
1522
+ name: channel.name,
1523
+ profileKey: channel.profileKey,
1524
+ status: "pending",
1525
+ createdAt: /* @__PURE__ */ new Date(),
1526
+ updatedAt: /* @__PURE__ */ new Date()
1527
+ };
1528
+ }
1529
+ async list() {
1530
+ return [
1531
+ {
1532
+ id: "iwinv-default",
1533
+ name: "IWINV Default Channel",
1534
+ profileKey: "default",
1535
+ status: "active",
1536
+ createdAt: /* @__PURE__ */ new Date(),
1537
+ updatedAt: /* @__PURE__ */ new Date()
1538
+ }
1539
+ ];
1540
+ }
1541
+ async addSenderNumber(channelId, number) {
1542
+ return {
1543
+ id: `sender_${Date.now()}`,
1544
+ channelId,
1545
+ phoneNumber: number,
1546
+ isVerified: false,
1547
+ createdAt: /* @__PURE__ */ new Date()
1548
+ };
1549
+ }
1550
+ async verifySenderNumber(number, verificationCode) {
1551
+ return true;
1552
+ }
1553
+ };
1554
+
1555
+ // src/iwinv/contracts/analytics.contract.ts
1556
+ var IWINVAnalyticsContract = class {
1557
+ constructor(config) {
1558
+ this.config = config;
1559
+ }
1560
+ async getUsage(period) {
1561
+ try {
1562
+ const response = await fetch(`${this.config.baseUrl}/history/list`, {
1563
+ method: "POST",
1564
+ headers: {
1565
+ "Content-Type": "application/json",
1566
+ "Authorization": `Bearer ${this.config.apiKey}`
1567
+ },
1568
+ body: JSON.stringify({
1569
+ startDate: period.from.toISOString(),
1570
+ endDate: period.to.toISOString(),
1571
+ page: 1,
1572
+ size: 1e3
1573
+ })
1574
+ });
1575
+ const result = await response.json();
1576
+ if (!response.ok) {
1577
+ throw new Error(`Failed to get usage stats: ${result.message}`);
1578
+ }
1579
+ const messages = result.list || [];
1580
+ const totalMessages = messages.length;
1581
+ const deliveredMessages = messages.filter((msg) => msg.statusCode === "OK").length;
1582
+ const failedMessages = messages.filter((msg) => msg.statusCode === "FAILED").length;
1583
+ const sentMessages = totalMessages - failedMessages;
1584
+ return {
1585
+ period,
1586
+ totalMessages,
1587
+ sentMessages,
1588
+ deliveredMessages,
1589
+ failedMessages,
1590
+ deliveryRate: totalMessages > 0 ? deliveredMessages / totalMessages * 100 : 0,
1591
+ failureRate: totalMessages > 0 ? failedMessages / totalMessages * 100 : 0,
1592
+ breakdown: {
1593
+ byTemplate: this.groupByTemplate(messages),
1594
+ byDay: this.groupByDay(messages, period),
1595
+ byHour: this.groupByHour(messages)
1596
+ }
1597
+ };
1598
+ } catch (error) {
1599
+ return {
1600
+ period,
1601
+ totalMessages: 0,
1602
+ sentMessages: 0,
1603
+ deliveredMessages: 0,
1604
+ failedMessages: 0,
1605
+ deliveryRate: 0,
1606
+ failureRate: 0,
1607
+ breakdown: {
1608
+ byTemplate: {},
1609
+ byDay: {},
1610
+ byHour: {}
1611
+ }
1612
+ };
1613
+ }
1614
+ }
1615
+ async getTemplateStats(templateId, period) {
1616
+ try {
1617
+ const usage = await this.getUsage(period);
1618
+ const templateMessages = usage.breakdown.byTemplate[templateId] || 0;
1619
+ return {
1620
+ templateId,
1621
+ period,
1622
+ totalSent: templateMessages,
1623
+ delivered: Math.round(templateMessages * (usage.deliveryRate / 100)),
1624
+ failed: Math.round(templateMessages * (usage.failureRate / 100)),
1625
+ deliveryRate: usage.deliveryRate,
1626
+ averageDeliveryTime: 30
1627
+ // Mock average delivery time in seconds
1628
+ };
1629
+ } catch (error) {
1630
+ return {
1631
+ templateId,
1632
+ period,
1633
+ totalSent: 0,
1634
+ delivered: 0,
1635
+ failed: 0,
1636
+ deliveryRate: 0,
1637
+ averageDeliveryTime: 0
1638
+ };
1639
+ }
1640
+ }
1641
+ async getDeliveryReport(messageId) {
1642
+ try {
1643
+ const response = await fetch(`${this.config.baseUrl}/history/detail`, {
1644
+ method: "POST",
1645
+ headers: {
1646
+ "Content-Type": "application/json",
1647
+ "Authorization": `Bearer ${this.config.apiKey}`
1648
+ },
1649
+ body: JSON.stringify({
1650
+ messageId: parseInt(messageId) || 0
1651
+ })
1652
+ });
1653
+ const result = await response.json();
1654
+ if (!response.ok) {
1655
+ throw new Error(`Failed to get delivery report: ${result.message}`);
1656
+ }
1657
+ return {
1658
+ messageId,
1659
+ phoneNumber: result.phone || "unknown",
1660
+ templateCode: result.templateCode || "unknown",
1661
+ status: this.mapStatus(result.statusCode),
1662
+ sentAt: result.sendDate ? new Date(result.sendDate) : void 0,
1663
+ deliveredAt: result.receiveDate ? new Date(result.receiveDate) : void 0,
1664
+ failedAt: result.statusCode === "FAILED" ? new Date(result.sendDate) : void 0,
1665
+ clickedAt: result.clickedAt ? new Date(result.clickedAt) : void 0,
1666
+ error: result.statusCode !== "OK" ? {
1667
+ code: result.statusCode,
1668
+ message: result.statusCodeName
1669
+ } : void 0,
1670
+ attempts: [
1671
+ {
1672
+ attemptNumber: 1,
1673
+ attemptedAt: new Date(result.requestDate),
1674
+ status: this.mapStatus(result.statusCode),
1675
+ error: result.statusCode !== "OK" ? {
1676
+ code: result.statusCode,
1677
+ message: result.statusCodeName
1678
+ } : void 0
1679
+ }
1680
+ ]
1681
+ };
1682
+ } catch (error) {
1683
+ return {
1684
+ messageId,
1685
+ phoneNumber: "unknown",
1686
+ templateCode: "unknown",
1687
+ status: "FAILED",
1688
+ error: {
1689
+ code: "API_ERROR",
1690
+ message: error instanceof Error ? error.message : "Unknown error"
1691
+ },
1692
+ attempts: []
1693
+ };
1694
+ }
1695
+ }
1696
+ groupByTemplate(messages) {
1697
+ const groups = {};
1698
+ messages.forEach((msg) => {
1699
+ const template = msg.templateCode || "unknown";
1700
+ groups[template] = (groups[template] || 0) + 1;
1701
+ });
1702
+ return groups;
1703
+ }
1704
+ groupByDay(messages, period) {
1705
+ const groups = {};
1706
+ const current = new Date(period.from);
1707
+ while (current <= period.to) {
1708
+ const dateKey = current.toISOString().split("T")[0];
1709
+ groups[dateKey] = 0;
1710
+ current.setDate(current.getDate() + 1);
1711
+ }
1712
+ messages.forEach((msg) => {
1713
+ if (msg.requestDate) {
1714
+ const dateKey = new Date(msg.requestDate).toISOString().split("T")[0];
1715
+ if (groups.hasOwnProperty(dateKey)) {
1716
+ groups[dateKey]++;
1717
+ }
1718
+ }
1719
+ });
1720
+ return groups;
1721
+ }
1722
+ groupByHour(messages) {
1723
+ const groups = {};
1724
+ for (let i = 0; i < 24; i++) {
1725
+ groups[i.toString()] = 0;
1726
+ }
1727
+ messages.forEach((msg) => {
1728
+ if (msg.requestDate) {
1729
+ const hour = new Date(msg.requestDate).getHours();
1730
+ groups[hour.toString()]++;
1731
+ }
1732
+ });
1733
+ return groups;
1734
+ }
1735
+ mapStatus(statusCode) {
1736
+ switch (statusCode) {
1737
+ case "OK":
1738
+ return "DELIVERED";
1739
+ case "PENDING":
1740
+ return "SENDING";
1741
+ case "FAILED":
1742
+ return "FAILED";
1743
+ default:
1744
+ return "SENT";
1745
+ }
1746
+ }
1747
+ };
1748
+
1749
+ // src/iwinv/contracts/account.contract.ts
1750
+ var IWINVAccountContract = class {
1751
+ constructor(config) {
1752
+ this.config = config;
1753
+ }
1754
+ async getBalance() {
1755
+ try {
1756
+ const response = await fetch(`${this.config.baseUrl}/balance`, {
1757
+ method: "GET",
1758
+ headers: {
1759
+ "Authorization": `Bearer ${this.config.apiKey}`
1760
+ }
1761
+ });
1762
+ const result = await response.json();
1763
+ if (!response.ok) {
1764
+ throw new Error(`Failed to get balance: ${result.message}`);
1765
+ }
1766
+ return {
1767
+ current: Number(result.balance) || 0,
1768
+ currency: "KRW",
1769
+ lastUpdated: /* @__PURE__ */ new Date(),
1770
+ threshold: Number(result.threshold) || void 0
1771
+ };
1772
+ } catch (error) {
1773
+ return {
1774
+ current: 0,
1775
+ currency: "KRW",
1776
+ lastUpdated: /* @__PURE__ */ new Date()
1777
+ };
1778
+ }
1779
+ }
1780
+ async getProfile() {
1781
+ try {
1782
+ const balance = await this.getBalance();
1783
+ return {
1784
+ accountId: "iwinv-account",
1785
+ name: "IWINV Account",
1786
+ email: "account@iwinv.kr",
1787
+ phone: "1588-1234",
1788
+ status: balance.current > 0 ? "active" : "suspended",
1789
+ tier: "standard",
1790
+ features: ["alimtalk", "sms", "lms"],
1791
+ limits: {
1792
+ dailyMessageLimit: 1e4,
1793
+ monthlyMessageLimit: 3e5,
1794
+ rateLimit: 100
1795
+ // per second
1796
+ }
1797
+ };
1798
+ } catch (error) {
1799
+ return {
1800
+ accountId: "iwinv-account",
1801
+ name: "IWINV Account",
1802
+ email: "account@iwinv.kr",
1803
+ phone: "1588-1234",
1804
+ status: "active",
1805
+ tier: "basic",
1806
+ features: ["alimtalk"],
1807
+ limits: {
1808
+ dailyMessageLimit: 1e3,
1809
+ monthlyMessageLimit: 3e4,
1810
+ rateLimit: 10
1811
+ }
1812
+ };
1813
+ }
1814
+ }
1815
+ };
1816
+
1817
+ // src/iwinv/provider.ts
1818
+ var IWINVProvider = class extends BaseAlimTalkProvider {
1819
+ id = "iwinv";
1820
+ name = "IWINV AlimTalk Provider";
1821
+ capabilities = {
1822
+ templates: {
1823
+ maxLength: 1e3,
1824
+ maxVariables: 20,
1825
+ maxButtons: 5,
1826
+ supportedButtonTypes: ["WL", "AL", "DB", "BK", "MD"],
1827
+ requiresApproval: true,
1828
+ approvalTime: "1-2 days"
1829
+ },
1830
+ messaging: {
1831
+ maxRecipientsPerRequest: 1,
1832
+ maxRequestsPerSecond: 100,
1833
+ supportsBulk: false,
1834
+ // IWINV doesn't have native bulk API
1835
+ supportsScheduling: true,
1836
+ maxScheduleDays: 30,
1837
+ supportsFallback: true
1838
+ },
1839
+ channels: {
1840
+ requiresBusinessVerification: true,
1841
+ maxSenderNumbers: 10,
1842
+ supportsMultipleChannels: false
1843
+ }
1844
+ };
1845
+ // Contract implementations
1846
+ templates;
1847
+ channels;
1848
+ messaging;
1849
+ analytics;
1850
+ account;
1851
+ constructor(config) {
1852
+ super(config);
1853
+ const iwinvConfig = this.getIWINVConfig();
1854
+ this.templates = new IWINVTemplateContract(iwinvConfig);
1855
+ this.channels = new IWINVChannelContract(iwinvConfig);
1856
+ this.messaging = new IWINVMessagingContract(iwinvConfig);
1857
+ this.analytics = new IWINVAnalyticsContract(iwinvConfig);
1858
+ this.account = new IWINVAccountContract(iwinvConfig);
1859
+ }
1860
+ getConfigurationSchema() {
1861
+ return {
1862
+ required: [
1863
+ {
1864
+ key: "apiKey",
1865
+ name: "IWINV API Key",
1866
+ type: "password",
1867
+ description: "Your IWINV API key",
1868
+ required: true,
1869
+ validation: {
1870
+ pattern: "^[a-zA-Z0-9-_]+$",
1871
+ min: 10
1872
+ }
1873
+ }
1874
+ ],
1875
+ optional: [
1876
+ {
1877
+ key: "baseUrl",
1878
+ name: "Base URL",
1879
+ type: "url",
1880
+ description: "IWINV API base URL",
1881
+ required: false,
1882
+ default: "https://alimtalk.bizservice.iwinv.kr",
1883
+ validation: {
1884
+ pattern: "^https?://.+"
1885
+ }
1886
+ },
1887
+ {
1888
+ key: "debug",
1889
+ name: "Debug Mode",
1890
+ type: "boolean",
1891
+ description: "Enable debug logging",
1892
+ required: false,
1893
+ default: false
1894
+ }
1895
+ ]
1896
+ };
1897
+ }
1898
+ async testConnectivity() {
1899
+ const config = this.getIWINVConfig();
1900
+ try {
1901
+ const response = await fetch(`${config.baseUrl}/balance`, {
1902
+ method: "GET",
1903
+ headers: {
1904
+ "Authorization": `Bearer ${config.apiKey}`
1905
+ }
1906
+ });
1907
+ if (!response.ok) {
1908
+ throw new Error(`Connectivity test failed: ${response.status} ${response.statusText}`);
1909
+ }
1910
+ } catch (error) {
1911
+ throw new Error(`Cannot connect to IWINV API: ${error instanceof Error ? error.message : "Unknown error"}`);
1912
+ }
1913
+ }
1914
+ async testAuthentication() {
1915
+ const config = this.getIWINVConfig();
1916
+ try {
1917
+ const response = await fetch(`${config.baseUrl}/balance`, {
1918
+ method: "GET",
1919
+ headers: {
1920
+ "Authorization": `Bearer ${config.apiKey}`
1921
+ }
1922
+ });
1923
+ if (response.status === 401 || response.status === 403) {
1924
+ throw new Error("Invalid API key or insufficient permissions");
1925
+ }
1926
+ if (!response.ok) {
1927
+ const result = await response.json().catch(() => ({}));
1928
+ throw new Error(`Authentication failed: ${result.message || response.statusText}`);
1929
+ }
1930
+ } catch (error) {
1931
+ if (error instanceof Error && error.message.includes("Authentication failed")) {
1932
+ throw error;
1933
+ }
1934
+ throw new Error(`Authentication test failed: ${error instanceof Error ? error.message : "Unknown error"}`);
1935
+ }
1936
+ }
1937
+ getVersion() {
1938
+ return "2.0.0";
1939
+ }
1940
+ /**
1941
+ * Get IWINV-specific configuration
1942
+ */
1943
+ getIWINVConfig() {
1944
+ return {
1945
+ apiKey: this.getConfig("apiKey"),
1946
+ baseUrl: this.getConfig("baseUrl") || "https://alimtalk.bizservice.iwinv.kr",
1947
+ debug: this.getConfig("debug") || false
1948
+ };
1949
+ }
1950
+ /**
1951
+ * IWINV-specific methods for backward compatibility
1952
+ */
1953
+ /**
1954
+ * Send AlimTalk message (legacy method)
1955
+ */
1956
+ async sendMessage(options) {
1957
+ return this.messaging.send({
1958
+ templateCode: options.templateCode,
1959
+ phoneNumber: options.phoneNumber,
1960
+ variables: options.variables,
1961
+ senderNumber: options.senderNumber
1962
+ });
1963
+ }
1964
+ /**
1965
+ * Get account balance (legacy method)
1966
+ */
1967
+ async getBalance() {
1968
+ return this.account.getBalance();
1969
+ }
1970
+ /**
1971
+ * List templates (legacy method)
1972
+ */
1973
+ async listTemplates(filters) {
1974
+ return this.templates.list(filters);
1975
+ }
1976
+ };
1977
+ export {
1978
+ AligoRequestAdapter,
1979
+ AligoResponseAdapter,
1980
+ BaseAlimTalkProvider,
1981
+ BasePlugin,
1982
+ BaseRequestAdapter,
1983
+ BaseResponseAdapter,
1984
+ IWINVProvider,
1985
+ IWINVRequestAdapter,
1986
+ IWINVResponseAdapter,
1987
+ KakaoRequestAdapter,
1988
+ KakaoResponseAdapter,
1989
+ NHNResponseAdapter,
1990
+ PluginRegistry,
1991
+ ProviderManager,
1992
+ RequestAdapterFactory,
1993
+ ResponseAdapterFactory,
1994
+ createCircuitBreakerMiddleware,
1995
+ createLoggingMiddleware,
1996
+ createMetricsMiddleware,
1997
+ createRateLimitMiddleware,
1998
+ createRetryMiddleware,
1999
+ delay,
2000
+ extractVariables,
2001
+ formatDateTime,
2002
+ normalizePhoneNumber,
2003
+ parseTemplate,
2004
+ retry,
2005
+ validatePhoneNumber
2006
+ };
2007
+ //# sourceMappingURL=index.js.map