--- name: sms-development description: Development guide for @rytass/sms base package (簡訊基底套件開發指南). Use when creating new SMS adapters (新增簡訊 adapter), understanding base interfaces, or extending SMS functionality. Covers SMSService interface and implementation patterns for Taiwan SMS providers (台灣簡訊服務提供商). Keywords - SMS adapter, 簡訊 adapter, service interface, 服務介面, message delivery, 訊息發送, batch processing, 批次處理, number normalization, 號碼正規化, Every8D, 互動資通, Interactive Communications --- # 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 # 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 ```bash npm install @rytass/sms ``` ## Core Interfaces ### SMSService The main interface that all SMS adapters must implement: ```typescript /** * 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; /** * Send single SMS message * @param request - Single SMS request * @returns Promise resolving to send response */ send(request: Request): Promise; /** * 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; } ``` ### Base Request Types ```typescript /** * 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 ```typescript /** * 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: ```typescript /** * 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: ```typescript 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 ```typescript 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; async send(request: MyProviderSMSRequest): Promise; async send(request: MyProviderSMSMultiTargetRequest): Promise; async send( requests: MyProviderSMSMultiTargetRequest | MyProviderSMSRequest | MyProviderSMSRequest[], ): Promise { // 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> { // Group requests by message text for batch optimization const batches = this.groupByMessage(requests); const results = new Map(); // 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 { const batches = new Map(); 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 { // 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 { const results = new Map(); // 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, ): 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 ```typescript // index.ts export { SMSServiceMyProvider } from './sms-service-my-provider'; export * from './typings'; ``` ### Step 4: Add Tests ```typescript // __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` - [ ] **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 ```typescript // ✅ 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 ```typescript // ✅ 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 ```typescript // ✅ GOOD: Group by message for efficiency private groupByMessage(requests: Array<{ mobile: string; text: string }>) { const batches = new Map(); 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 ```typescript // ✅ 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 { // ... } } ``` ### 5. Configuration ```typescript // ✅ 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 ```typescript // 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 ```typescript 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; async send(request: SimpleSMSRequest): Promise; async send(request: SimpleMultiTargetRequest): Promise; async send(request: any): Promise { // 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 { // 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: ```typescript private async callProviderAPI(mobile: string, text: string): Promise { 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: ```typescript 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: ```typescript import crypto from 'crypto'; private generateSignature(params: Record): 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:** ```typescript // Error: Type 'X' is not assignable to type 'SMSService<...>' // Solution: Ensure all three send() overloads are implemented async send(requests: MyProviderSMSRequest[]): Promise; async send(request: MyProviderSMSRequest): Promise; async send(request: MyProviderSMSMultiTargetRequest): Promise; ``` **Phone Number Validation:** ```typescript // 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:** ```typescript // 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()); ``` ## 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](../../packages/sms-adapter-every8d) - Complete reference implementation (互動資通 / Interactive Communications) - [Base Package](../../packages/sms) - Core interfaces and utilities For usage guidance: - [SMS Adapters Skill](/sms-adapters) - User guide for SMS adapters