@stacksjs/notifications 0.75.165 → 0.75.166
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.d.ts +2 -0
- package/dist/index.js +5 -5
- package/dist/sequences.d.ts +60 -0
- package/dist/sequences.js +2 -0
- package/package.json +12 -11
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type { CreateNotificationOptions, DatabaseNotification } from './drivers/
|
|
|
11
11
|
export type { NotificationPreferenceRow, PreferenceChannel } from './preferences';
|
|
12
12
|
export type { EmailBodyPart, NotificationAction, NotificationEmailPayload, NotificationFrameLocale, NotificationUnsubscribe } from './email-body';
|
|
13
13
|
export type { NotificationFrameStrings } from './frame-strings';
|
|
14
|
+
export type { AdvanceSequenceOptions, QuietHours, SequenceAdvance, SequenceStepLike } from './sequences';
|
|
14
15
|
export type { ParsedUnsubscribeToken, UnsubscribeChannel, UnsubscribeLinkOptions } from './unsubscribe';
|
|
15
16
|
export type { AccountNotificationPaths, AccountNotificationTemplateName, CommerceNotificationPaths, CommerceNotificationTemplateName, NotificationTemplateAction, NotificationTemplateCatalog, NotificationTemplateCatalogOptions, NotificationTemplateCopy, NotificationTemplateDefinition, NotificationTemplateVariables, RenderedNotificationTemplate, RenderNotificationTemplateOptions, SubscriptionNotificationPaths, SubscriptionNotificationTemplateName } from './templates';
|
|
16
17
|
export declare function ensureSuccessfulEmailResult(result: EmailResult): void;
|
|
@@ -142,5 +143,6 @@ export {
|
|
|
142
143
|
} from './preferences';
|
|
143
144
|
export { DEFAULT_SMS_OPT_OUT, DEFAULT_UNSUBSCRIBE_NOTICE, emailParagraphs, listUnsubscribeHeaders, notificationEmailHtml, notificationEmailMessage, notificationEmailText, notificationSmsBody, SMS_SENDER_MAX_LENGTH, smsSenderPrefix } from './email-body';
|
|
144
145
|
export { NOTIFICATION_FRAME_STRINGS, notificationFrameStrings, notificationLanguageTag } from './frame-strings';
|
|
146
|
+
export { advanceSequence, isQuietTime, nextSendableTime, sequenceTimeline } from './sequences';
|
|
145
147
|
export { notificationUnsubscribeLinks, notificationUnsubscribeToken, parseNotificationUnsubscribeToken, unsubscribeTokenFromRequest, unsubscribeWithToken } from './unsubscribe';
|
|
146
148
|
export { accountNotificationTemplates, commerceNotificationTemplates, createNotificationTemplates, defineNotificationTemplates, interpolateNotificationText, notificationTemplateVariables, renderNotificationTemplate, resolveNotificationLocale, subscriptionNotificationTemplates } from './templates';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{log as
|
|
2
|
+
import{log as L}from"@stacksjs/cli";import{notification as We}from"@stacksjs/config";import{mail as be}from"@stacksjs/email";import{log as F}from"@stacksjs/cli";var h={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,o=i?.getServer;if(!r||!o)return{delivered:!1,channel:n,event:t,reason:"@stacksjs/realtime is not available"};if(!o())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}),F.info(`[notifications] broadcast to '${n}' (${t})`),{delivered:!0,channel:n,event:t}}catch(i){let r=i instanceof Error?i.message:String(i);return F.warn(`[notifications] broadcast failed: ${r}`),{delivered:!1,channel:n,event:t,reason:r}}}};import*as B from"@stacksjs/chat";import{log as q}from"@stacksjs/cli";import{createHash as Ne}from"crypto";import{db as u,enqueueAfterCommit as we,lockRow as Te,sqlDateTime as S}from"@stacksjs/database/runtime";import{emit as xe,getServer as ke}from"@stacksjs/realtime";function z(e){return`private-user.${e}`}function v(e,t,n){let i=()=>{try{if(ke())xe(z(e),t,n)}catch{q.warn("Notification inbox broadcast failed; the persisted inbox remains available")}};if(!we(i))i()}async function M(e,t){let n=S(),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(),o=Array.isArray(r)?r[0]:r,s=Number(o?.id??o?.insertId??o?.lastInsertRowid??o?.lastInsertId??0);if(!Number.isSafeInteger(s)||s<=0)throw Error("Database notification did not return a generated id");if(q.info(`Database notification sent to user ${e.userId}: ${e.type}`),e.broadcast!==!1)v(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 y={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=Ne("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 Te(i,"users",{id:e.userId}))throw Error("Notification recipient not found");let o=await u.selectFrom("notifications").selectAll().where("user_id","=",e.userId).where("uuid","=",n).executeTakeFirst();if(o)return{...o,id:Number(o.id),user_id:Number(o.user_id)};return M(e,n)})}return M(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:S()}).where("id","=",e).whereNull("read_at");if(t!==void 0)n=n.where("user_id","=",t);if(await n.execute(),t)v(t,"notifications.read",{id:e})},async markAllAsRead(e){await u.updateTable("notifications").set({read_at:S()}).where("user_id","=",e).whereNull("read_at").execute(),v(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 H from"@stacksjs/push";import*as N from"@stacksjs/sms";import{log as Se}from"@stacksjs/cli";import{config as K}from"@stacksjs/config";import{db as ve,sqlDateTime as Y}from"@stacksjs/database/runtime";function Q(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 _(e,t,n,i,r=Y()){return{userId:e.userId,channel:n.channel,recipient:Q(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 _e(e){let t=K.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(!_e(e.channel))return;let t=Y();try{await ve.insertInto("notification_deliveries").values({user_id:e.userId??null,channel:e.channel,recipient:e.recipient,subject:e.subject??null,body:K.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);Se.warn(`[notifications] could not record ${e.channel} delivery: ${i}`)}}import{templateByName as Ae}from"@stacksjs/email";function V(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.
|
|
3
3
|
|
|
4
4
|
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.
|
|
5
5
|
|
|
@@ -37,13 +37,13 @@ Bestellung {{order_number}}.`}},action:{path:n,label:{en:"View your sales",de:"V
|
|
|
37
37
|
|
|
38
38
|
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}}
|
|
39
39
|
|
|
40
|
-
Dein Guthaben betr\xE4gt jetzt {{balance}}. Du kannst es bei deinem n\xE4chsten Einkauf verwenden.`}}}}}function
|
|
40
|
+
Dein Guthaben betr\xE4gt jetzt {{balance}}. Du kannst es bei deinem n\xE4chsten Einkauf verwenden.`}}}}}function W(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 Z=/\{\{\s*([a-z0-9_]+)\s*\}\}/gi;function Pe(e){return e}function w(e,t={}){return e.replace(Z,(n,i)=>{let r=t[i];return r==null?"":String(r)})}function J(e){return[...new Set([...e.matchAll(Z)].map((t)=>t[1]))]}function T(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 o=e[0];if(!o)throw Error("A notification template needs copy in at least one language");return o}function Ee(e,t){if(/^https?:\/\//i.test(t))return t;let n=String(e||"").replace(/\/+$/,"");if(!t)return n;return`${n}${t.startsWith("/")?"":"/"}${t}`}function X(e,t={},n={}){let i=n.fallbackLocale||"en",r=T(Object.keys(e.copy),n.locale,i),o=e.copy[r],s={subject:w(o.subject,t),body:w(o.body,t)},a=e.action;if(a){let c=J(a.path).some((g)=>t[g]==null||t[g]===""),l=a.label[r]??a.label[i]??Object.values(a.label)[0];if(!c&&l)s.action={label:l,url:Ee(n.baseUrl,w(a.path,t))}}return s}function Re(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,o={},s){let a=e[r];if(!a)throw Error(`Unknown notification template "${String(r)}"`);return X(a,o,{locale:s,fallbackLocale:t.fallbackLocale,baseUrl:i()})}}}var f=Object.freeze({en:Object.freeze({automatedNotice:"This is an automated message from {app}. Please do not reply to this email.",copyright:"{year} {app}. All rights reserved.",unsubscribeNotice:"You are receiving this because these emails are turned on in your settings.",unsubscribe:"Unsubscribe",preferences:"Email preferences",action:"Open",smsOptOut:"Reply STOP to opt out."}),de:Object.freeze({automatedNotice:"Diese Nachricht wurde automatisch von {app} verschickt. Bitte antworte nicht auf diese E-Mail.",copyright:"{year} {app}. Alle Rechte vorbehalten.",unsubscribeNotice:"Du erh\xE4ltst diese E-Mail, weil diese E-Mails in deinen Einstellungen aktiviert sind.",unsubscribe:"Abmelden",preferences:"E-Mail-Einstellungen",action:"\xD6ffnen",smsOptOut:"Antworte STOP zum Abbestellen."})});function b(e,t){let n=T(Object.keys(f),e,"en"),i={...f[n]};for(let[r,o]of Object.entries(t??{}))if(r in i&&typeof o==="string"&&o.trim())i[r]=o;return i}var De=/^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/i;function E(e){let t=String(e??"").trim().replace(/_/g,"-");return De.test(t)?t:"en"}var Ce=f.en.unsubscribeNotice,Oe=f.en.smsOptOut,Ue=/https?:\/\/[^\s<>"']+[^\s<>"'.,;:!?)\]]/g;function ee(e){return String(e??"").replace(/\r\n?/g,`
|
|
41
41
|
`).split(/\n\s*\n/).map((t)=>t.trim()).filter(Boolean).map((t)=>{let n=[];return t.split(`
|
|
42
|
-
`).forEach((i,r)=>{if(r>0)n.push({br:!0});let
|
|
42
|
+
`).forEach((i,r)=>{if(r>0)n.push({br:!0});let o=0;for(let s of i.matchAll(Ue)){if(s.index>o)n.push({text:i.slice(o,s.index)});n.push({url:s[0]}),o=s.index+s[0].length}if(o<i.length)n.push({text:i.slice(o)})}),n})}function d(e){return String(e??"").replace(/[&<>"']/g,(t)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t])}async function te(e){let t=e.unsubscribe?.url?e.unsubscribe:void 0,n=b(e.locale,e.strings);try{let{html:o}=await Ae("notification",{subject:e.subject??"",variables:{subject:e.subject??"",paragraphs:ee(e.body),preheader:String(e.body??"").replace(/\s+/g," ").trim().slice(0,140),lang:E(e.locale),automatedNotice:n.automatedNotice,copyrightNotice:n.copyright,unsubscribeLabel:n.unsubscribe,preferencesLabel:n.preferences,...e.action?.url?{actionUrl:e.action.url,actionLabel:e.action.label||n.action}:{},...t?{unsubscribeUrl:t.url,unsubscribeNotice:t.notice||n.unsubscribeNotice,...t.preferencesUrl?{preferencesUrl:t.preferencesUrl}:{}}:{}}});if(o)return o}catch{}let i=e.action?.url?`<p><a href="${d(e.action.url)}">${d(e.action.label||e.action.url)}</a></p>`:"",r=t?`<p>${d(t.notice||n.unsubscribeNotice)} <a href="${d(t.url)}">${d(n.unsubscribe)}</a></p>`:"";return`<p>${d(e.body)}</p>${i}${r}`}function ne(e){let t=b(e.locale,e.strings),n=e.action?.url?`${e.body}
|
|
43
43
|
|
|
44
44
|
${e.action.label||t.action}: ${e.action.url}`:e.body;if(e.unsubscribe?.url)n+=`
|
|
45
45
|
|
|
46
46
|
${e.unsubscribe.notice||t.unsubscribeNotice}
|
|
47
|
-
${t.unsubscribe}: ${e.unsubscribe.url}`;return n}function
|
|
47
|
+
${t.unsubscribe}: ${e.unsubscribe.url}`;return n}function ie(e){if(!e?.url)return{};return{"List-Unsubscribe":`<${e.oneClickUrl||e.url}>`,...e.oneClickUrl?{"List-Unsubscribe-Post":"List-Unsubscribe=One-Click"}:{}}}async function R(e,t){let n={...t.headers,...ie(t.unsubscribe)};return{to:e,subject:t.subject??"",text:ne(t),html:await te(t),...Object.keys(n).length?{headers:n}:{}}}var re=30;function oe(e){let t=String(e??"").replace(/\s+/g," ").trim().slice(0,re).trim();return t?`${t}: `:""}function D(e,t){let n=String(e.body??""),i=oe(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"?b(e.locale,e.strings).smsOptOut:"";if(!r||n.toLowerCase().includes(r.toLowerCase()))return n;return`${n.trimEnd()}
|
|
48
48
|
|
|
49
|
-
${r}`}import{log as Ce}from"@stacksjs/cli";import{db as m,lockRow as re,sqlDateTime as De}from"@stacksjs/database/runtime";var T="notification_preferences";function D(e,t){return t===null?e.whereNull("category"):e.where("category","=",t)}async function C(e,t){let n=new Map;try{let r=await D(m.selectFrom(T).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){Ce.debug?.(`[notifications] preferences lookup failed (table missing?): ${i.message}`)}return n}async function A(e,t,n,i){await m.transaction(async(r)=>{await re(r,"users",{id:e}),await ae(e,t,n,i)})}async function ae(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=De(),a=i??null,s=m.selectFrom(T).select(["id"]).where("user_id","=",e).where("channel","=",t),o=await D(s,a).executeTakeFirst();if(o?.id){await m.updateTable(T).set({enabled:n,updated_at:r}).where("id","=",o.id).execute();return}await m.insertInto(T).values({user_id:e,channel:t,enabled:n,category:a,created_at:r,updated_at:r}).execute()}async function Ae(e,t){await m.transaction(async(n)=>{await re(n,"users",{id:e});for(let i of t)await ae(e,i.channel,i.enabled,i.category)})}async function U(e,t,n){let i=await C(e,n);if(n!==void 0){let r=await C(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 x from"process";import{createHmac as Ue,timingSafeEqual as Oe}from"crypto";import{db as $e}from"@stacksjs/database/runtime";var oe=["email","sms"];function je(){let e=x.env.APP_KEY;if((!e||e.length<16)&&(x.env.APP_ENV==="production"||x.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 se(e){return Ue("sha256",je()).update(e).digest("hex")}function ce(e,t,n="marketing"){if(!Number.isSafeInteger(e)||e<=0)throw Error("An unsubscribe token needs a positive user id");if(!oe.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}:${se(i)}`).toString("base64url")}function ue(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||!oe.includes(r))return null;let o=se(`${i}:${r}:${a}`);if(s.length!==o.length||!Oe(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 Ie(e,t={}){let n=String(t.baseUrl||x.env.APP_URL||"http://localhost").replace(/\/$/,""),i=ce(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 Le(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 Fe(e){let t=ue(e);if(!t)return null;if(await $e.selectFrom("users").select(["id"]).where("id","=",t.userId).executeTakeFirst())await A(t.userId,t.channel,!1,t.category);return t}var Me=Be;function ze(e){if(!e.success)throw Error(e.message||`Email delivery failed through ${e.provider||"the configured provider"}.`)}function jt(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 O(e){if(!e.success)throw Error(e.error||e.message||"Notification provider rejected delivery")}function fe(e){let t=e||"slack";if(!["slack","discord","teams"].includes(t))throw Error(`Unsupported chat driver: ${t}`);return L[t]}function de(e){if(e)try{return le.use(e)}catch(t){j.warn(`[notifications] email driver '${e}' not registered - falling back to default mail singleton (${t.message})`)}return le}function me(e){if(!e)return y;if(e!=="twilio"&&e!=="vonage")throw Error(`Unsupported SMS driver: ${e}`);let t=e;return{send:async(n)=>(await y.init(),y.getDriver(t).send(n))}}function Ke(){return z}function qe(){return h}function It(){return g}function Ye(e,t){let n=e||Me?.default||"email",i=t;switch(n){case"email":return de(i);case"chat":return fe(i);case"sms":return me(i);case"database":return qe();default:throw Error(`Notification type "${n}" is not supported`)}}async function Lt(e,t,n=["email"],i={}){let r=[...new Set(n)];if(e.userId&&!i.ignorePreferences)try{r=await U(e.userId,r,i.category)}catch(o){j.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 de().send(await S(e.email,t));ze(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=me();O(await c.send({to:e.phone,body:R(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=fe();O(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 h.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=Ke();O(await c.send(e.pushTokens,{title:t.subject,body:t.body,data:t.data},{driver:e.pushDriver}));break}case"broadcast":{let c=await g.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 b=o.reason instanceof Error?o.reason.message:String(o.reason);j.warn(`[notify] ${l} channel failed: ${b}`)}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)=>v(_(e,t,o,i)))),s}function Ft(){return Ye()}export{g as BroadcastNotificationDriver,Se as DEFAULT_SMS_OPT_OUT,Ee as DEFAULT_UNSUBSCRIBE_NOTICE,h as DatabaseNotificationDriver,f as NOTIFICATION_FRAME_STRINGS,ne as SMS_SENDER_MAX_LENGTH,V as accountNotificationTemplates,Ae as bulkSetPreferences,G as commerceNotificationTemplates,Pe as createNotificationTemplates,xe as defineNotificationTemplates,Z as emailParagraphs,ze as ensureSuccessfulEmailResult,jt as ensureSuccessfulNotificationResults,U as filterChannelsByPreferences,C as getNotificationPreferences,N as interpolateNotificationText,te as listUnsubscribeHeaders,_ as makeDeliveryRecord,Ft as notification,M as notificationChannel,X as notificationEmailHtml,S as notificationEmailMessage,ee as notificationEmailText,p as notificationFrameStrings,E as notificationLanguageTag,R as notificationSmsBody,J as notificationTemplateVariables,Ie as notificationUnsubscribeLinks,ce as notificationUnsubscribeToken,Lt as notify,ue as parseNotificationUnsubscribeToken,v as recordNotificationDelivery,Q as renderNotificationTemplate,Y as resolveDeliveryRecipient,w as resolveNotificationLocale,A as setNotificationPreference,ie as smsSenderPrefix,H as subscriptionNotificationTemplates,Le as unsubscribeTokenFromRequest,Fe as unsubscribeWithToken,It as useBroadcast,fe as useChat,qe as useDatabase,de as useEmail,Ye as useNotification,Ke as usePush,me as useSMS,D as wherePreferenceCategory};
|
|
49
|
+
${r}`}import{log as $e}from"@stacksjs/cli";import{db as m,lockRow as ae,sqlDateTime as je}from"@stacksjs/database/runtime";var x="notification_preferences";function C(e,t){return t===null?e.whereNull("category"):e.where("category","=",t)}async function A(e,t){let n=new Map;try{let r=await C(m.selectFrom(x).select(["channel","enabled","category"]).where("user_id","=",e),t??null).execute();for(let o of r)n.set(o.channel,o.enabled===!0||o.enabled===1)}catch(i){$e.debug?.(`[notifications] preferences lookup failed (table missing?): ${i.message}`)}return n}async function O(e,t,n,i){await m.transaction(async(r)=>{await ae(r,"users",{id:e}),await se(e,t,n,i)})}async function se(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=je(),o=i??null,s=m.selectFrom(x).select(["id"]).where("user_id","=",e).where("channel","=",t),a=await C(s,o).executeTakeFirst();if(a?.id){await m.updateTable(x).set({enabled:n,updated_at:r}).where("id","=",a.id).execute();return}await m.insertInto(x).values({user_id:e,channel:t,enabled:n,category:o,created_at:r,updated_at:r}).execute()}async function Ie(e,t){await m.transaction(async(n)=>{await ae(n,"users",{id:e});for(let i of t)await se(e,i.channel,i.enabled,i.category)})}async function U(e,t,n){let i=await A(e,n);if(n!==void 0){let r=await A(e,void 0);for(let[o,s]of r)if(!s||!i.has(o))i.set(o,s)}return t.filter((r)=>{let o=i.get(r);return o===void 0?!0:o===!0})}import{localDateTime as ce,zonedTimeToUtc as Le}from"@stacksjs/datetime";var Fe=/^([01]\d|2[0-3]):([0-5]\d)$/;function p(e,t){let n=Fe.exec(String(e));if(!n)throw RangeError(`Quiet hours ${t} must be a time like 21:00`);return Number(n[1])*60+Number(n[2])}function Be(e,t){let n=new Date(`${e}T00:00:00Z`);return n.setUTCDate(n.getUTCDate()+t),n.toISOString().slice(0,10)}function j(e,t){if(!t)return!1;let n=p(t.start,"start"),i=p(t.end,"end");if(n===i)return!1;let r=ce(e,t.timeZone),o=p(r.time,"time");return n<i?o>=n&&o<i:o>=n||o<i}function ue(e,t){if(!t||!j(e,t))return e;let n=p(t.start,"start"),i=ce(e,t.timeZone),r=p(i.time,"time"),o=n>p(t.end,"end")&&r>=n?Be(i.date,1):i.date;return Le(`${o}T${t.end}`,i.timeZone)}function Me(e){let t=Number(e.minutes??0);if(!Number.isFinite(t)||t<0)throw RangeError("A wait step needs zero or more minutes");return t}function le(e,t,n,i={}){if(!Number.isSafeInteger(t)||t<0)throw RangeError("A sequence position must be a whole number of zero or more");let r=i.respectsQuietHours??((s)=>s.type!=="wait"),o=[];for(let s=t;s<e.length;s++){let a=e[s];if(a.type==="wait"){let c=Me(a);if(c>0)return{run:o,next:{index:s+1,at:new Date(n.getTime()+c*60000)}};continue}if(r(a)&&j(n,i.quietHours))return{run:o,next:{index:s,at:ue(n,i.quietHours)}};o.push({index:s,step:a})}return{run:o,next:null}}function qe(e,t,n={}){let i=[],r=0,o=t;for(let s=0;s<=e.length*2+1;s++){let a=le(e,r,o,n);for(let c of a.run)i.push({...c,at:o});if(!a.next)break;r=a.next.index,o=a.next.at}return i}import k from"process";import{createHmac as ze,timingSafeEqual as He}from"crypto";import{db as Ke}from"@stacksjs/database/runtime";var fe=["email","sms"];function Ye(){let e=k.env.APP_KEY;if((!e||e.length<16)&&(k.env.APP_ENV==="production"||k.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 de(e){return ze("sha256",Ye()).update(e).digest("hex")}function me(e,t,n="marketing"){if(!Number.isSafeInteger(e)||e<=0)throw Error("An unsubscribe token needs a positive user id");if(!fe.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}:${de(i)}`).toString("base64url")}function pe(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,o,s]=n;if(!i||!o||!s||!fe.includes(r))return null;let a=de(`${i}:${r}:${o}`);if(s.length!==a.length||!He(Buffer.from(s),Buffer.from(a)))return null;let c=Number(i);return Number.isSafeInteger(c)&&c>0&&String(c)===i?{userId:c,channel:r,category:o}:null}catch{return null}}function Qe(e,t={}){let n=String(t.baseUrl||k.env.APP_URL||"http://localhost").replace(/\/$/,""),i=me(e,t.channel??"email",t.category??"marketing"),r=(o)=>o.startsWith("/")?o:`/${o}`;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 Ve(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 Ge(e){let t=pe(e);if(!t)return null;if(await Ke.selectFrom("users").select(["id"]).where("id","=",t.userId).executeTakeFirst())await O(t.userId,t.channel,!1,t.category);return t}var Ze=We;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 I(e){if(!e.success)throw Error(e.error||e.message||"Notification provider rejected delivery")}function ge(e){let t=e||"slack";if(!["slack","discord","teams"].includes(t))throw Error(`Unsupported chat driver: ${t}`);return B[t]}function he(e){if(e)try{return be.use(e)}catch(t){L.warn(`[notifications] email driver '${e}' not registered - falling back to default mail singleton (${t.message})`)}return be}function ye(e){if(!e)return N;if(e!=="twilio"&&e!=="vonage")throw Error(`Unsupported SMS driver: ${e}`);let t=e;return{send:async(n)=>(await N.init(),N.getDriver(t).send(n))}}function Xe(){return H}function et(){return y}function Gt(){return h}function tt(e,t){let n=e||Ze?.default||"email",i=t;switch(n){case"email":return he(i);case"chat":return ge(i);case"sms":return ye(i);case"database":return et();default:throw Error(`Notification type "${n}" is not supported`)}}async function Wt(e,t,n=["email"],i={}){let r=[...new Set(n)];if(e.userId&&!i.ignorePreferences)try{r=await U(e.userId,r,i.category)}catch(a){L.warn(`[notify] preferences filter failed, sending all channels: ${a.message}`)}let s=(await Promise.allSettled(r.map(async(a)=>{switch(a){case"email":{if(!e.email)throw Error("[notify] email channel requires recipient.email");let l=await he().send(await R(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=ye();I(await c.send({to:e.phone,body:D(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=ge();I(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 y.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=Xe();I(await c.send(e.pushTokens,{title:t.subject,body:t.body,data:t.data},{driver:e.pushDriver}));break}case"broadcast":{let c=await h.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: ${a}`)}}))).map((a,c)=>{let l=r[c];if(!l)throw Error(`Missing notification channel for result ${c}`);if(a.status==="rejected"){let g=a.reason instanceof Error?a.reason.message:String(a.reason);L.warn(`[notify] ${l} channel failed: ${g}`)}return{channel:l,success:a.status==="fulfilled",error:a.status==="rejected"?a.reason instanceof Error?a.reason:Error(String(a.reason)):void 0}});return await Promise.all(s.map((a)=>P(_(e,t,a,i)))),s}function Zt(){return tt()}export{h as BroadcastNotificationDriver,Oe as DEFAULT_SMS_OPT_OUT,Ce as DEFAULT_UNSUBSCRIBE_NOTICE,y as DatabaseNotificationDriver,f as NOTIFICATION_FRAME_STRINGS,re as SMS_SENDER_MAX_LENGTH,V as accountNotificationTemplates,le as advanceSequence,Ie as bulkSetPreferences,G as commerceNotificationTemplates,Re as createNotificationTemplates,Pe as defineNotificationTemplates,ee as emailParagraphs,Je as ensureSuccessfulEmailResult,Vt as ensureSuccessfulNotificationResults,U as filterChannelsByPreferences,A as getNotificationPreferences,w as interpolateNotificationText,j as isQuietTime,ie as listUnsubscribeHeaders,_ as makeDeliveryRecord,ue as nextSendableTime,Zt as notification,z as notificationChannel,te as notificationEmailHtml,R as notificationEmailMessage,ne as notificationEmailText,b as notificationFrameStrings,E as notificationLanguageTag,D as notificationSmsBody,J as notificationTemplateVariables,Qe as notificationUnsubscribeLinks,me as notificationUnsubscribeToken,Wt as notify,pe as parseNotificationUnsubscribeToken,P as recordNotificationDelivery,X as renderNotificationTemplate,Q as resolveDeliveryRecipient,T as resolveNotificationLocale,qe as sequenceTimeline,O as setNotificationPreference,oe as smsSenderPrefix,W as subscriptionNotificationTemplates,Ve as unsubscribeTokenFromRequest,Ge as unsubscribeWithToken,Gt as useBroadcast,ge as useChat,et as useDatabase,he as useEmail,tt as useNotification,Xe as usePush,ye as useSMS,C as wherePreferenceCategory};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Whether `at` falls inside the quiet window. A window that starts and ends at the same time is no window. */
|
|
2
|
+
export declare function isQuietTime(at: Date, quiet: QuietHours | null | undefined): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* The first instant at or after `at` that is outside the quiet window: `at`
|
|
5
|
+
* itself when it is not quiet, else the window's end in the zone's local
|
|
6
|
+
* time (the next morning, for a window that started the evening before).
|
|
7
|
+
*/
|
|
8
|
+
export declare function nextSendableTime(at: Date, quiet: QuietHours | null | undefined): Date;
|
|
9
|
+
/**
|
|
10
|
+
* What to do now for someone whose next step is `index`, at `now`.
|
|
11
|
+
*
|
|
12
|
+
* Runs forward from `index`: every step that is due is returned in `run`, up
|
|
13
|
+
* to the first wait (which sets when the sequence continues) or the first
|
|
14
|
+
* step that would land in quiet hours (which is held until they end). A
|
|
15
|
+
* wait of zero minutes is passed straight over. When the steps run out,
|
|
16
|
+
* `next` is null and the sequence is finished.
|
|
17
|
+
*/
|
|
18
|
+
export declare function advanceSequence<S extends SequenceStepLike>(steps: readonly S[], index: number, now: Date, options?: AdvanceSequenceOptions<S>): SequenceAdvance<S>;
|
|
19
|
+
/**
|
|
20
|
+
* When each step would run for someone who starts the sequence at `start`,
|
|
21
|
+
* assuming every tick comes on time: what a preview shows ("day 0, 09:00:
|
|
22
|
+
* welcome email; day 2: text"). Waits are not listed.
|
|
23
|
+
*/
|
|
24
|
+
export declare function sequenceTimeline<S extends SequenceStepLike>(steps: readonly S[], start: Date, options?: AdvanceSequenceOptions<S>): Array<{ index: number, step: S, at: Date }>;
|
|
25
|
+
/**
|
|
26
|
+
* Sequences: a list of steps run one after another for one person, with
|
|
27
|
+
* waits between them. "Email now, wait two days, text, wait a week, give
|
|
28
|
+
* the front desk a call task" is a sequence; so is a welcome series or a
|
|
29
|
+
* win-back journey.
|
|
30
|
+
*
|
|
31
|
+
* This module is the timing engine only, and it is pure. It never sends
|
|
32
|
+
* or stores anything. The application keeps each person's place (the next
|
|
33
|
+
* step's index and when it is due), and on every tick asks
|
|
34
|
+
* {@link advanceSequence} what to do now and where to pick up afterwards.
|
|
35
|
+
* Sending, and remembering what was sent, stay with the caller, so the
|
|
36
|
+
* same engine drives email, SMS, push, or tasks for staff.
|
|
37
|
+
*
|
|
38
|
+
* Quiet hours are read in the recipient's (or the business's) time zone,
|
|
39
|
+
* so "not between 21:00 and 08:00" means the same thing to a gym in
|
|
40
|
+
* Berlin and one in Denver, across daylight saving changes.
|
|
41
|
+
*/
|
|
42
|
+
/** A window, in local wall-clock time, in which messages are held back. */
|
|
43
|
+
export declare interface QuietHours {
|
|
44
|
+
start: string
|
|
45
|
+
end: string
|
|
46
|
+
timeZone: string
|
|
47
|
+
}
|
|
48
|
+
/** The smallest shape a step needs: its type, and the length of a wait. */
|
|
49
|
+
export declare interface SequenceStepLike {
|
|
50
|
+
type: string
|
|
51
|
+
minutes?: number
|
|
52
|
+
}
|
|
53
|
+
export declare interface SequenceAdvance<S> {
|
|
54
|
+
run: Array<{ index: number, step: S }>
|
|
55
|
+
next: { index: number, at: Date } | null
|
|
56
|
+
}
|
|
57
|
+
export declare interface AdvanceSequenceOptions<S> {
|
|
58
|
+
quietHours?: QuietHours | null
|
|
59
|
+
respectsQuietHours?: (step: S) => boolean
|
|
60
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{localDateTime as m,zonedTimeToUtc as S}from"@stacksjs/datetime";var l=/^([01]\d|2[0-3]):([0-5]\d)$/;function a(n,e){let t=l.exec(String(n));if(!t)throw RangeError(`Quiet hours ${e} must be a time like 21:00`);return Number(t[1])*60+Number(t[2])}function f(n,e){let t=new Date(`${n}T00:00:00Z`);return t.setUTCDate(t.getUTCDate()+e),t.toISOString().slice(0,10)}function d(n,e){if(!e)return!1;let t=a(e.start,"start"),r=a(e.end,"end");if(t===r)return!1;let u=m(n,e.timeZone),i=a(u.time,"time");return t<r?i>=t&&i<r:i>=t||i<r}function p(n,e){if(!e||!d(n,e))return n;let t=a(e.start,"start"),r=m(n,e.timeZone),u=a(r.time,"time"),i=t>a(e.end,"end")&&u>=t?f(r.date,1):r.date;return S(`${i}T${e.end}`,r.timeZone)}function x(n){let e=Number(n.minutes??0);if(!Number.isFinite(e)||e<0)throw RangeError("A wait step needs zero or more minutes");return e}function b(n,e,t,r={}){if(!Number.isSafeInteger(e)||e<0)throw RangeError("A sequence position must be a whole number of zero or more");let u=r.respectsQuietHours??((o)=>o.type!=="wait"),i=[];for(let o=e;o<n.length;o++){let s=n[o];if(s.type==="wait"){let c=x(s);if(c>0)return{run:i,next:{index:o+1,at:new Date(t.getTime()+c*60000)}};continue}if(u(s)&&d(t,r.quietHours))return{run:i,next:{index:o,at:p(t,r.quietHours)}};i.push({index:o,step:s})}return{run:i,next:null}}function D(n,e,t={}){let r=[],u=0,i=e;for(let o=0;o<=n.length*2+1;o++){let s=b(n,u,i,t);for(let c of s.run)r.push({...c,at:i});if(!s.next)break;u=s.next.index,i=s.next.at}return r}export{b as advanceSequence,d as isQuietTime,p as nextSendableTime,D as sequenceTimeline};
|
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.
|
|
5
|
+
"version": "0.75.166",
|
|
6
6
|
"description": "The Stacks notifications integration.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -64,18 +64,19 @@
|
|
|
64
64
|
"prepublishOnly": "bun run build"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@stacksjs/chat": "0.75.
|
|
68
|
-
"@stacksjs/cli": "0.75.
|
|
69
|
-
"@stacksjs/config": "0.75.
|
|
70
|
-
"@stacksjs/database": "0.75.
|
|
71
|
-
"@stacksjs/
|
|
72
|
-
"@stacksjs/
|
|
73
|
-
"@stacksjs/
|
|
74
|
-
"@stacksjs/
|
|
67
|
+
"@stacksjs/chat": "0.75.166",
|
|
68
|
+
"@stacksjs/cli": "0.75.166",
|
|
69
|
+
"@stacksjs/config": "0.75.166",
|
|
70
|
+
"@stacksjs/database": "0.75.166",
|
|
71
|
+
"@stacksjs/datetime": "0.75.166",
|
|
72
|
+
"@stacksjs/email": "0.75.166",
|
|
73
|
+
"@stacksjs/push": "0.75.166",
|
|
74
|
+
"@stacksjs/realtime": "0.75.166",
|
|
75
|
+
"@stacksjs/sms": "0.75.166"
|
|
75
76
|
},
|
|
76
77
|
"devDependencies": {
|
|
77
|
-
"@stacksjs/error-handling": "0.75.
|
|
78
|
-
"@stacksjs/types": "0.75.
|
|
78
|
+
"@stacksjs/error-handling": "0.75.166",
|
|
79
|
+
"@stacksjs/types": "0.75.166",
|
|
79
80
|
"better-dx": "^0.2.24"
|
|
80
81
|
}
|
|
81
82
|
}
|