@stacksjs/notifications 0.75.142 → 0.75.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,7 +58,8 @@ returned channel results; the tracking log is not a final receipt.
58
58
  `payload.unsubscribe` (build it with `notificationUnsubscribeLinks(userId)`)
59
59
  puts an Unsubscribe link in the email footer and text part and sets
60
60
  `List-Unsubscribe` / `List-Unsubscribe-Post` (RFC 8058). A `marketing` text
61
- ends with "Reply STOP to opt out." unless `payload.smsOptOut` says otherwise.
61
+ ends with "Reply STOP to opt out." unless `payload.smsOptOut` says otherwise,
62
+ and `payload.smsSender` starts every text with `{sender}: `.
62
63
  The signed token turns off one channel + category preference; handle the
63
64
  app's route with `unsubscribeWithToken(await unsubscribeTokenFromRequest(request))`.
64
65
 
@@ -30,12 +30,21 @@ export declare function listUnsubscribeHeaders(unsubscribe?: NotificationUnsubsc
30
30
  */
31
31
  export declare function notificationEmailMessage(to: string, payload: NotificationEmailPayload): Promise<EmailMessage>;
32
32
  /**
33
- * What a notification texts. A `marketing` text ends with
33
+ * The name a text starts with, as `{sender}: `: whitespace collapsed, and cut
34
+ * to {@link SMS_SENDER_MAX_LENGTH} characters so a long name cannot eat the
35
+ * message (no ellipsis: one character outside GSM-7 doubles the cost of the
36
+ * whole text). Empty when there is no name.
37
+ */
38
+ export declare function smsSenderPrefix(sender?: string): string;
39
+ /**
40
+ * What a notification texts. With `smsSender` it starts with the sender's
41
+ * name, so a text from a shared number says who it is from (carriers expect
42
+ * the brand in every message). A `marketing` text ends with
34
43
  * {@link DEFAULT_SMS_OPT_OUT} on its own line; `smsOptOut` replaces that line
35
44
  * (and adds it to any category), and `false` leaves it out. A body that
36
- * already contains the line is left alone.
45
+ * already starts with the name, or already contains the line, is left alone.
37
46
  */
38
- export declare function notificationSmsBody(payload: { body: string, smsOptOut?: string | false }, category?: string): string;
47
+ export declare function notificationSmsBody(payload: { body: string, smsSender?: string, smsOptOut?: string | false }, category?: string): string;
39
48
  /** The footer sentence of an email with an unsubscribe link, unless the payload names its own. */
40
49
  export declare const DEFAULT_UNSUBSCRIBE_NOTICE: 'You are receiving this because these emails are turned on in your settings.';
41
50
  /**
@@ -44,6 +53,8 @@ export declare const DEFAULT_UNSUBSCRIBE_NOTICE: 'You are receiving this because
44
53
  * STOP is the keyword every provider and `@stacksjs/sms` honour.
45
54
  */
46
55
  export declare const DEFAULT_SMS_OPT_OUT: 'Reply STOP to opt out.';
56
+ /** The longest sender name a text starts with; a longer one is cut. */
57
+ export declare const SMS_SENDER_MAX_LENGTH: 30;
47
58
  /** Where a notification leads: the email's button. */
48
59
  export declare interface NotificationAction {
49
60
  label: string
package/dist/index.d.ts CHANGED
@@ -100,6 +100,7 @@ export declare interface NotificationPayload {
100
100
  unsubscribe?: NotificationUnsubscribe
101
101
  headers?: Record<string, string>
102
102
  smsOptOut?: string | false
103
+ smsSender?: string
103
104
  }
104
105
  export declare interface NotificationRecipient {
105
106
  email?: string
@@ -132,6 +133,6 @@ export {
132
133
  setNotificationPreference,
133
134
  wherePreferenceCategory,
134
135
  } from './preferences';
135
- export { DEFAULT_SMS_OPT_OUT, DEFAULT_UNSUBSCRIBE_NOTICE, emailParagraphs, listUnsubscribeHeaders, notificationEmailHtml, notificationEmailMessage, notificationEmailText, notificationSmsBody } from './email-body';
136
+ export { DEFAULT_SMS_OPT_OUT, DEFAULT_UNSUBSCRIBE_NOTICE, emailParagraphs, listUnsubscribeHeaders, notificationEmailHtml, notificationEmailMessage, notificationEmailText, notificationSmsBody, SMS_SENDER_MAX_LENGTH, smsSenderPrefix } from './email-body';
136
137
  export { notificationUnsubscribeLinks, notificationUnsubscribeToken, parseNotificationUnsubscribeToken, unsubscribeTokenFromRequest, unsubscribeWithToken } from './unsubscribe';
137
138
  export { accountNotificationTemplates, commerceNotificationTemplates, createNotificationTemplates, defineNotificationTemplates, interpolateNotificationText, notificationTemplateVariables, renderNotificationTemplate, resolveNotificationLocale, subscriptionNotificationTemplates } from './templates';
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  // @bun
2
- import{log as S}from"@stacksjs/cli";import{notification as $e}from"@stacksjs/config";import{mail as se}from"@stacksjs/email";import{log as A}from"@stacksjs/cli";var m={async send(e){let t=e.event??"notification",n=e.channel;if(!n)n=e.userId!==void 0?`private-user.${e.userId}`:"notifications";try{let i=await import("@stacksjs/realtime").catch(()=>null),r=i?.emit,a=i?.getServer;if(!r||!a)return{delivered:!1,channel:n,event:t,reason:"@stacksjs/realtime is not available"};if(!a())return{delivered:!1,channel:n,event:t,reason:"realtime server not running"};let s=e.private??e.userId!==void 0;return r(n,t,e.data,{private:s}),A.info(`[notifications] broadcast to '${n}' (${t})`),{delivered:!0,channel:n,event:t}}catch(i){let r=i instanceof Error?i.message:String(i);return A.warn(`[notifications] broadcast failed: ${r}`),{delivered:!1,channel:n,event:t,reason:r}}}};import*as j from"@stacksjs/chat";import{log as I}from"@stacksjs/cli";import{createHash as fe}from"crypto";import{db as u,enqueueAfterCommit as de,lockRow as pe,sqlDateTime as T}from"@stacksjs/database/runtime";import{emit as me,getServer as be}from"@stacksjs/realtime";function B(e){return`private-user.${e}`}function x(e,t,n){let i=()=>{try{if(be())me(B(e),t,n)}catch{I.warn("Notification inbox broadcast failed; the persisted inbox remains available")}};if(!de(i))i()}async function O(e,t){let n=T(),r=await u.insertInto("notifications").values({user_id:e.userId,...t?{uuid:t}:{},type:e.type,data:JSON.stringify(e.data),read_at:null,created_at:n,updated_at:n}).returning("id").execute(),a=Array.isArray(r)?r[0]:r,s=Number(a?.id??a?.insertId??a?.lastInsertRowid??a?.lastInsertId??0);if(!Number.isSafeInteger(s)||s<=0)throw Error("Database notification did not return a generated id");if(I.info(`Database notification sent to user ${e.userId}: ${e.type}`),e.broadcast!==!1)x(e.userId,"notifications.created",{id:s});return{id:s,user_id:e.userId,type:e.type,data:JSON.stringify(e.data),read_at:null,created_at:n,updated_at:n}}var b={async send(e){if(e.idempotencyKey!==void 0){if(!e.idempotencyKey||e.idempotencyKey.length>255)throw Error("A notification idempotency key must contain 1 to 255 characters");let t=fe("sha256").update(JSON.stringify([e.userId,e.type,e.idempotencyKey])).digest("hex"),n=`${t.slice(0,8)}-${t.slice(8,12)}-8${t.slice(13,16)}-8${t.slice(17,20)}-${t.slice(20,32)}`;return u.transaction(async(i)=>{if(!await pe(i,"users",{id:e.userId}))throw Error("Notification recipient not found");let a=await u.selectFrom("notifications").selectAll().where("user_id","=",e.userId).where("uuid","=",n).executeTakeFirst();if(a)return{...a,id:Number(a.id),user_id:Number(a.user_id)};return O(e,n)})}return O(e)},async getUserNotifications(e){return(await u.selectFrom("notifications").selectAll().where("user_id","=",e).orderBy("created_at","desc").execute()).map((n)=>({...n,id:Number(n.id),user_id:Number(n.user_id)}))},async getUnreadNotifications(e){return(await u.selectFrom("notifications").selectAll().where("user_id","=",e).whereNull("read_at").orderBy("created_at","desc").execute()).map((n)=>({...n,id:Number(n.id),user_id:Number(n.user_id)}))},async markAsRead(e,t){let n=u.updateTable("notifications").set({read_at:T()}).where("id","=",e).whereNull("read_at");if(t!==void 0)n=n.where("user_id","=",t);if(await n.execute(),t)x(t,"notifications.read",{id:e})},async markAllAsRead(e){await u.updateTable("notifications").set({read_at:T()}).where("user_id","=",e).whereNull("read_at").execute(),x(e,"notifications.read",{all:!0})},async unreadCount(e){let t=await u.selectFrom("notifications").select(u.fn.countAll().as("count")).where("user_id","=",e).whereNull("read_at").executeTakeFirst();return Number(t?.count??0)},async deleteNotification(e,t){let n=u.deleteFrom("notifications").where("id","=",e);if(t!==void 0)n=n.where("user_id","=",t);await n.execute()},async deleteAllNotifications(e){await u.deleteFrom("notifications").where("user_id","=",e).execute()}};import*as L from"@stacksjs/push";import*as h from"@stacksjs/sms";import{log as he}from"@stacksjs/cli";import{config as M}from"@stacksjs/config";import{db as ge,sqlDateTime as F}from"@stacksjs/database/runtime";function K(e,t){switch(t){case"email":return e.email||"";case"sms":return e.phone||"";case"push":return Array.isArray(e.pushTokens)?e.pushTokens.join(", "):e.pushTokens||"";case"broadcast":return e.broadcastChannel||(e.userId?`private-user.${e.userId}`:"notifications");case"database":return e.userId?`User #${e.userId}`:"";case"chat":return Array.isArray(e.chatRecipient)?e.chatRecipient.join(", "):e.chatRecipient||""}}function k(e,t,n,i,r=F()){return{userId:e.userId,channel:n.channel,recipient:K(e,n.channel),subject:t.subject,body:t.body,status:n.success?"sent":"failed",error:n.error?.message,metadata:{...t.data,...i.category?{category:i.category}:{}},sentAt:r}}function ye(e){let t=M.notification?.tracking;if(t?.enabled===!1)return!1;let n=t?.channels;if(!n||n.length===0)return!0;return n.includes(e)}async function P(e){if(!ye(e.channel))return;let t=F();try{await ge.insertInto("notification_deliveries").values({user_id:e.userId??null,channel:e.channel,recipient:e.recipient,subject:e.subject??null,body:M.notification?.tracking?.body===!1?"":e.body,status:e.status,error:e.error??null,metadata:e.metadata?JSON.stringify(e.metadata):null,sent_at:e.sentAt,created_at:t,updated_at:t}).execute()}catch(n){let i=n instanceof Error?n.message:String(n);he.warn(`[notifications] could not record ${e.channel} delivery: ${i}`)}}import{templateByName as we}from"@stacksjs/email";var g="You are receiving this because these emails are turned on in your settings.",z="Reply STOP to opt out.",Ne=/https?:\/\/[^\s<>"']+[^\s<>"'.,;:!?)\]]/g;function q(e){return String(e??"").replace(/\r\n?/g,`
2
+ import{log as U}from"@stacksjs/cli";import{notification as Oe}from"@stacksjs/config";import{mail as ue}from"@stacksjs/email";import{log as A}from"@stacksjs/cli";var p={async send(e){let t=e.event??"notification",n=e.channel;if(!n)n=e.userId!==void 0?`private-user.${e.userId}`:"notifications";try{let i=await import("@stacksjs/realtime").catch(()=>null),r=i?.emit,a=i?.getServer;if(!r||!a)return{delivered:!1,channel:n,event:t,reason:"@stacksjs/realtime is not available"};if(!a())return{delivered:!1,channel:n,event:t,reason:"realtime server not running"};let s=e.private??e.userId!==void 0;return r(n,t,e.data,{private:s}),A.info(`[notifications] broadcast to '${n}' (${t})`),{delivered:!0,channel:n,event:t}}catch(i){let r=i instanceof Error?i.message:String(i);return A.warn(`[notifications] broadcast failed: ${r}`),{delivered:!1,channel:n,event:t,reason:r}}}};import*as j from"@stacksjs/chat";import{log as I}from"@stacksjs/cli";import{createHash as me}from"crypto";import{db as u,enqueueAfterCommit as pe,lockRow as be,sqlDateTime as T}from"@stacksjs/database/runtime";import{emit as ge,getServer as he}from"@stacksjs/realtime";function B(e){return`private-user.${e}`}function x(e,t,n){let i=()=>{try{if(he())ge(B(e),t,n)}catch{I.warn("Notification inbox broadcast failed; the persisted inbox remains available")}};if(!pe(i))i()}async function O(e,t){let n=T(),r=await u.insertInto("notifications").values({user_id:e.userId,...t?{uuid:t}:{},type:e.type,data:JSON.stringify(e.data),read_at:null,created_at:n,updated_at:n}).returning("id").execute(),a=Array.isArray(r)?r[0]:r,s=Number(a?.id??a?.insertId??a?.lastInsertRowid??a?.lastInsertId??0);if(!Number.isSafeInteger(s)||s<=0)throw Error("Database notification did not return a generated id");if(I.info(`Database notification sent to user ${e.userId}: ${e.type}`),e.broadcast!==!1)x(e.userId,"notifications.created",{id:s});return{id:s,user_id:e.userId,type:e.type,data:JSON.stringify(e.data),read_at:null,created_at:n,updated_at:n}}var b={async send(e){if(e.idempotencyKey!==void 0){if(!e.idempotencyKey||e.idempotencyKey.length>255)throw Error("A notification idempotency key must contain 1 to 255 characters");let t=me("sha256").update(JSON.stringify([e.userId,e.type,e.idempotencyKey])).digest("hex"),n=`${t.slice(0,8)}-${t.slice(8,12)}-8${t.slice(13,16)}-8${t.slice(17,20)}-${t.slice(20,32)}`;return u.transaction(async(i)=>{if(!await be(i,"users",{id:e.userId}))throw Error("Notification recipient not found");let a=await u.selectFrom("notifications").selectAll().where("user_id","=",e.userId).where("uuid","=",n).executeTakeFirst();if(a)return{...a,id:Number(a.id),user_id:Number(a.user_id)};return O(e,n)})}return O(e)},async getUserNotifications(e){return(await u.selectFrom("notifications").selectAll().where("user_id","=",e).orderBy("created_at","desc").execute()).map((n)=>({...n,id:Number(n.id),user_id:Number(n.user_id)}))},async getUnreadNotifications(e){return(await u.selectFrom("notifications").selectAll().where("user_id","=",e).whereNull("read_at").orderBy("created_at","desc").execute()).map((n)=>({...n,id:Number(n.id),user_id:Number(n.user_id)}))},async markAsRead(e,t){let n=u.updateTable("notifications").set({read_at:T()}).where("id","=",e).whereNull("read_at");if(t!==void 0)n=n.where("user_id","=",t);if(await n.execute(),t)x(t,"notifications.read",{id:e})},async markAllAsRead(e){await u.updateTable("notifications").set({read_at:T()}).where("user_id","=",e).whereNull("read_at").execute(),x(e,"notifications.read",{all:!0})},async unreadCount(e){let t=await u.selectFrom("notifications").select(u.fn.countAll().as("count")).where("user_id","=",e).whereNull("read_at").executeTakeFirst();return Number(t?.count??0)},async deleteNotification(e,t){let n=u.deleteFrom("notifications").where("id","=",e);if(t!==void 0)n=n.where("user_id","=",t);await n.execute()},async deleteAllNotifications(e){await u.deleteFrom("notifications").where("user_id","=",e).execute()}};import*as L from"@stacksjs/push";import*as g from"@stacksjs/sms";import{log as ye}from"@stacksjs/cli";import{config as M}from"@stacksjs/config";import{db as Ne,sqlDateTime as F}from"@stacksjs/database/runtime";function K(e,t){switch(t){case"email":return e.email||"";case"sms":return e.phone||"";case"push":return Array.isArray(e.pushTokens)?e.pushTokens.join(", "):e.pushTokens||"";case"broadcast":return e.broadcastChannel||(e.userId?`private-user.${e.userId}`:"notifications");case"database":return e.userId?`User #${e.userId}`:"";case"chat":return Array.isArray(e.chatRecipient)?e.chatRecipient.join(", "):e.chatRecipient||""}}function k(e,t,n,i,r=F()){return{userId:e.userId,channel:n.channel,recipient:K(e,n.channel),subject:t.subject,body:t.body,status:n.success?"sent":"failed",error:n.error?.message,metadata:{...t.data,...i.category?{category:i.category}:{}},sentAt:r}}function we(e){let t=M.notification?.tracking;if(t?.enabled===!1)return!1;let n=t?.channels;if(!n||n.length===0)return!0;return n.includes(e)}async function P(e){if(!we(e.channel))return;let t=F();try{await Ne.insertInto("notification_deliveries").values({user_id:e.userId??null,channel:e.channel,recipient:e.recipient,subject:e.subject??null,body:M.notification?.tracking?.body===!1?"":e.body,status:e.status,error:e.error??null,metadata:e.metadata?JSON.stringify(e.metadata):null,sent_at:e.sentAt,created_at:t,updated_at:t}).execute()}catch(n){let i=n instanceof Error?n.message:String(n);ye.warn(`[notifications] could not record ${e.channel} delivery: ${i}`)}}import{templateByName as Te}from"@stacksjs/email";var h="You are receiving this because these emails are turned on in your settings.",z="Reply STOP to opt out.",xe=/https?:\/\/[^\s<>"']+[^\s<>"'.,;:!?)\]]/g;function q(e){return String(e??"").replace(/\r\n?/g,`
3
3
  `).split(/\n\s*\n/).map((t)=>t.trim()).filter(Boolean).map((t)=>{let n=[];return t.split(`
4
- `).forEach((i,r)=>{if(r>0)n.push({br:!0});let a=0;for(let s of i.matchAll(Ne)){if(s.index>a)n.push({text:i.slice(a,s.index)});n.push({url:s[0]}),a=s.index+s[0].length}if(a<i.length)n.push({text:i.slice(a)})}),n})}function d(e){return String(e??"").replace(/[&<>"']/g,(t)=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t])}async function Y(e){let t=e.unsubscribe?.url?e.unsubscribe:void 0;try{let{html:r}=await we("notification",{subject:e.subject??"",variables:{subject:e.subject??"",paragraphs:q(e.body),preheader:String(e.body??"").replace(/\s+/g," ").trim().slice(0,140),...e.action?.url?{actionUrl:e.action.url,actionLabel:e.action.label||"Open"}:{},...t?{unsubscribeUrl:t.url,unsubscribeNotice:t.notice||g,...t.preferencesUrl?{preferencesUrl:t.preferencesUrl}:{}}:{}}});if(r)return r}catch{}let n=e.action?.url?`<p><a href="${d(e.action.url)}">${d(e.action.label||e.action.url)}</a></p>`:"",i=t?`<p>${d(t.notice||g)} <a href="${d(t.url)}">Unsubscribe</a></p>`:"";return`<p>${d(e.body)}</p>${n}${i}`}function V(e){let t=e.action?.url?`${e.body}
4
+ `).forEach((i,r)=>{if(r>0)n.push({br:!0});let a=0;for(let s of i.matchAll(xe)){if(s.index>a)n.push({text:i.slice(a,s.index)});n.push({url:s[0]}),a=s.index+s[0].length}if(a<i.length)n.push({text:i.slice(a)})}),n})}function d(e){return String(e??"").replace(/[&<>"']/g,(t)=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t])}async function Y(e){let t=e.unsubscribe?.url?e.unsubscribe:void 0;try{let{html:r}=await Te("notification",{subject:e.subject??"",variables:{subject:e.subject??"",paragraphs:q(e.body),preheader:String(e.body??"").replace(/\s+/g," ").trim().slice(0,140),...e.action?.url?{actionUrl:e.action.url,actionLabel:e.action.label||"Open"}:{},...t?{unsubscribeUrl:t.url,unsubscribeNotice:t.notice||h,...t.preferencesUrl?{preferencesUrl:t.preferencesUrl}:{}}:{}}});if(r)return r}catch{}let n=e.action?.url?`<p><a href="${d(e.action.url)}">${d(e.action.label||e.action.url)}</a></p>`:"",i=t?`<p>${d(t.notice||h)} <a href="${d(t.url)}">Unsubscribe</a></p>`:"";return`<p>${d(e.body)}</p>${n}${i}`}function V(e){let t=e.action?.url?`${e.body}
5
5
 
6
6
  ${e.action.label||"Open"}: ${e.action.url}`:e.body;if(e.unsubscribe?.url)t+=`
7
7
 
8
- ${e.unsubscribe.notice||g}
9
- Unsubscribe: ${e.unsubscribe.url}`;return t}function W(e){if(!e?.url)return{};return{"List-Unsubscribe":`<${e.oneClickUrl||e.url}>`,...e.oneClickUrl?{"List-Unsubscribe-Post":"List-Unsubscribe=One-Click"}:{}}}async function v(e,t){let n={...t.headers,...W(t.unsubscribe)};return{to:e,subject:t.subject??"",text:V(t),html:await Y(t),...Object.keys(n).length?{headers:n}:{}}}function _(e,t){let n=String(e.body??"");if(e.smsOptOut===!1)return n;let i=typeof e.smsOptOut==="string"?e.smsOptOut.trim():t==="marketing"?z:"";if(!i||n.toLowerCase().includes(i.toLowerCase()))return n;return`${n.trimEnd()}
8
+ ${e.unsubscribe.notice||h}
9
+ Unsubscribe: ${e.unsubscribe.url}`;return t}function H(e){if(!e?.url)return{};return{"List-Unsubscribe":`<${e.oneClickUrl||e.url}>`,...e.oneClickUrl?{"List-Unsubscribe-Post":"List-Unsubscribe=One-Click"}:{}}}async function _(e,t){let n={...t.headers,...H(t.unsubscribe)};return{to:e,subject:t.subject??"",text:V(t),html:await Y(t),...Object.keys(n).length?{headers:n}:{}}}var W=30;function G(e){let t=String(e??"").replace(/\s+/g," ").trim().slice(0,W).trim();return t?`${t}: `:""}function v(e,t){let n=String(e.body??""),i=G(e.smsSender);if(i&&!n.trimStart().toLowerCase().startsWith(i.trim().toLowerCase()))n=`${i}${n.trimStart()}`;if(e.smsOptOut===!1)return n;let r=typeof e.smsOptOut==="string"?e.smsOptOut.trim():t==="marketing"?z:"";if(!r||n.toLowerCase().includes(r.toLowerCase()))return n;return`${n.trimEnd()}
10
10
 
11
- ${i}`}import{log as Te}from"@stacksjs/cli";import{db as f,lockRow as H,sqlDateTime as xe}from"@stacksjs/database/runtime";var y="notification_preferences";function R(e,t){return t===null?e.whereNull("category"):e.where("category","=",t)}async function E(e,t){let n=new Map;try{let r=await R(f.selectFrom(y).select(["channel","enabled","category"]).where("user_id","=",e),t??null).execute();for(let a of r)n.set(a.channel,a.enabled===!0||a.enabled===1)}catch(i){Te.debug?.(`[notifications] preferences lookup failed (table missing?): ${i.message}`)}return n}async function C(e,t,n,i){await f.transaction(async(r)=>{await H(r,"users",{id:e}),await J(e,t,n,i)})}async function J(e,t,n,i){if(!Number.isSafeInteger(e)||e<=0||typeof n!=="boolean")throw Error("A notification preference needs a valid user id and enabled boolean");let r=xe(),a=i??null,s=f.selectFrom(y).select(["id"]).where("user_id","=",e).where("channel","=",t),o=await R(s,a).executeTakeFirst();if(o?.id){await f.updateTable(y).set({enabled:n,updated_at:r}).where("id","=",o.id).execute();return}await f.insertInto(y).values({user_id:e,channel:t,enabled:n,category:a,created_at:r,updated_at:r}).execute()}async function ke(e,t){await f.transaction(async(n)=>{await H(n,"users",{id:e});for(let i of t)await J(e,i.channel,i.enabled,i.category)})}async function D(e,t,n){let i=await E(e,n);if(n!==void 0){let r=await E(e,void 0);for(let[a,s]of r)if(!s||!i.has(a))i.set(a,s)}return t.filter((r)=>{let a=i.get(r);return a===void 0?!0:a===!0})}import w from"process";import{createHmac as Pe,timingSafeEqual as ve}from"crypto";import{db as _e}from"@stacksjs/database/runtime";var Q=["email","sms"];function Ee(){let e=w.env.APP_KEY;if((!e||e.length<16)&&(w.env.APP_ENV==="production"||w.env.NODE_ENV==="production"))throw Error("[notifications/unsubscribe] APP_KEY is missing or too short (need at least 16 characters). Cannot sign unsubscribe links.");return e||"stacks-default-key-dev-only-do-not-use-prod"}function Z(e){return Pe("sha256",Ee()).update(e).digest("hex")}function G(e,t,n="marketing"){if(!Number.isSafeInteger(e)||e<=0)throw Error("An unsubscribe token needs a positive user id");if(!Q.includes(t))throw Error(`Unsupported unsubscribe channel: ${t}`);if(!n||n.includes(":"))throw Error("An unsubscribe category must be non-empty and contain no colon");let i=`${e}:${t}:${n}`;return Buffer.from(`${i}:${Z(i)}`).toString("base64url")}function X(e){try{let t=Buffer.from(String(e??""),"base64url").toString("utf8");if(!e||Buffer.from(t).toString("base64url")!==e)return null;let n=t.split(":");if(n.length!==4)return null;let[i,r,a,s]=n;if(!i||!a||!s||!Q.includes(r))return null;let o=Z(`${i}:${r}:${a}`);if(s.length!==o.length||!ve(Buffer.from(s),Buffer.from(o)))return null;let c=Number(i);return Number.isSafeInteger(c)&&c>0&&String(c)===i?{userId:c,channel:r,category:a}:null}catch{return null}}function Re(e,t={}){let n=String(t.baseUrl||w.env.APP_URL||"http://localhost").replace(/\/$/,""),i=G(e,t.channel??"email",t.category??"marketing"),r=(a)=>a.startsWith("/")?a:`/${a}`;return{url:`${n}${r(t.pagePath??"/unsubscribe")}?token=${i}`,oneClickUrl:`${n}${r(t.endpointPath??"/api/notifications/unsubscribe")}?token=${i}`,...t.preferencesPath===!1?{}:{preferencesUrl:`${n}${r(t.preferencesPath??"/settings")}`},...t.notice?{notice:t.notice}:{}}}async function Ce(e){let t=new URL(e.url,"http://localhost").searchParams.get("token");if(t)return t;let n=await e.json?.().catch(()=>({}));return String(n?.token||"")}async function De(e){let t=X(e);if(!t)return null;if(await _e.selectFrom("users").select(["id"]).where("id","=",t.userId).executeTakeFirst())await C(t.userId,t.channel,!1,t.category);return t}function ee(e={}){let t=e.resetPassword??"/forgot-password";return{password_changed:{copy:{en:{subject:"Your {{app}} password was changed",body:`The password for your {{app}} account was changed.
11
+ ${r}`}import{log as ke}from"@stacksjs/cli";import{db as f,lockRow as J,sqlDateTime as Pe}from"@stacksjs/database/runtime";var y="notification_preferences";function R(e,t){return t===null?e.whereNull("category"):e.where("category","=",t)}async function E(e,t){let n=new Map;try{let r=await R(f.selectFrom(y).select(["channel","enabled","category"]).where("user_id","=",e),t??null).execute();for(let a of r)n.set(a.channel,a.enabled===!0||a.enabled===1)}catch(i){ke.debug?.(`[notifications] preferences lookup failed (table missing?): ${i.message}`)}return n}async function S(e,t,n,i){await f.transaction(async(r)=>{await J(r,"users",{id:e}),await Q(e,t,n,i)})}async function Q(e,t,n,i){if(!Number.isSafeInteger(e)||e<=0||typeof n!=="boolean")throw Error("A notification preference needs a valid user id and enabled boolean");let r=Pe(),a=i??null,s=f.selectFrom(y).select(["id"]).where("user_id","=",e).where("channel","=",t),o=await R(s,a).executeTakeFirst();if(o?.id){await f.updateTable(y).set({enabled:n,updated_at:r}).where("id","=",o.id).execute();return}await f.insertInto(y).values({user_id:e,channel:t,enabled:n,category:a,created_at:r,updated_at:r}).execute()}async function _e(e,t){await f.transaction(async(n)=>{await J(n,"users",{id:e});for(let i of t)await Q(e,i.channel,i.enabled,i.category)})}async function C(e,t,n){let i=await E(e,n);if(n!==void 0){let r=await E(e,void 0);for(let[a,s]of r)if(!s||!i.has(a))i.set(a,s)}return t.filter((r)=>{let a=i.get(r);return a===void 0?!0:a===!0})}import N from"process";import{createHmac as ve,timingSafeEqual as Ee}from"crypto";import{db as Re}from"@stacksjs/database/runtime";var Z=["email","sms"];function Se(){let e=N.env.APP_KEY;if((!e||e.length<16)&&(N.env.APP_ENV==="production"||N.env.NODE_ENV==="production"))throw Error("[notifications/unsubscribe] APP_KEY is missing or too short (need at least 16 characters). Cannot sign unsubscribe links.");return e||"stacks-default-key-dev-only-do-not-use-prod"}function X(e){return ve("sha256",Se()).update(e).digest("hex")}function ee(e,t,n="marketing"){if(!Number.isSafeInteger(e)||e<=0)throw Error("An unsubscribe token needs a positive user id");if(!Z.includes(t))throw Error(`Unsupported unsubscribe channel: ${t}`);if(!n||n.includes(":"))throw Error("An unsubscribe category must be non-empty and contain no colon");let i=`${e}:${t}:${n}`;return Buffer.from(`${i}:${X(i)}`).toString("base64url")}function te(e){try{let t=Buffer.from(String(e??""),"base64url").toString("utf8");if(!e||Buffer.from(t).toString("base64url")!==e)return null;let n=t.split(":");if(n.length!==4)return null;let[i,r,a,s]=n;if(!i||!a||!s||!Z.includes(r))return null;let o=X(`${i}:${r}:${a}`);if(s.length!==o.length||!Ee(Buffer.from(s),Buffer.from(o)))return null;let c=Number(i);return Number.isSafeInteger(c)&&c>0&&String(c)===i?{userId:c,channel:r,category:a}:null}catch{return null}}function Ce(e,t={}){let n=String(t.baseUrl||N.env.APP_URL||"http://localhost").replace(/\/$/,""),i=ee(e,t.channel??"email",t.category??"marketing"),r=(a)=>a.startsWith("/")?a:`/${a}`;return{url:`${n}${r(t.pagePath??"/unsubscribe")}?token=${i}`,oneClickUrl:`${n}${r(t.endpointPath??"/api/notifications/unsubscribe")}?token=${i}`,...t.preferencesPath===!1?{}:{preferencesUrl:`${n}${r(t.preferencesPath??"/settings")}`},...t.notice?{notice:t.notice}:{}}}async function De(e){let t=new URL(e.url,"http://localhost").searchParams.get("token");if(t)return t;let n=await e.json?.().catch(()=>({}));return String(n?.token||"")}async function Ue(e){let t=te(e);if(!t)return null;if(await Re.selectFrom("users").select(["id"]).where("id","=",t.userId).executeTakeFirst())await S(t.userId,t.channel,!1,t.category);return t}function ne(e={}){let t=e.resetPassword??"/forgot-password";return{password_changed:{copy:{en:{subject:"Your {{app}} password was changed",body:`The password for your {{app}} account was changed.
12
12
 
13
13
  If you did this, there\u2019s nothing else to do. If you didn\u2019t, reset your password right away and let us know at {{support_email}}.`},de:{subject:"Dein {{app}}-Passwort wurde ge\xE4ndert",body:`Das Passwort f\xFCr dein {{app}}-Konto wurde ge\xE4ndert.
14
14
 
@@ -24,7 +24,7 @@ Hast du das nicht veranlasst, schreib uns an {{support_email}}.`}}},account_crea
24
24
 
25
25
  Set a password to sign in. The link works for {{days}} days; after that, use \u201CForgot password\u201D on the sign-in page.`},de:{subject:"{{inviter}} hat dich zu {{app}} hinzugef\xFCgt",body:`{{inviter}} hat f\xFCr dich ein {{app}}-Konto mit dieser E-Mail-Adresse angelegt.
26
26
 
27
- Lege ein Passwort fest, um dich anzumelden. Der Link gilt {{days}} Tage; danach nutze \u201EPasswort vergessen\u201C auf der Anmeldeseite.`}},action:{path:"{{set_password_url}}",label:{en:"Set your password",de:"Passwort festlegen"}}}}}function te(e={}){let t=e.orders??"/orders",n=e.sales??"/orders";return{order_receipt:{copy:{en:{subject:"Your receipt from {{seller}}",body:`Thanks for your purchase from {{seller}}.
27
+ Lege ein Passwort fest, um dich anzumelden. Der Link gilt {{days}} Tage; danach nutze \u201EPasswort vergessen\u201C auf der Anmeldeseite.`}},action:{path:"{{set_password_url}}",label:{en:"Set your password",de:"Passwort festlegen"}}}}}function ie(e={}){let t=e.orders??"/orders",n=e.sales??"/orders";return{order_receipt:{copy:{en:{subject:"Your receipt from {{seller}}",body:`Thanks for your purchase from {{seller}}.
28
28
 
29
29
  {{lines}}
30
30
  Total: {{total}}
@@ -46,4 +46,4 @@ Bestellung {{order_number}}.`}},action:{path:n,label:{en:"View your sales",de:"V
46
46
 
47
47
  Your balance is now {{balance}}. You can use it on your next purchase.`},de:{subject:"{{amount}} Guthaben von {{seller}}",body:`{{seller}} hat deinem Konto {{amount}} gutgeschrieben: {{reason}}
48
48
 
49
- Dein Guthaben betr\xE4gt jetzt {{balance}}. Du kannst es bei deinem n\xE4chsten Einkauf verwenden.`}}}}}function ne(e={}){let t=e.billing??"/billing";return{subscription_trial_ending:{copy:{en:{subject:"Your {{app}} trial ends on {{date}}",body:"Your {{plan}} trial ends on {{date}}. To keep using {{plan}}, check that your payment details are up to date. To stop, cancel before then and you won\u2019t be charged."},de:{subject:"Deine {{app}}-Testphase endet am {{date}}",body:"Deine Testphase f\xFCr {{plan}} endet am {{date}}. Wenn du {{plan}} weiter nutzen willst, pr\xFCfe, ob deine Zahlungsdaten aktuell sind. Wenn nicht, k\xFCndige vorher, dann wird nichts berechnet."}},action:{path:t,label:{en:"Manage billing",de:"Abrechnung verwalten"}}},subscription_payment_failed:{copy:{en:{subject:"Payment for {{plan}} failed",body:"We couldn\u2019t charge your payment method for your {{app}} {{plan}} plan. Update it to keep {{plan}} active."},de:{subject:"Zahlung f\xFCr {{plan}} fehlgeschlagen",body:"Wir konnten deine Zahlungsmethode f\xFCr deinen {{app}}-Tarif {{plan}} nicht belasten. Aktualisiere sie, damit {{plan}} aktiv bleibt."}},action:{path:t,label:{en:"Update payment method",de:"Zahlungsmethode aktualisieren"}}},subscription_ended:{copy:{en:{subject:"Your {{app}} {{plan}} plan has ended",body:"Your {{plan}} plan ended on {{date}}. Your account and data are still here; choose a plan again whenever you\u2019re ready."},de:{subject:"Dein {{app}}-Tarif {{plan}} ist beendet",body:"Dein Tarif {{plan}} endete am {{date}}. Dein Konto und deine Daten bleiben erhalten; du kannst jederzeit wieder einen Tarif w\xE4hlen."}},action:{path:t,label:{en:"Choose a plan",de:"Tarif w\xE4hlen"}}}}}var ie=/\{\{\s*([a-z0-9_]+)\s*\}\}/gi;function Ue(e){return e}function N(e,t={}){return e.replace(ie,(n,i)=>{let r=t[i];return r==null?"":String(r)})}function re(e){return[...new Set([...e.matchAll(ie)].map((t)=>t[1]))]}function ae(e,t,n="en"){let i=String(t||"").trim();if(i&&e.includes(i))return i;let r=i.split(/[-_]/)[0]?.toLowerCase();if(r&&e.includes(r))return r;if(e.includes(n))return n;let a=e[0];if(!a)throw Error("A notification template needs copy in at least one language");return a}function Se(e,t){if(/^https?:\/\//i.test(t))return t;let n=String(e||"").replace(/\/+$/,"");if(!t)return n;return`${n}${t.startsWith("/")?"":"/"}${t}`}function oe(e,t={},n={}){let i=n.fallbackLocale||"en",r=ae(Object.keys(e.copy),n.locale,i),a=e.copy[r],s={subject:N(a.subject,t),body:N(a.body,t)},o=e.action;if(o){let c=re(o.path).some((p)=>t[p]==null||t[p]===""),l=o.label[r]??o.label[i]??Object.values(o.label)[0];if(!c&&l)s.action={label:l,url:Se(n.baseUrl,N(o.path,t))}}return s}function Ae(e,t={}){let n=Object.keys(e),i=()=>typeof t.baseUrl==="function"?t.baseUrl():t.baseUrl;return{templates:e,names:n,has:(r)=>Object.hasOwn(e,r),render(r,a={},s){let o=e[r];if(!o)throw Error(`Unknown notification template "${String(r)}"`);return oe(o,a,{locale:s,fallbackLocale:t.fallbackLocale,baseUrl:i()})}}}var je=$e;function Oe(e){if(!e.success)throw Error(e.message||`Email delivery failed through ${e.provider||"the configured provider"}.`)}function Et(e){if(!e.length)throw Error("No notification channels were delivered");let t=e.filter((n)=>!n.success);if(t.length){let n=t.map((i)=>i.error instanceof Error?i.error:Error(`Notification delivery failed through ${i.channel}`));if(n.length===1)throw n[0];throw AggregateError(n,n.map((i)=>i.message).join("; "))}}function U(e){if(!e.success)throw Error(e.error||e.message||"Notification provider rejected delivery")}function ce(e){let t=e||"slack";if(!["slack","discord","teams"].includes(t))throw Error(`Unsupported chat driver: ${t}`);return j[t]}function ue(e){if(e)try{return se.use(e)}catch(t){S.warn(`[notifications] email driver '${e}' not registered - falling back to default mail singleton (${t.message})`)}return se}function le(e){if(!e)return h;if(e!=="twilio"&&e!=="vonage")throw Error(`Unsupported SMS driver: ${e}`);let t=e;return{send:async(n)=>(await h.init(),h.getDriver(t).send(n))}}function Ie(){return L}function Be(){return b}function Rt(){return m}function Le(e,t){let n=e||je?.default||"email",i=t;switch(n){case"email":return ue(i);case"chat":return ce(i);case"sms":return le(i);case"database":return Be();default:throw Error(`Notification type "${n}" is not supported`)}}async function Ct(e,t,n=["email"],i={}){let r=[...new Set(n)];if(e.userId&&!i.ignorePreferences)try{r=await D(e.userId,r,i.category)}catch(o){S.warn(`[notify] preferences filter failed, sending all channels: ${o.message}`)}let s=(await Promise.allSettled(r.map(async(o)=>{switch(o){case"email":{if(!e.email)throw Error("[notify] email channel requires recipient.email");let l=await ue().send(await v(e.email,t));Oe(l);break}case"sms":{if(!e.phone)throw Error("[notify] sms channel requires recipient.phone");if(i.ignorePreferences!==!0){let{isPhoneOptedOut:l}=await import("@stacksjs/sms");if(await l(e.phone))throw Error("[notify] sms recipient has opted out (STOP)")}let c=le();U(await c.send({to:e.phone,body:_(t,i.category)}));break}case"chat":{if(!e.chatRecipient||Array.isArray(e.chatRecipient)&&!e.chatRecipient.length)throw Error("[notify] chat channel requires recipient.chatRecipient");let c=ce();U(await c.send({to:e.chatRecipient,content:t.body,subject:t.subject,data:t.data}));break}case"database":{if(!e.userId)throw Error("[notify] database channel requires recipient.userId");await b.send({userId:e.userId,type:t.subject||"notification",data:{body:t.body,...t.data}});break}case"push":{if(!e.pushTokens||Array.isArray(e.pushTokens)&&e.pushTokens.length===0)throw Error("[notify] push channel requires recipient.pushTokens");let c=Ie();U(await c.send(e.pushTokens,{title:t.subject,body:t.body,data:t.data},{driver:e.pushDriver}));break}case"broadcast":{let c=await m.send({channel:e.broadcastChannel,userId:e.userId,event:t.subject??"notification",data:{body:t.body,...t.data}});if(!c.delivered)throw Error(c.reason||"Broadcast notification was not delivered");break}default:throw Error(`Unsupported notification channel: ${o}`)}}))).map((o,c)=>{let l=r[c];if(!l)throw Error(`Missing notification channel for result ${c}`);if(o.status==="rejected"){let p=o.reason instanceof Error?o.reason.message:String(o.reason);S.warn(`[notify] ${l} channel failed: ${p}`)}return{channel:l,success:o.status==="fulfilled",error:o.status==="rejected"?o.reason instanceof Error?o.reason:Error(String(o.reason)):void 0}});return await Promise.all(s.map((o)=>P(k(e,t,o,i)))),s}function Dt(){return Le()}export{m as BroadcastNotificationDriver,z as DEFAULT_SMS_OPT_OUT,g as DEFAULT_UNSUBSCRIBE_NOTICE,b as DatabaseNotificationDriver,ee as accountNotificationTemplates,ke as bulkSetPreferences,te as commerceNotificationTemplates,Ae as createNotificationTemplates,Ue as defineNotificationTemplates,q as emailParagraphs,Oe as ensureSuccessfulEmailResult,Et as ensureSuccessfulNotificationResults,D as filterChannelsByPreferences,E as getNotificationPreferences,N as interpolateNotificationText,W as listUnsubscribeHeaders,k as makeDeliveryRecord,Dt as notification,B as notificationChannel,Y as notificationEmailHtml,v as notificationEmailMessage,V as notificationEmailText,_ as notificationSmsBody,re as notificationTemplateVariables,Re as notificationUnsubscribeLinks,G as notificationUnsubscribeToken,Ct as notify,X as parseNotificationUnsubscribeToken,P as recordNotificationDelivery,oe as renderNotificationTemplate,K as resolveDeliveryRecipient,ae as resolveNotificationLocale,C as setNotificationPreference,ne as subscriptionNotificationTemplates,Ce as unsubscribeTokenFromRequest,De as unsubscribeWithToken,Rt as useBroadcast,ce as useChat,Be as useDatabase,ue as useEmail,Le as useNotification,Ie as usePush,le as useSMS,R as wherePreferenceCategory};
49
+ Dein Guthaben betr\xE4gt jetzt {{balance}}. Du kannst es bei deinem n\xE4chsten Einkauf verwenden.`}}}}}function re(e={}){let t=e.billing??"/billing";return{subscription_trial_ending:{copy:{en:{subject:"Your {{app}} trial ends on {{date}}",body:"Your {{plan}} trial ends on {{date}}. To keep using {{plan}}, check that your payment details are up to date. To stop, cancel before then and you won\u2019t be charged."},de:{subject:"Deine {{app}}-Testphase endet am {{date}}",body:"Deine Testphase f\xFCr {{plan}} endet am {{date}}. Wenn du {{plan}} weiter nutzen willst, pr\xFCfe, ob deine Zahlungsdaten aktuell sind. Wenn nicht, k\xFCndige vorher, dann wird nichts berechnet."}},action:{path:t,label:{en:"Manage billing",de:"Abrechnung verwalten"}}},subscription_payment_failed:{copy:{en:{subject:"Payment for {{plan}} failed",body:"We couldn\u2019t charge your payment method for your {{app}} {{plan}} plan. Update it to keep {{plan}} active."},de:{subject:"Zahlung f\xFCr {{plan}} fehlgeschlagen",body:"Wir konnten deine Zahlungsmethode f\xFCr deinen {{app}}-Tarif {{plan}} nicht belasten. Aktualisiere sie, damit {{plan}} aktiv bleibt."}},action:{path:t,label:{en:"Update payment method",de:"Zahlungsmethode aktualisieren"}}},subscription_ended:{copy:{en:{subject:"Your {{app}} {{plan}} plan has ended",body:"Your {{plan}} plan ended on {{date}}. Your account and data are still here; choose a plan again whenever you\u2019re ready."},de:{subject:"Dein {{app}}-Tarif {{plan}} ist beendet",body:"Dein Tarif {{plan}} endete am {{date}}. Dein Konto und deine Daten bleiben erhalten; du kannst jederzeit wieder einen Tarif w\xE4hlen."}},action:{path:t,label:{en:"Choose a plan",de:"Tarif w\xE4hlen"}}}}}var ae=/\{\{\s*([a-z0-9_]+)\s*\}\}/gi;function Ae(e){return e}function w(e,t={}){return e.replace(ae,(n,i)=>{let r=t[i];return r==null?"":String(r)})}function oe(e){return[...new Set([...e.matchAll(ae)].map((t)=>t[1]))]}function se(e,t,n="en"){let i=String(t||"").trim();if(i&&e.includes(i))return i;let r=i.split(/[-_]/)[0]?.toLowerCase();if(r&&e.includes(r))return r;if(e.includes(n))return n;let a=e[0];if(!a)throw Error("A notification template needs copy in at least one language");return a}function $e(e,t){if(/^https?:\/\//i.test(t))return t;let n=String(e||"").replace(/\/+$/,"");if(!t)return n;return`${n}${t.startsWith("/")?"":"/"}${t}`}function ce(e,t={},n={}){let i=n.fallbackLocale||"en",r=se(Object.keys(e.copy),n.locale,i),a=e.copy[r],s={subject:w(a.subject,t),body:w(a.body,t)},o=e.action;if(o){let c=oe(o.path).some((m)=>t[m]==null||t[m]===""),l=o.label[r]??o.label[i]??Object.values(o.label)[0];if(!c&&l)s.action={label:l,url:$e(n.baseUrl,w(o.path,t))}}return s}function je(e,t={}){let n=Object.keys(e),i=()=>typeof t.baseUrl==="function"?t.baseUrl():t.baseUrl;return{templates:e,names:n,has:(r)=>Object.hasOwn(e,r),render(r,a={},s){let o=e[r];if(!o)throw Error(`Unknown notification template "${String(r)}"`);return ce(o,a,{locale:s,fallbackLocale:t.fallbackLocale,baseUrl:i()})}}}var Ie=Oe;function Be(e){if(!e.success)throw Error(e.message||`Email delivery failed through ${e.provider||"the configured provider"}.`)}function St(e){if(!e.length)throw Error("No notification channels were delivered");let t=e.filter((n)=>!n.success);if(t.length){let n=t.map((i)=>i.error instanceof Error?i.error:Error(`Notification delivery failed through ${i.channel}`));if(n.length===1)throw n[0];throw AggregateError(n,n.map((i)=>i.message).join("; "))}}function D(e){if(!e.success)throw Error(e.error||e.message||"Notification provider rejected delivery")}function le(e){let t=e||"slack";if(!["slack","discord","teams"].includes(t))throw Error(`Unsupported chat driver: ${t}`);return j[t]}function fe(e){if(e)try{return ue.use(e)}catch(t){U.warn(`[notifications] email driver '${e}' not registered - falling back to default mail singleton (${t.message})`)}return ue}function de(e){if(!e)return g;if(e!=="twilio"&&e!=="vonage")throw Error(`Unsupported SMS driver: ${e}`);let t=e;return{send:async(n)=>(await g.init(),g.getDriver(t).send(n))}}function Le(){return L}function Me(){return b}function Ct(){return p}function Fe(e,t){let n=e||Ie?.default||"email",i=t;switch(n){case"email":return fe(i);case"chat":return le(i);case"sms":return de(i);case"database":return Me();default:throw Error(`Notification type "${n}" is not supported`)}}async function Dt(e,t,n=["email"],i={}){let r=[...new Set(n)];if(e.userId&&!i.ignorePreferences)try{r=await C(e.userId,r,i.category)}catch(o){U.warn(`[notify] preferences filter failed, sending all channels: ${o.message}`)}let s=(await Promise.allSettled(r.map(async(o)=>{switch(o){case"email":{if(!e.email)throw Error("[notify] email channel requires recipient.email");let l=await fe().send(await _(e.email,t));Be(l);break}case"sms":{if(!e.phone)throw Error("[notify] sms channel requires recipient.phone");if(i.ignorePreferences!==!0){let{isPhoneOptedOut:l}=await import("@stacksjs/sms");if(await l(e.phone))throw Error("[notify] sms recipient has opted out (STOP)")}let c=de();D(await c.send({to:e.phone,body:v(t,i.category)}));break}case"chat":{if(!e.chatRecipient||Array.isArray(e.chatRecipient)&&!e.chatRecipient.length)throw Error("[notify] chat channel requires recipient.chatRecipient");let c=le();D(await c.send({to:e.chatRecipient,content:t.body,subject:t.subject,data:t.data}));break}case"database":{if(!e.userId)throw Error("[notify] database channel requires recipient.userId");await b.send({userId:e.userId,type:t.subject||"notification",data:{body:t.body,...t.data}});break}case"push":{if(!e.pushTokens||Array.isArray(e.pushTokens)&&e.pushTokens.length===0)throw Error("[notify] push channel requires recipient.pushTokens");let c=Le();D(await c.send(e.pushTokens,{title:t.subject,body:t.body,data:t.data},{driver:e.pushDriver}));break}case"broadcast":{let c=await p.send({channel:e.broadcastChannel,userId:e.userId,event:t.subject??"notification",data:{body:t.body,...t.data}});if(!c.delivered)throw Error(c.reason||"Broadcast notification was not delivered");break}default:throw Error(`Unsupported notification channel: ${o}`)}}))).map((o,c)=>{let l=r[c];if(!l)throw Error(`Missing notification channel for result ${c}`);if(o.status==="rejected"){let m=o.reason instanceof Error?o.reason.message:String(o.reason);U.warn(`[notify] ${l} channel failed: ${m}`)}return{channel:l,success:o.status==="fulfilled",error:o.status==="rejected"?o.reason instanceof Error?o.reason:Error(String(o.reason)):void 0}});return await Promise.all(s.map((o)=>P(k(e,t,o,i)))),s}function Ut(){return Fe()}export{p as BroadcastNotificationDriver,z as DEFAULT_SMS_OPT_OUT,h as DEFAULT_UNSUBSCRIBE_NOTICE,b as DatabaseNotificationDriver,W as SMS_SENDER_MAX_LENGTH,ne as accountNotificationTemplates,_e as bulkSetPreferences,ie as commerceNotificationTemplates,je as createNotificationTemplates,Ae as defineNotificationTemplates,q as emailParagraphs,Be as ensureSuccessfulEmailResult,St as ensureSuccessfulNotificationResults,C as filterChannelsByPreferences,E as getNotificationPreferences,w as interpolateNotificationText,H as listUnsubscribeHeaders,k as makeDeliveryRecord,Ut as notification,B as notificationChannel,Y as notificationEmailHtml,_ as notificationEmailMessage,V as notificationEmailText,v as notificationSmsBody,oe as notificationTemplateVariables,Ce as notificationUnsubscribeLinks,ee as notificationUnsubscribeToken,Dt as notify,te as parseNotificationUnsubscribeToken,P as recordNotificationDelivery,ce as renderNotificationTemplate,K as resolveDeliveryRecipient,se as resolveNotificationLocale,S as setNotificationPreference,G as smsSenderPrefix,re as subscriptionNotificationTemplates,De as unsubscribeTokenFromRequest,Ue as unsubscribeWithToken,Ct as useBroadcast,le as useChat,Me as useDatabase,fe as useEmail,Fe as useNotification,Le as usePush,de as useSMS,R as wherePreferenceCategory};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/notifications",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.142",
5
+ "version": "0.75.143",
6
6
  "description": "The Stacks notifications integration.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -64,18 +64,18 @@
64
64
  "prepublishOnly": "bun run build"
65
65
  },
66
66
  "dependencies": {
67
- "@stacksjs/chat": "0.75.142",
68
- "@stacksjs/cli": "0.75.142",
69
- "@stacksjs/config": "0.75.142",
70
- "@stacksjs/database": "0.75.142",
71
- "@stacksjs/email": "0.75.142",
72
- "@stacksjs/push": "0.75.142",
73
- "@stacksjs/realtime": "0.75.142",
74
- "@stacksjs/sms": "0.75.142"
67
+ "@stacksjs/chat": "0.75.143",
68
+ "@stacksjs/cli": "0.75.143",
69
+ "@stacksjs/config": "0.75.143",
70
+ "@stacksjs/database": "0.75.143",
71
+ "@stacksjs/email": "0.75.143",
72
+ "@stacksjs/push": "0.75.143",
73
+ "@stacksjs/realtime": "0.75.143",
74
+ "@stacksjs/sms": "0.75.143"
75
75
  },
76
76
  "devDependencies": {
77
- "@stacksjs/error-handling": "0.75.142",
78
- "@stacksjs/types": "0.75.142",
77
+ "@stacksjs/error-handling": "0.75.143",
78
+ "@stacksjs/types": "0.75.143",
79
79
  "better-dx": "^0.2.24"
80
80
  }
81
81
  }