@stacksjs/sms 0.72.1 → 0.72.2

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.
@@ -1,5 +1,6 @@
1
1
  import type { SmsInboundIntent, SmsInboundMessage, SmsSegmentEstimate } from '@stacksjs/types';
2
2
  export declare function classifySmsIntent(body: string, options?: { optOut?: string[], optIn?: string[], help?: string[] }): { intent: SmsInboundIntent, keyword?: string };
3
+ export declare function smsComplianceReply(intent: SmsInboundIntent, options?: { appName?: string, helpContact?: string }): string | null;
3
4
  export declare function parseTwilioInbound(fields: Record<string, string | undefined>, options?: { optOut?: string[], optIn?: string[], help?: string[] }): SmsInboundMessage;
4
5
  export declare function verifyTwilioWebhook(url: string, fields: Record<string, string | undefined>, signature: string, authToken: string): boolean;
5
6
  export declare function estimateSmsSegments(body: string): SmsSegmentEstimate;
@@ -1,2 +1,2 @@
1
1
  import{createHmac,timingSafeEqual}from"node:crypto";const DEFAULT_OPT_OUT=["STOP","UNSUBSCRIBE","CANCEL","END","QUIT"],DEFAULT_OPT_IN=["START","UNSTOP","YES"],DEFAULT_HELP=["HELP","INFO"],GSM_7_BASIC=new Set(`@\xA3$\xA5\xE8\xE9\xF9\xEC\xF2\xC7
2
- \xD8\xF8\r\xC5\xE5\u0394_\u03A6\u0393\u039B\u03A9\u03A0\u03A8\u03A3\u0398\u039E\xC6\xE6\xDF\xC9 !"#\xA4%&'()*+,-./0123456789:;<=>?\xA1ABCDEFGHIJKLMNOPQRSTUVWXYZ\xC4\xD6\xD1\xDC\xA7\xBFabcdefghijklmnopqrstuvwxyz\xE4\xF6\xF1\xFC\xE0`),GSM_7_EXTENDED=new Set("^{}\\[~]|\u20AC");function normalizedKeyword(body){return body.trim().split(/\s+/,1)[0]?.toUpperCase()??""}export function classifySmsIntent(body,options={}){const keyword=normalizedKeyword(body),includes=(values,defaults)=>(values??defaults).some((value)=>value.trim().toUpperCase()===keyword);if(includes(options.optOut,DEFAULT_OPT_OUT))return{intent:"opt-out",keyword};if(includes(options.optIn,DEFAULT_OPT_IN))return{intent:"opt-in",keyword};if(includes(options.help,DEFAULT_HELP))return{intent:"help",keyword};return{intent:"message"}}export function parseTwilioInbound(fields,options={}){const body=fields.Body??"";return{from:fields.From??"",to:fields.To??"",body,messageId:fields.MessageSid??fields.SmsSid,...classifySmsIntent(body,options)}}export function verifyTwilioWebhook(url,fields,signature,authToken){if(!url||!signature||!authToken)return!1;const payload=Object.keys(fields).sort().reduce((value,key)=>`${value}${key}${fields[key]??""}`,url),expected=createHmac("sha1",authToken).update(payload).digest("base64"),expectedBuffer=Buffer.from(expected),actualBuffer=Buffer.from(signature);if(expectedBuffer.length!==actualBuffer.length){timingSafeEqual(expectedBuffer,expectedBuffer);return!1}return timingSafeEqual(expectedBuffer,actualBuffer)}export function estimateSmsSegments(body){let septets=0,gsm7=!0;for(const character of body)if(GSM_7_BASIC.has(character))septets+=1;else if(GSM_7_EXTENDED.has(character))septets+=2;else{gsm7=!1;break}if(gsm7){const perSegment=septets<=160?160:153;return{encoding:"gsm-7",characters:body.length,segments:Math.max(1,Math.ceil(septets/perSegment)),perSegment}}const characters=[...body].length,perSegment=characters<=70?70:67;return{encoding:"ucs-2",characters,segments:Math.max(1,Math.ceil(characters/perSegment)),perSegment}}export function isWithinSmsQuietHours(at,options){const hour=Number(new Intl.DateTimeFormat("en-US",{hour:"numeric",hourCycle:"h23",timeZone:options.timezone??"UTC"}).format(at)),start=Math.min(23,Math.max(0,Math.floor(options.startHour))),end=Math.min(23,Math.max(0,Math.floor(options.endHour)));if(start===end)return!0;return start<end?hour>=start&&hour<end:hour>=start||hour<end}
2
+ \xD8\xF8\r\xC5\xE5\u0394_\u03A6\u0393\u039B\u03A9\u03A0\u03A8\u03A3\u0398\u039E\xC6\xE6\xDF\xC9 !"#\xA4%&'()*+,-./0123456789:;<=>?\xA1ABCDEFGHIJKLMNOPQRSTUVWXYZ\xC4\xD6\xD1\xDC\xA7\xBFabcdefghijklmnopqrstuvwxyz\xE4\xF6\xF1\xFC\xE0`),GSM_7_EXTENDED=new Set("^{}\\[~]|\u20AC");function normalizedKeyword(body){return body.trim().split(/\s+/,1)[0]?.toUpperCase()??""}export function classifySmsIntent(body,options={}){const keyword=normalizedKeyword(body),includes=(values,defaults)=>(values??defaults).some((value)=>value.trim().toUpperCase()===keyword);if(includes(options.optOut,DEFAULT_OPT_OUT))return{intent:"opt-out",keyword};if(includes(options.optIn,DEFAULT_OPT_IN))return{intent:"opt-in",keyword};if(includes(options.help,DEFAULT_HELP))return{intent:"help",keyword};return{intent:"message"}}export function smsComplianceReply(intent,options={}){const name=options.appName?.trim()||"This service";if(intent==="opt-out")return`${name}: you are unsubscribed. No more messages will be sent. Reply START to resubscribe.`;if(intent==="opt-in")return`${name}: you are resubscribed. Reply STOP to unsubscribe.`;if(intent==="help")return`${name}: reply STOP to unsubscribe or START to resubscribe.${options.helpContact?` Contact ${options.helpContact}.`:""}`;return null}export function parseTwilioInbound(fields,options={}){const body=fields.Body??"";return{from:fields.From??"",to:fields.To??"",body,messageId:fields.MessageSid??fields.SmsSid,...classifySmsIntent(body,options)}}export function verifyTwilioWebhook(url,fields,signature,authToken){if(!url||!signature||!authToken)return!1;const payload=Object.keys(fields).sort().reduce((value,key)=>`${value}${key}${fields[key]??""}`,url),expected=createHmac("sha1",authToken).update(payload).digest("base64"),expectedBuffer=Buffer.from(expected),actualBuffer=Buffer.from(signature);if(expectedBuffer.length!==actualBuffer.length){timingSafeEqual(expectedBuffer,expectedBuffer);return!1}return timingSafeEqual(expectedBuffer,actualBuffer)}export function estimateSmsSegments(body){let septets=0,gsm7=!0;for(const character of body)if(GSM_7_BASIC.has(character))septets+=1;else if(GSM_7_EXTENDED.has(character))septets+=2;else{gsm7=!1;break}if(gsm7){const perSegment=septets<=160?160:153;return{encoding:"gsm-7",characters:body.length,segments:Math.max(1,Math.ceil(septets/perSegment)),perSegment}}const characters=[...body].length,perSegment=characters<=70?70:67;return{encoding:"ucs-2",characters,segments:Math.max(1,Math.ceil(characters/perSegment)),perSegment}}export function isWithinSmsQuietHours(at,options){const hour=Number(new Intl.DateTimeFormat("en-US",{hour:"numeric",hourCycle:"h23",timeZone:options.timezone??"UTC"}).format(at)),start=Math.min(23,Math.max(0,Math.floor(options.startHour))),end=Math.min(23,Math.max(0,Math.floor(options.endHour)));if(start===end)return!0;return start<end?hour>=start&&hour<end:hour>=start||hour<end}
package/dist/sms.js CHANGED
@@ -1 +1 @@
1
- import{TwilioDriver}from"./drivers/twilio";import{VonageDriver}from"./drivers/vonage";let defaultDriver=null,verificationDriver=null,smsConfig={},_configPromise=null;async function loadConfig(){try{smsConfig=(await import("../../../../../config/sms")).default}catch{}}async function ensureConfig(){if(!_configPromise)_configPromise=loadConfig();await _configPromise}export function configure(config){smsConfig={...smsConfig,...config};defaultDriver=null;verificationDriver=null}export function getDriver(provider){const targetProvider=provider||smsConfig.provider||"twilio";switch(targetProvider){case"twilio":{const twilioConfig=smsConfig.drivers?.twilio;if(!twilioConfig?.accountSid||!twilioConfig?.authToken)throw Error("Twilio configuration is incomplete. Please provide accountSid and authToken.");return new TwilioDriver({...twilioConfig,from:twilioConfig.from||smsConfig.from},twilioConfig.verifyServiceSid)}case"vonage":{const vonageConfig=smsConfig.drivers?.vonage;if(!vonageConfig?.apiKey||!vonageConfig?.apiSecret)throw Error("Vonage configuration is incomplete. Please provide apiKey and apiSecret.");return new VonageDriver({...vonageConfig,from:vonageConfig.from||smsConfig.from})}default:throw Error(`Unsupported SMS provider: ${targetProvider}`)}}export function getVerificationDriver(provider){const driver=getDriver(provider);if(!("startVerification"in driver))throw Error(`Provider does not support verification: ${provider||smsConfig.provider}`);return driver}function getDefaultDriver(){if(!defaultDriver)defaultDriver=getDriver();return defaultDriver}function getDefaultVerificationDriver(){if(!verificationDriver)verificationDriver=getVerificationDriver();return verificationDriver}export async function send(message){await ensureConfig();return getDefaultDriver().send(message)}export const sendSms=send;export async function sendBulk(messages){await ensureConfig();const driver=getDefaultDriver(),concurrency=Math.max(1,Math.floor(smsConfig.bulk?.concurrency??10)),delayMs=Math.max(0,Math.floor(smsConfig.bulk?.delayMs??0)),results=[];for(let offset=0;offset<messages.length;offset+=concurrency){const batch=messages.slice(offset,offset+concurrency);results.push(...await driver.sendBulk(batch));if(delayMs>0&&offset+concurrency<messages.length)await new Promise((resolve)=>setTimeout(resolve,delayMs))}return results}export async function getStatus(messageId){await ensureConfig();const driver=getDefaultDriver();if(driver.getStatus)return driver.getStatus(messageId);return null}export async function verifyNumber(phoneNumber){await ensureConfig();const driver=getDefaultDriver();if(driver.verify)return driver.verify(phoneNumber);return{valid:!0}}export async function getBalance(){await ensureConfig();const driver=getDefaultDriver();if(driver.getBalance)return driver.getBalance();return null}export async function startVerification(request){await ensureConfig();return getDefaultVerificationDriver().startVerification(request)}export async function checkVerification(request){await ensureConfig();return getDefaultVerificationDriver().checkVerification(request)}export async function cancelVerification(verificationId){await ensureConfig();const driver=getDefaultVerificationDriver();if(driver.cancelVerification)return driver.cancelVerification(verificationId);return!1}export class SmsBuilder{message={};provider;to(phoneNumber){this.message.to=phoneNumber;return this}body(text){this.message.body=text;return this}text(text){return this.body(text)}from(sender){this.message.from=sender;return this}media(urls){this.message.mediaUrls=Array.isArray(urls)?urls:[urls];return this}callback(url){this.message.statusCallback=url;return this}via(provider){this.provider=provider;return this}async send(){await ensureConfig();if(!this.message.to)return{success:!1,to:"",error:"Recipient is required",provider:this.provider||smsConfig.provider||"twilio"};if(!this.message.body)return{success:!1,to:Array.isArray(this.message.to)?this.message.to[0]??"":this.message.to,error:"Message body is required",provider:this.provider||smsConfig.provider||"twilio"};return(this.provider?getDriver(this.provider):getDefaultDriver()).send(this.message)}}export function sms(){return new SmsBuilder}export async function sendTemplate(to,templateName,variables={}){await ensureConfig();const template=smsConfig.templates?.find((t)=>t.name===templateName);if(!template)return{success:!1,to:Array.isArray(to)?to[0]??"":to,error:`Template not found: ${templateName}`,provider:smsConfig.provider||"twilio"};let body=template.body;for(const[key,value]of Object.entries(variables)){const escapedKey=key.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");body=body.replace(new RegExp(`\\{${escapedKey}\\}`,"g"),value)}return send({to,body})}export function formatE164(phoneNumber,defaultCountryCode){let cleaned=phoneNumber.replace(/[\s\-()]/g,"");if(cleaned.startsWith("+"))return cleaned;const countryCode=defaultCountryCode||smsConfig.defaultCountryCode||"1";if(cleaned.startsWith("00"))cleaned=`+${cleaned.slice(2)}`;else if(!cleaned.startsWith("+"))cleaned=`+${countryCode}${cleaned}`;return cleaned}export function isValidPhoneNumber(phoneNumber){const e164Regex=/^\+[1-9]\d{6,14}$/,formatted=formatE164(phoneNumber);return e164Regex.test(formatted)}export async function init(){await loadConfig()}export function isEnabled(){return smsConfig.enabled===!0}export function getConfig(){return{...smsConfig}}export const SMS={init,configure,isEnabled,getConfig,send,sendSms,sendBulk,sendTemplate,getStatus,getBalance,verifyNumber,startVerification,checkVerification,cancelVerification,formatE164,isValidPhoneNumber,sms,getDriver,getVerificationDriver};export default SMS;
1
+ import{TwilioDriver}from"./drivers/twilio";import{VonageDriver}from"./drivers/vonage";let defaultDriver=null,verificationDriver=null,smsConfig={},_configPromise=null;async function loadConfig(){try{smsConfig=(await import("../../../../../config/sms")).default}catch{}}async function ensureConfig(){if(!_configPromise)_configPromise=loadConfig();await _configPromise}export function configure(config){smsConfig={...smsConfig,...config};defaultDriver=null;verificationDriver=null}export function getDriver(provider){const targetProvider=provider||smsConfig.provider||"twilio";switch(targetProvider){case"twilio":{const twilioConfig=smsConfig.drivers?.twilio;if(!twilioConfig?.accountSid||!twilioConfig?.authToken)throw Error("Twilio configuration is incomplete. Please provide accountSid and authToken.");return new TwilioDriver({...twilioConfig,from:twilioConfig.from||smsConfig.from},twilioConfig.verifyServiceSid)}case"vonage":{const vonageConfig=smsConfig.drivers?.vonage;if(!vonageConfig?.apiKey||!vonageConfig?.apiSecret)throw Error("Vonage configuration is incomplete. Please provide apiKey and apiSecret.");return new VonageDriver({...vonageConfig,from:vonageConfig.from||smsConfig.from})}default:throw Error(`Unsupported SMS provider: ${targetProvider}`)}}export function getVerificationDriver(provider){const driver=getDriver(provider);if(!("startVerification"in driver))throw Error(`Provider does not support verification: ${provider||smsConfig.provider}`);return driver}function getDefaultDriver(){if(!defaultDriver)defaultDriver=getDriver();return defaultDriver}function getDefaultVerificationDriver(){if(!verificationDriver)verificationDriver=getVerificationDriver();return verificationDriver}export async function send(message){await ensureConfig();return getDefaultDriver().send(message)}export const sendSms=send;export async function sendBulk(messages){await ensureConfig();const driver=getDefaultDriver(),concurrency=Math.max(1,Math.floor(smsConfig.bulk?.concurrency??10)),delayMs=Math.max(0,Math.floor(smsConfig.bulk?.delayMs??0)),results=[];for(let offset=0;offset<messages.length;offset+=concurrency){const batch=messages.slice(offset,offset+concurrency);results.push(...await driver.sendBulk(batch));if(delayMs>0&&offset+concurrency<messages.length)await new Promise((resolve)=>setTimeout(resolve,delayMs))}return results}export async function getStatus(messageId){await ensureConfig();const driver=getDefaultDriver();if(driver.getStatus)return driver.getStatus(messageId);return null}export async function verifyNumber(phoneNumber){await ensureConfig();const driver=getDefaultDriver();if(driver.verify)return driver.verify(phoneNumber);return{valid:!0}}export async function getBalance(){await ensureConfig();const driver=getDefaultDriver();if(driver.getBalance)return driver.getBalance();return null}export async function startVerification(request){await ensureConfig();return getDefaultVerificationDriver().startVerification(request)}export async function checkVerification(request){await ensureConfig();return getDefaultVerificationDriver().checkVerification(request)}export async function cancelVerification(verificationId){await ensureConfig();const driver=getDefaultVerificationDriver();if(driver.cancelVerification)return driver.cancelVerification(verificationId);return!1}export class SmsBuilder{message={};provider;to(phoneNumber){this.message.to=phoneNumber;return this}body(text){this.message.body=text;return this}text(text){return this.body(text)}from(sender){this.message.from=sender;return this}media(urls){this.message.mediaUrls=Array.isArray(urls)?urls:[urls];return this}callback(url){this.message.statusCallback=url;return this}via(provider){this.provider=provider;return this}async send(){await ensureConfig();if(!this.message.to)return{success:!1,to:"",error:"Recipient is required",provider:this.provider||smsConfig.provider||"twilio"};if(!this.message.body)return{success:!1,to:Array.isArray(this.message.to)?this.message.to[0]??"":this.message.to,error:"Message body is required",provider:this.provider||smsConfig.provider||"twilio"};return(this.provider?getDriver(this.provider):getDefaultDriver()).send(this.message)}}export function sms(){return new SmsBuilder}export async function sendTemplate(to,templateName,variables={}){await ensureConfig();const template=smsConfig.templates?.find((t)=>t.name===templateName);if(!template)return{success:!1,to:Array.isArray(to)?to[0]??"":to,error:`Template not found: ${templateName}`,provider:smsConfig.provider||"twilio"};let body=template.body;for(const[key,value]of Object.entries(variables)){const escapedKey=key.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");body=body.replace(new RegExp(`\\{${escapedKey}\\}`,"g"),value)}return send({to,body})}export function formatE164(phoneNumber,defaultCountryCode){let cleaned=phoneNumber.replace(/[\s\-()]/g,"");if(cleaned.startsWith("+"))return cleaned;const countryCode=String(defaultCountryCode||smsConfig.defaultCountryCode||"1").replace(/^\+/,"");if(!/^[1-9]\d{0,3}$/.test(countryCode))throw Error("Default SMS country code must be a numeric dialing prefix, such as 1 for the US.");if(cleaned.startsWith("00"))cleaned=`+${cleaned.slice(2)}`;else if(!cleaned.startsWith("+"))cleaned=`+${countryCode}${cleaned}`;return cleaned}export function isValidPhoneNumber(phoneNumber){const e164Regex=/^\+[1-9]\d{6,14}$/,formatted=formatE164(phoneNumber);return e164Regex.test(formatted)}export async function init(){await loadConfig()}export function isEnabled(){return smsConfig.enabled===!0}export function getConfig(){return{...smsConfig}}export const SMS={init,configure,isEnabled,getConfig,send,sendSms,sendBulk,sendTemplate,getStatus,getBalance,verifyNumber,startVerification,checkVerification,cancelVerification,formatE164,isValidPhoneNumber,sms,getDriver,getVerificationDriver};export default SMS;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/sms",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.1",
5
+ "version": "0.72.2",
6
6
  "description": "The Stacks SMS integration. Painlessly create & manage your inboxes, templates, and send sms.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -63,10 +63,10 @@
63
63
  "prepublishOnly": "bun run build"
64
64
  },
65
65
  "devDependencies": {
66
- "@stacksjs/cli": "0.72.1",
67
- "@stacksjs/config": "0.72.1",
66
+ "@stacksjs/cli": "0.72.2",
67
+ "@stacksjs/config": "0.72.2",
68
68
  "better-dx": "^0.2.23",
69
- "@stacksjs/error-handling": "0.72.1",
70
- "@stacksjs/types": "0.72.1"
69
+ "@stacksjs/error-handling": "0.72.2",
70
+ "@stacksjs/types": "0.72.2"
71
71
  }
72
72
  }