skills/rytass/utils/sms-development

sms-development

SKILL.md

SMS Development Guide

This skill provides guidance for developers working with the @rytass/sms base package, including creating new SMS adapters for Taiwan SMS service providers.

Overview

The @rytass/sms package defines the core interfaces and types that all SMS adapters must implement. It follows the adapter pattern to provide a unified API across different SMS providers.

Architecture

@rytass/sms (Base Package)
    ├── SMSService<Request, SendResponse, MultiTarget>  # Service interface
    ├── SMSRequest                                       # Single message interface
    ├── SMSSendResponse                                  # Response interface
    ├── MultiTargetRequest                              # Batch message interface
    ├── SMSRequestResult                                # Status enum
    └── Taiwan Phone Number Helpers                     # Normalization utilities

@rytass/sms-adapter-*                                   # Provider implementations
    ├── [Provider]SMSService                            # Implements SMSService
    ├── [Provider]SMSRequest                            # Extends SMSRequest
    ├── [Provider]SMSSendResponse                       # Extends SMSSendResponse
    └── [Provider] specific types and errors

Installation

npm install @rytass/sms

Core Interfaces

SMSService

The main interface that all SMS adapters must implement:

/**
 * SMS service interface
 * @template Request - SMS request type extending SMSRequest
 * @template SendResponse - SMS response type extending SMSSendResponse
 * @template MultiTarget - Multi-target request type extending MultiTargetRequest
 */
interface SMSService<
  Request extends SMSRequest,
  SendResponse extends SMSSendResponse,
  MultiTarget extends MultiTargetRequest,
> {
  /**
   * Send multiple SMS messages with different content
   * @param request - Array of SMS requests
   * @returns Promise resolving to array of send responses
   */
  send(request: Request[]): Promise<SendResponse[]>;

  /**
   * Send single SMS message
   * @param request - Single SMS request
   * @returns Promise resolving to send response
   */
  send(request: Request): Promise<SendResponse>;

  /**
   * Send same message to multiple recipients
   * @param request - Multi-target request with recipient list
   * @returns Promise resolving to array of send responses
   */
  send(request: MultiTarget): Promise<SendResponse[]>;
}

Base Request Types

/**
 * Base SMS request interface
 */
interface SMSRequest {
  /** Recipient mobile phone number */
  mobile: string;

  /** Message text content */
  text: string;
}

/**
 * Multi-target request for sending same message to multiple recipients
 */
interface MultiTargetRequest {
  /** Array of recipient mobile phone numbers */
  mobileList: string[];

  /** Message text content (same for all recipients) */
  text: string;
}

Base Response Types

/**
 * SMS request result status
 */
enum SMSRequestResult {
  /** Message sent successfully */
  SUCCESS = 'SUCCESS',

  /** Message delivery failed */
  FAILED = 'FAILED',
}

/**
 * Base SMS send response interface
 */
interface SMSSendResponse {
  /** Unique message identifier (if provided by gateway) */
  messageId?: string;

  /** Delivery status */
  status: SMSRequestResult;

  /** Recipient mobile phone number (normalized) */
  mobile: string;
}

Taiwan Phone Number Utilities

The base package provides utilities for handling Taiwan mobile numbers:

/**
 * Regular expression for validating Taiwan mobile phone numbers
 * Matches formats:
 * - 09XXXXXXXX (standard)
 * - 0912-345-678 (with dashes)
 * - 0912 345 678 (with spaces)
 * - +8869XXXXXXXX (international)
 * - 8869XXXXXXXX (international without +)
 */
const TAIWAN_PHONE_NUMBER_RE = /^(0|\+?886-?)9\d{2}-?\d{3}-?\d{3}$/;

/**
 * Normalize Taiwan mobile phone number to standard format (09XXXXXXXX)
 * @param mobile - Phone number in any supported format
 * @returns Normalized phone number (09XXXXXXXX)
 *
 * @example
 * normalizedTaiwanMobilePhoneNumber('0987-654-321') // '0987654321'
 * normalizedTaiwanMobilePhoneNumber('+886987654321') // '0987654321'
 * normalizedTaiwanMobilePhoneNumber('886987654321') // '0987654321'
 */
function normalizedTaiwanMobilePhoneNumber(mobile: string): string;

Implementation Guide

Step 1: Define Provider-Specific Types

Create interfaces extending the base types:

import {
  SMSRequest,
  SMSSendResponse,
  MultiTargetRequest,
  SMSRequestResult,
} from '@rytass/sms';

/**
 * Provider-specific initialization options
 */
export interface MyProviderSMSRequestInit {
  /** API username/account ID */
  username: string;

  /** API password/secret key */
  password: string;

  /** API base URL (optional, defaults to production) */
  baseUrl?: string;

  /** Restrict to Taiwan mobile numbers only */
  onlyTaiwanMobileNumber?: boolean;

  // Add any other provider-specific config
}

/**
 * Provider-specific error codes
 */
export enum MyProviderError {
  /** Invalid credentials */
  INVALID_CREDENTIALS = -1,

  /** Invalid phone number format */
  FORMAT_ERROR = -2,

  /** Insufficient account balance */
  INSUFFICIENT_BALANCE = -3,

  /** Rate limit exceeded */
  RATE_LIMIT_EXCEEDED = -4,

  /** Unknown error */
  UNKNOWN = -99,
}

/**
 * Provider-specific SMS request
 * Extends base SMSRequest with additional fields if needed
 */
export interface MyProviderSMSRequest extends SMSRequest {
  mobile: string;
  text: string;

  // Add provider-specific fields if needed
  // priority?: 'low' | 'normal' | 'high';
  // scheduledTime?: Date;
}

/**
 * Provider-specific send response
 * Extends base SMSSendResponse with additional fields
 */
export interface MyProviderSMSSendResponse extends SMSSendResponse {
  messageId?: string;
  status: SMSRequestResult;
  mobile: string;

  /** Error message if delivery failed */
  errorMessage?: string;

  /** Provider-specific error code */
  errorCode?: MyProviderError;

  // Add provider-specific fields if needed
  // cost?: number;
  // remainingBalance?: number;
}

/**
 * Provider-specific multi-target request
 */
export interface MyProviderSMSMultiTargetRequest extends MultiTargetRequest {
  mobileList: string[];
  text: string;
}

Step 2: Implement the SMS Service

import {
  SMSService,
  SMSRequestResult,
  normalizedTaiwanMobilePhoneNumber,
  TAIWAN_PHONE_NUMBER_RE,
} from '@rytass/sms';
import axios from 'axios';

/**
 * SMS service implementation for MyProvider
 * Implements the SMSService interface with provider-specific logic
 */
export class SMSServiceMyProvider implements SMSService<
  MyProviderSMSRequest,
  MyProviderSMSSendResponse,
  MyProviderSMSMultiTargetRequest
> {
  private readonly username: string;
  private readonly password: string;
  private readonly baseUrl: string;
  private readonly onlyTaiwanMobileNumber: boolean;

  /**
   * Initialize SMS service
   * @param options - Provider configuration options
   */
  constructor(options: MyProviderSMSRequestInit) {
    this.username = options.username;
    this.password = options.password;
    this.baseUrl = options.baseUrl || 'https://api.myprovider.com';
    this.onlyTaiwanMobileNumber = options.onlyTaiwanMobileNumber || false;
  }

  /**
   * Send SMS message(s)
   * Handles three sending patterns:
   * 1. Single SMS to one recipient
   * 2. Multiple SMS with different messages
   * 3. Same message to multiple recipients
   */
  async send(requests: MyProviderSMSRequest[]): Promise<MyProviderSMSSendResponse[]>;
  async send(request: MyProviderSMSRequest): Promise<MyProviderSMSSendResponse>;
  async send(request: MyProviderSMSMultiTargetRequest): Promise<MyProviderSMSSendResponse[]>;

  async send(
    requests: MyProviderSMSMultiTargetRequest | MyProviderSMSRequest | MyProviderSMSRequest[],
  ): Promise<MyProviderSMSSendResponse | MyProviderSMSSendResponse[]> {
    // Validate input
    if (
      (Array.isArray(requests) && !requests.length) ||
      ((requests as MyProviderSMSMultiTargetRequest).mobileList &&
        !(requests as MyProviderSMSMultiTargetRequest).mobileList?.length)
    ) {
      throw new Error('No target provided.');
    }

    // Process and validate phone numbers
    const processedRequests = this.processRequests(requests);

    // Send to provider API
    const results = await this.sendToProvider(processedRequests);

    // Return results in appropriate format
    return this.formatResults(requests, results);
  }

  /**
   * Process and validate phone numbers
   * @param requests - Raw requests
   * @returns Processed requests with normalized phone numbers
   */
  private processRequests(
    requests: MyProviderSMSMultiTargetRequest | MyProviderSMSRequest | MyProviderSMSRequest[],
  ): Array<{ mobile: string; text: string }> {
    const requestArray = Array.isArray(requests) ? requests : [requests];
    const processed: Array<{ mobile: string; text: string }> = [];

    for (const request of requestArray) {
      if ((request as MyProviderSMSMultiTargetRequest).mobileList) {
        // Multi-target request
        const multiTarget = request as MyProviderSMSMultiTargetRequest;

        for (const mobile of multiTarget.mobileList) {
          const normalizedMobile = this.validateAndNormalizeMobile(mobile);
          processed.push({
            mobile: normalizedMobile,
            text: multiTarget.text,
          });
        }
      } else {
        // Single request
        const singleRequest = request as MyProviderSMSRequest;
        const normalizedMobile = this.validateAndNormalizeMobile(singleRequest.mobile);
        processed.push({
          mobile: normalizedMobile,
          text: singleRequest.text,
        });
      }
    }

    return processed;
  }

  /**
   * Validate and normalize phone number
   * @param mobile - Raw phone number
   * @returns Normalized phone number
   * @throws Error if number is invalid and onlyTaiwanMobileNumber is true
   */
  private validateAndNormalizeMobile(mobile: string): string {
    // Check if Taiwan number
    if (TAIWAN_PHONE_NUMBER_RE.test(mobile)) {
      return normalizedTaiwanMobilePhoneNumber(mobile);
    }

    // If strict Taiwan-only mode, reject non-Taiwan numbers
    if (this.onlyTaiwanMobileNumber) {
      throw new Error(
        `${mobile} is not taiwan mobile phone (\`onlyTaiwanMobileNumber\` option is true)`
      );
    }

    // Return as-is for international numbers
    return mobile;
  }

  /**
   * Send requests to provider API
   * @param requests - Processed requests
   * @returns API responses
   */
  private async sendToProvider(
    requests: Array<{ mobile: string; text: string }>,
  ): Promise<Map<string, MyProviderSMSSendResponse>> {
    // Group requests by message text for batch optimization
    const batches = this.groupByMessage(requests);
    const results = new Map<string, MyProviderSMSSendResponse>();

    // Send each batch
    for (const [message, mobileList] of batches.entries()) {
      try {
        // Call provider API
        const response = await this.callProviderAPI(mobileList, message);

        // Process API response
        const batchResults = this.parseAPIResponse(response, mobileList, message);

        // Store results
        for (const [key, result] of batchResults.entries()) {
          results.set(key, result);
        }
      } catch (error) {
        // Handle API errors
        for (const mobile of mobileList) {
          results.set(`${message}:${mobile}`, {
            status: SMSRequestResult.FAILED,
            mobile,
            errorMessage: error.message,
            errorCode: MyProviderError.UNKNOWN,
          });
        }
      }
    }

    return results;
  }

  /**
   * Group requests by message text for batch sending
   * @param requests - Array of requests
   * @returns Map of message text to mobile numbers
   */
  private groupByMessage(
    requests: Array<{ mobile: string; text: string }>,
  ): Map<string, string[]> {
    const batches = new Map<string, string[]>();

    for (const request of requests) {
      const existing = batches.get(request.text) || [];
      batches.set(request.text, [...existing, request.mobile]);
    }

    return batches;
  }

  /**
   * Call provider API
   * IMPORTANT: Implement this method according to your provider's API specification
   * @param mobileList - Array of phone numbers
   * @param message - Message text
   * @returns API response
   *
   * NOTE: Every8D uses the following API specification:
   * - Endpoint: `${baseUrl}/API21/HTTP/SendSMS.ashx`
   * - Method: POST with application/x-www-form-urlencoded
   * - Parameters: UID (username), PWD (password), MSG (message), DEST (comma-separated mobiles)
   * - Response: CSV format: "credit,sent,cost,unsent,batchId" or "errorCode,errorMessage"
   */
  private async callProviderAPI(
    mobileList: string[],
    message: string,
  ): Promise<any> {
    // Example implementation using Every8D API format
    // Other providers may use different endpoints and parameters
    const { data } = await axios.post(
      `${this.baseUrl}/API21/HTTP/SendSMS.ashx`,
      new URLSearchParams({
        UID: this.username,    // Every8D uses UID for username
        PWD: this.password,    // Every8D uses PWD for password
        MSG: message,          // Every8D uses MSG for message content
        DEST: mobileList.join(','), // Every8D uses DEST for comma-separated recipients
      }),
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
      }
    );

    return data;
  }

  /**
   * Parse provider API response
   * IMPORTANT: Implement this method according to your provider's response format
   * @param response - API response data
   * @param mobileList - Array of phone numbers
   * @param message - Message text
   * @returns Map of results keyed by "message:mobile"
   */
  private parseAPIResponse(
    response: any,
    mobileList: string[],
    message: string,
  ): Map<string, MyProviderSMSSendResponse> {
    const results = new Map<string, MyProviderSMSSendResponse>();

    // Example parsing - REPLACE WITH ACTUAL PROVIDER RESPONSE PARSING
    if (response.success) {
      // All succeeded
      for (const mobile of mobileList) {
        results.set(`${message}:${mobile}`, {
          messageId: response.messageId,
          status: SMSRequestResult.SUCCESS,
          mobile,
        });
      }
    } else {
      // All failed
      for (const mobile of mobileList) {
        results.set(`${message}:${mobile}`, {
          status: SMSRequestResult.FAILED,
          mobile,
          errorMessage: response.errorMessage,
          errorCode: response.errorCode,
        });
      }
    }

    return results;
  }

  /**
   * Format results based on original request type
   * @param originalRequest - Original request
   * @param results - Processed results map
   * @returns Formatted response(s)
   */
  private formatResults(
    originalRequest: MyProviderSMSMultiTargetRequest | MyProviderSMSRequest | MyProviderSMSRequest[],
    results: Map<string, MyProviderSMSSendResponse>,
  ): MyProviderSMSSendResponse | MyProviderSMSSendResponse[] {
    // Multi-target request
    if ((originalRequest as MyProviderSMSMultiTargetRequest).mobileList) {
      const multiTarget = originalRequest as MyProviderSMSMultiTargetRequest;

      return multiTarget.mobileList.map(mobile => {
        const normalizedMobile = TAIWAN_PHONE_NUMBER_RE.test(mobile)
          ? normalizedTaiwanMobilePhoneNumber(mobile)
          : mobile;

        return results.get(`${multiTarget.text}:${normalizedMobile}`)!;
      });
    }

    // Array of requests
    if (Array.isArray(originalRequest)) {
      return originalRequest.map(request => {
        const normalizedMobile = TAIWAN_PHONE_NUMBER_RE.test(request.mobile)
          ? normalizedTaiwanMobilePhoneNumber(request.mobile)
          : request.mobile;

        return results.get(`${request.text}:${normalizedMobile}`)!;
      });
    }

    // Single request
    const singleRequest = originalRequest as MyProviderSMSRequest;
    const normalizedMobile = TAIWAN_PHONE_NUMBER_RE.test(singleRequest.mobile)
      ? normalizedTaiwanMobilePhoneNumber(singleRequest.mobile)
      : singleRequest.mobile;

    return results.get(`${singleRequest.text}:${normalizedMobile}`)!;
  }
}

Step 3: Export Public API

// index.ts
export { SMSServiceMyProvider } from './sms-service-my-provider';
export * from './typings';

Step 4: Add Tests

// __tests__/sms-service-my-provider.spec.ts
import { SMSServiceMyProvider } from '../src/sms-service-my-provider';
import { SMSRequestResult } from '@rytass/sms';

describe('SMSServiceMyProvider', () => {
  let smsService: SMSServiceMyProvider;

  beforeEach(() => {
    smsService = new SMSServiceMyProvider({
      username: 'test-username',
      password: 'test-password',
      onlyTaiwanMobileNumber: true,
    });
  });

  describe('send - single SMS', () => {
    it('should send single SMS successfully', async () => {
      const result = await smsService.send({
        mobile: '0987654321',
        text: 'Test message',
      });

      expect(result.status).toBe(SMSRequestResult.SUCCESS);
      expect(result.mobile).toBe('0987654321');
      expect(result.messageId).toBeDefined();
    });

    it('should normalize Taiwan phone number', async () => {
      const result = await smsService.send({
        mobile: '+886987654321',
        text: 'Test message',
      });

      expect(result.mobile).toBe('0987654321');
    });

    it('should reject non-Taiwan number when onlyTaiwanMobileNumber is true', async () => {
      await expect(
        smsService.send({
          mobile: '+1234567890',
          text: 'Test message',
        })
      ).rejects.toThrow('is not taiwan mobile phone');
    });
  });

  describe('send - batch SMS', () => {
    it('should send same message to multiple recipients', async () => {
      const results = await smsService.send({
        mobileList: ['0987654321', '0912345678', '0923456789'],
        text: 'Batch message',
      });

      expect(results).toHaveLength(3);
      expect(results.every(r => r.status === SMSRequestResult.SUCCESS)).toBe(true);
    });

    it('should send different messages to multiple recipients', async () => {
      const results = await smsService.send([
        { mobile: '0987654321', text: 'Message 1' },
        { mobile: '0912345678', text: 'Message 2' },
        { mobile: '0923456789', text: 'Message 3' },
      ]);

      expect(results).toHaveLength(3);
      expect(results.every(r => r.status === SMSRequestResult.SUCCESS)).toBe(true);
    });
  });

  describe('error handling', () => {
    it('should handle API errors gracefully', async () => {
      // Mock API error
      // ... test implementation
    });

    it('should return FAILED status on delivery failure', async () => {
      // Mock failed delivery
      // ... test implementation
    });
  });
});

Implementation Checklist

When implementing a new SMS adapter, ensure:

Required Features

  • Interface Implementation: Implements SMSService<Request, SendResponse, MultiTarget>
  • Single SMS: Supports sending single SMS to one recipient
  • Batch SMS: Supports sending same message to multiple recipients
  • Multi-target: Supports sending different messages to multiple recipients
  • Phone Validation: Validates phone numbers before sending
  • Number Normalization: Uses normalizedTaiwanMobilePhoneNumber() for Taiwan numbers
  • Error Handling: Returns appropriate error codes and messages
  • Type Safety: All methods have proper TypeScript types

Recommended Features

  • Taiwan Number Support: Uses TAIWAN_PHONE_NUMBER_RE for validation
  • Strict Mode: Implements onlyTaiwanMobileNumber option
  • International Support: Handles international numbers when strict mode is disabled
  • Message Batching: Groups messages for efficient API calls
  • Rate Limiting: Implements rate limiting if required by provider
  • Retry Logic: Implements retry for transient failures
  • Logging: Logs API calls and errors for debugging
  • Documentation: Includes comprehensive JSDoc comments

Quality Assurance

  • Unit Tests: Comprehensive test coverage (>80%)
  • Integration Tests: Tests against provider API (staging environment)
  • Error Cases: Tests all error scenarios
  • Edge Cases: Tests edge cases (empty lists, invalid numbers, etc.)
  • Performance: Tests batch performance with large recipient lists
  • README: Complete README with examples and API reference

Best Practices

1. Phone Number Handling

// ✅ GOOD: Use provided utilities
import { normalizedTaiwanMobilePhoneNumber, TAIWAN_PHONE_NUMBER_RE } from '@rytass/sms';

private validateMobile(mobile: string): string {
  if (TAIWAN_PHONE_NUMBER_RE.test(mobile)) {
    return normalizedTaiwanMobilePhoneNumber(mobile);
  }

  if (this.onlyTaiwanMobileNumber) {
    throw new Error(`Invalid Taiwan mobile number: ${mobile}`);
  }

  return mobile;
}

// ❌ BAD: Custom regex without normalization
private validateMobile(mobile: string): string {
  if (!/^09\d{8}$/.test(mobile)) {
    throw new Error('Invalid number');
  }
  return mobile;
}

2. Error Handling

// ✅ GOOD: Detailed error information
catch (error) {
  return {
    status: SMSRequestResult.FAILED,
    mobile,
    errorMessage: error.response?.data?.message || error.message,
    errorCode: this.mapProviderErrorCode(error.response?.data?.code),
  };
}

// ❌ BAD: Generic error without details
catch (error) {
  return {
    status: SMSRequestResult.FAILED,
    mobile,
  };
}

3. Batch Optimization

// ✅ GOOD: Group by message for efficiency
private groupByMessage(requests: Array<{ mobile: string; text: string }>) {
  const batches = new Map<string, string[]>();

  for (const request of requests) {
    const existing = batches.get(request.text) || [];
    batches.set(request.text, [...existing, request.mobile]);
  }

  return batches;
}

// ❌ BAD: Send each message individually
for (const request of requests) {
  await this.callAPI(request.mobile, request.text);
}

4. Type Safety

// ✅ GOOD: Strict typing with generics
export class SMSServiceMyProvider implements SMSService<
  MyProviderSMSRequest,
  MyProviderSMSSendResponse,
  MyProviderSMSMultiTargetRequest
> {
  // ...
}

// ❌ BAD: Using any or loose typing
export class SMSServiceMyProvider {
  async send(request: any): Promise<any> {
    // ...
  }
}

5. Configuration

// ✅ GOOD: Environment-based configuration
export class SMSServiceMyProvider {
  constructor(options: MyProviderSMSRequestInit) {
    this.baseUrl = options.baseUrl || this.getDefaultBaseUrl();
  }

  private getDefaultBaseUrl(): string {
    return process.env.NODE_ENV === 'production'
      ? 'https://api.myprovider.com'
      : 'https://api-staging.myprovider.com';
  }
}

// ❌ BAD: Hardcoded production URL
export class SMSServiceMyProvider {
  private baseUrl = 'https://api.myprovider.com';
}

API Reference

Base Package Exports

// Types
export {
  SMSService,          // Service interface
  SMSRequest,          // Single message interface
  SMSSendResponse,     // Response interface
  MultiTargetRequest,  // Batch message interface
  SMSRequestResult,    // Status enum
};

// Utilities
export {
  TAIWAN_PHONE_NUMBER_RE,                   // Taiwan number regex
  normalizedTaiwanMobilePhoneNumber,        // Normalization function
};

Taiwan Number Validation

Supported Formats:

  • 09XXXXXXXX - Standard Taiwan format
  • 0912-345-678 - With dashes
  • 0912 345 678 - With spaces
  • +8869XXXXXXXX - International with +
  • 8869XXXXXXXX - International without +

Normalization Output:

  • Always returns 09XXXXXXXX format
  • Removes dashes, spaces, and country code
  • Converts +886 or 886 prefix to 0

Examples

Minimal Adapter Implementation

import {
  SMSService,
  SMSRequest,
  SMSSendResponse,
  MultiTargetRequest,
  SMSRequestResult,
} from '@rytass/sms';

interface SimpleSMSRequest extends SMSRequest {
  mobile: string;
  text: string;
}

interface SimpleSMSResponse extends SMSSendResponse {
  messageId?: string;
  status: SMSRequestResult;
  mobile: string;
}

interface SimpleMultiTargetRequest extends MultiTargetRequest {
  mobileList: string[];
  text: string;
}

export class SimpleSMSService implements SMSService<
  SimpleSMSRequest,
  SimpleSMSResponse,
  SimpleMultiTargetRequest
> {
  async send(requests: SimpleSMSRequest[]): Promise<SimpleSMSResponse[]>;
  async send(request: SimpleSMSRequest): Promise<SimpleSMSResponse>;
  async send(request: SimpleMultiTargetRequest): Promise<SimpleSMSResponse[]>;

  async send(request: any): Promise<any> {
    // Minimal implementation
    if (Array.isArray(request)) {
      return Promise.all(request.map(r => this.sendSingle(r)));
    }

    if (request.mobileList) {
      return Promise.all(
        request.mobileList.map(mobile =>
          this.sendSingle({ mobile, text: request.text })
        )
      );
    }

    return this.sendSingle(request);
  }

  private async sendSingle(request: SimpleSMSRequest): Promise<SimpleSMSResponse> {
    // Call provider API
    // Return response
    return {
      messageId: 'MSG-' + Date.now(),
      status: SMSRequestResult.SUCCESS,
      mobile: request.mobile,
    };
  }
}

Common Provider Patterns

HTTP-based API

Most Taiwan SMS providers use HTTP POST:

private async callProviderAPI(mobile: string, text: string): Promise<any> {
  const { data } = await axios.post(
    `${this.baseUrl}/api/sms/send`,
    new URLSearchParams({
      username: this.username,
      password: this.password,
      mobile: mobile,
      message: text,
    }),
    {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    }
  );

  return data;
}

CSV Response Format

Some providers return CSV responses:

private parseCSVResponse(data: string): {
  messageId: string;
  status: SMSRequestResult;
} {
  const [credit, sent, cost, unsent, messageId] = data.split(',');

  return {
    messageId,
    status: messageId ? SMSRequestResult.SUCCESS : SMSRequestResult.FAILED,
  };
}

Signature-based Authentication

Some providers require request signatures:

import crypto from 'crypto';

private generateSignature(params: Record<string, string>): string {
  const sorted = Object.keys(params)
    .sort()
    .map(key => `${key}=${params[key]}`)
    .join('&');

  return crypto
    .createHash('md5')
    .update(sorted + this.secretKey)
    .digest('hex');
}

Troubleshooting

Common Issues

TypeScript Errors:

// Error: Type 'X' is not assignable to type 'SMSService<...>'
// Solution: Ensure all three send() overloads are implemented

async send(requests: MyProviderSMSRequest[]): Promise<MyProviderSMSSendResponse[]>;
async send(request: MyProviderSMSRequest): Promise<MyProviderSMSSendResponse>;
async send(request: MyProviderSMSMultiTargetRequest): Promise<MyProviderSMSSendResponse[]>;

Phone Number Validation:

// Issue: Numbers not being normalized correctly
// Solution: Use TAIWAN_PHONE_NUMBER_RE before normalizing

if (TAIWAN_PHONE_NUMBER_RE.test(mobile)) {
  mobile = normalizedTaiwanMobilePhoneNumber(mobile);
}

Batch Optimization:

// Issue: Too many API calls
// Solution: Group requests by message text

const batches = requests.reduce((map, req) => {
  const list = map.get(req.text) || [];
  map.set(req.text, [...list, req.mobile]);
  return map;
}, new Map<string, string[]>());

Publishing Checklist

Before publishing your adapter:

  • Package name follows convention: @rytass/sms-adapter-{provider}
  • Peer dependency on @rytass/sms is declared
  • All exports are properly typed
  • README includes installation, usage, and examples
  • Tests pass with >80% coverage
  • TypeScript builds without errors
  • Package builds with npm run build
  • Version follows semantic versioning
  • CHANGELOG.md is updated
  • License is included (MIT recommended)

Resources

For reference implementations:

  • Every8D Adapter - Complete reference implementation (互動資通 / Interactive Communications)
  • Base Package - Core interfaces and utilities

For usage guidance:

Weekly Installs
6
Repository
rytass/utils
GitHub Stars
6
First Seen
Feb 5, 2026
Installed on
amp6
gemini-cli6
replit6
github-copilot6
codex6
kimi-cli6