@stacksjs/notifications 0.75.139 → 0.75.141

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/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  // @bun
2
- import{log as S}from"@stacksjs/cli";import{notification as Ae}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 Ne}from"@stacksjs/email";var g="You are receiving this because these emails are turned on in your settings.",z="Reply STOP to opt out.",we=/https?:\/\/[^\s<>"']+[^\s<>"'.,;:!?)\]]/g;function q(e){return String(e??"").replace(/\r\n?/g,`
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,`
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(we)){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 Ne("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(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}
5
5
 
6
6
  ${e.action.label||"Open"}: ${e.action.url}`:e.body;if(e.unsubscribe?.url)t+=`
7
7
 
8
8
  ${e.unsubscribe.notice||g}
9
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()}
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 N from"process";import{createHmac as Pe,timingSafeEqual as ve}from"crypto";var Q=["email","sms"];function _e(){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 Z(e){return Pe("sha256",_e()).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 Ee(e,t={}){let n=String(t.baseUrl||N.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 Re(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 Ce(e){let t=X(e);if(!t)return null;return await C(t.userId,t.channel,!1,t.category),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
+ ${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.
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
 
@@ -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 De(e){return e}function w(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 Ue(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:w(a.subject,t),body:w(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:Ue(n.baseUrl,w(o.path,t))}}return s}function Se(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 $e=Ae;function je(e){if(!e.success)throw Error(e.message||`Email delivery failed through ${e.provider||"the configured provider"}.`)}function vt(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 Oe(){return L}function Ie(){return b}function _t(){return m}function Be(e,t){let n=e||$e?.default||"email",i=t;switch(n){case"email":return ue(i);case"chat":return ce(i);case"sms":return le(i);case"database":return Ie();default:throw Error(`Notification type "${n}" is not supported`)}}async function Et(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));je(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=Oe();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 Rt(){return Be()}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,Se as createNotificationTemplates,De as defineNotificationTemplates,q as emailParagraphs,je as ensureSuccessfulEmailResult,vt as ensureSuccessfulNotificationResults,D as filterChannelsByPreferences,E as getNotificationPreferences,w as interpolateNotificationText,W as listUnsubscribeHeaders,k as makeDeliveryRecord,Rt as notification,B as notificationChannel,Y as notificationEmailHtml,v as notificationEmailMessage,V as notificationEmailText,_ as notificationSmsBody,re as notificationTemplateVariables,Ee as notificationUnsubscribeLinks,G as notificationUnsubscribeToken,Et as notify,X as parseNotificationUnsubscribeToken,P as recordNotificationDelivery,oe as renderNotificationTemplate,K as resolveDeliveryRecipient,ae as resolveNotificationLocale,C as setNotificationPreference,ne as subscriptionNotificationTemplates,Re as unsubscribeTokenFromRequest,Ce as unsubscribeWithToken,_t as useBroadcast,ce as useChat,Ie as useDatabase,ue as useEmail,Be as useNotification,Oe 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 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};
@@ -22,6 +22,10 @@ export declare function unsubscribeTokenFromRequest(request: { url: string, json
22
22
  /**
23
23
  * Verify a token and turn its preference off. Returns what was turned off,
24
24
  * or null for an invalid token (answer 422).
25
+ *
26
+ * A valid token for an account that no longer exists succeeds without
27
+ * writing anything: nothing is sent to a deleted account, so the link has
28
+ * done its job, and a preference row for it would break the foreign key.
25
29
  */
26
30
  export declare function unsubscribeWithToken(token: string): Promise<ParsedUnsubscribeToken | null>;
27
31
  export declare interface ParsedUnsubscribeToken {
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.139",
5
+ "version": "0.75.141",
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.139",
68
- "@stacksjs/cli": "0.75.139",
69
- "@stacksjs/config": "0.75.139",
70
- "@stacksjs/database": "0.75.139",
71
- "@stacksjs/email": "0.75.139",
72
- "@stacksjs/push": "0.75.139",
73
- "@stacksjs/realtime": "0.75.139",
74
- "@stacksjs/sms": "0.75.139"
67
+ "@stacksjs/chat": "0.75.141",
68
+ "@stacksjs/cli": "0.75.141",
69
+ "@stacksjs/config": "0.75.141",
70
+ "@stacksjs/database": "0.75.141",
71
+ "@stacksjs/email": "0.75.141",
72
+ "@stacksjs/push": "0.75.141",
73
+ "@stacksjs/realtime": "0.75.141",
74
+ "@stacksjs/sms": "0.75.141"
75
75
  },
76
76
  "devDependencies": {
77
- "@stacksjs/error-handling": "0.75.139",
78
- "@stacksjs/types": "0.75.139",
77
+ "@stacksjs/error-handling": "0.75.141",
78
+ "@stacksjs/types": "0.75.141",
79
79
  "better-dx": "^0.2.24"
80
80
  }
81
81
  }