sms-development
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_REfor validation - Strict Mode: Implements
onlyTaiwanMobileNumberoption - 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 format0912-345-678- With dashes0912 345 678- With spaces+8869XXXXXXXX- International with +8869XXXXXXXX- International without +
Normalization Output:
- Always returns
09XXXXXXXXformat - Removes dashes, spaces, and country code
- Converts
+886or886prefix to0
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/smsis 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:
- SMS Adapters Skill - User guide for SMS adapters