From 175ecb24f9261f7e2f2ea2940027b255e26d7cc5 Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Date: Tue, 16 Dec 2025 11:28:34 +0100 Subject: [PATCH] whatsapp notification (#1651) * feat: whatsapp * ci: apply automated fixes * whatsapp * ci: apply automated fixes * small change * typo * use test template * chore: minor updates * add test button * fix changelog * why? --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Maximilian Kaske --- apps/dashboard/package.json | 1 + .../app/(dashboard)/notifications/client.tsx | 4 +- .../forms/notifications/form-whatsapp.tsx | 231 ++++++++++++++++++ .../components/forms/notifications/form.tsx | 1 + .../src/data/notifications.client.ts | 10 +- .../content/docs/reference/notification.mdx | 11 +- apps/web/public/assets/changelog/whatsapp.png | Bin 0 -> 22027 bytes .../changelog/whatsapp-notifications.mdx | 10 + .../src/content/pages/unrelated/pricing.mdx | 3 +- apps/workflows/.dockerignore | 2 +- apps/workflows/Dockerfile | 5 +- apps/workflows/dofigen.lock | 67 ++--- apps/workflows/dofigen.yml | 3 +- apps/workflows/package.json | 1 + apps/workflows/src/checker/utils.ts | 10 + packages/api/package.json | 1 + packages/api/src/router/notification.ts | 14 ++ .../db/src/schema/notifications/constants.ts | 5 +- .../db/src/schema/notifications/validation.ts | 21 +- packages/db/src/schema/plan/config.ts | 3 + packages/db/src/schema/plan/schema.ts | 1 + packages/icons/src/index.tsx | 1 + packages/icons/src/whatsapp.tsx | 15 ++ .../twillio-whatsapp/.env.example | 2 + .../twillio-whatsapp/package.json | 21 ++ .../notifications/twillio-whatsapp/src/env.ts | 30 +++ .../twillio-whatsapp/src/index.test.ts | 132 ++++++++++ .../twillio-whatsapp/src/index.ts | 175 +++++++++++++ .../twillio-whatsapp/tsconfig.json | 7 + pnpm-lock.yaml | 184 ++++++++------ 30 files changed, 849 insertions(+), 122 deletions(-) create mode 100644 apps/dashboard/src/components/forms/notifications/form-whatsapp.tsx create mode 100644 apps/web/public/assets/changelog/whatsapp.png create mode 100644 apps/web/src/content/pages/changelog/whatsapp-notifications.mdx create mode 100644 packages/icons/src/whatsapp.tsx create mode 100644 packages/notifications/twillio-whatsapp/.env.example create mode 100644 packages/notifications/twillio-whatsapp/package.json create mode 100644 packages/notifications/twillio-whatsapp/src/env.ts create mode 100644 packages/notifications/twillio-whatsapp/src/index.test.ts create mode 100644 packages/notifications/twillio-whatsapp/src/index.ts create mode 100644 packages/notifications/twillio-whatsapp/tsconfig.json diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 4af54dee7..c7ac173d5 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -37,6 +37,7 @@ "@openstatus/notification-pagerduty": "workspace:*", "@openstatus/notification-slack": "workspace:*", "@openstatus/notification-telegram": "workspace:*", + "@openstatus/notification-twillio-whatsapp": "workspace:*", "@openstatus/notification-webhook": "workspace:*", "@openstatus/react": "workspace:*", "@openstatus/regions": "workspace:*", diff --git a/apps/dashboard/src/app/(dashboard)/notifications/client.tsx b/apps/dashboard/src/app/(dashboard)/notifications/client.tsx index d0e5908f2..58ac0bbfe 100644 --- a/apps/dashboard/src/app/(dashboard)/notifications/client.tsx +++ b/apps/dashboard/src/app/(dashboard)/notifications/client.tsx @@ -93,7 +93,9 @@ export function Client() { if (key in workspace.limits) { enabled = - workspace.limits[key as "opsgenie" | "sms" | "opsgenie"]; + workspace.limits[ + key as "opsgenie" | "sms" | "opsgenie" | "whatsapp" + ]; } if (limitReached) { diff --git a/apps/dashboard/src/components/forms/notifications/form-whatsapp.tsx b/apps/dashboard/src/components/forms/notifications/form-whatsapp.tsx new file mode 100644 index 000000000..c8fa753ea --- /dev/null +++ b/apps/dashboard/src/components/forms/notifications/form-whatsapp.tsx @@ -0,0 +1,231 @@ +"use client"; + +import { Checkbox } from "@/components/ui/checkbox"; +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; + +import { + FormCardContent, + FormCardSeparator, +} from "@/components/forms/form-card"; +import { useFormSheetDirty } from "@/components/forms/form-sheet"; +import { Button } from "@/components/ui/button"; +import { Form } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +import { useTRPC } from "@/lib/trpc/client"; +import { cn } from "@/lib/utils"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { isTRPCClientError } from "@trpc/client"; +import React, { useTransition } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +const schema = z.object({ + name: z.string(), + provider: z.literal("whatsapp"), + data: z.string(), + monitors: z.array(z.number()), +}); + +type FormValues = z.infer; + +export function FormWhatsApp({ + defaultValues, + onSubmit, + className, + monitors, + ...props +}: Omit, "onSubmit"> & { + defaultValues?: FormValues; + onSubmit: (values: FormValues) => Promise; + monitors: { id: number; name: string }[]; +}) { + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: defaultValues ?? { + name: "", + provider: "whatsapp", + data: "", + monitors: [], + }, + }); + const [isPending, startTransition] = useTransition(); + const { setIsDirty } = useFormSheetDirty(); + const trpc = useTRPC(); + + const sendTestMutation = useMutation( + trpc.notification.sendTest.mutationOptions(), + ); + + const formIsDirty = form.formState.isDirty; + React.useEffect(() => { + setIsDirty(formIsDirty); + }, [formIsDirty, setIsDirty]); + + function submitAction(values: FormValues) { + if (isPending) return; + + startTransition(async () => { + try { + const promise = onSubmit(values); + toast.promise(promise, { + loading: "Saving...", + success: "Saved", + error: (error) => { + if (isTRPCClientError(error)) { + return error.message; + } + return "Failed to save"; + }, + }); + await promise; + } catch (error) { + console.error(error); + } + }); + } + + function testAction() { + if (isPending) return; + + startTransition(async () => { + try { + const provider = form.getValues("provider"); + const data = form.getValues("data"); + const promise = sendTestMutation.mutateAsync({ + provider, + data: { + whatsapp: data, + }, + }); + toast.promise(promise, { + loading: "Sending test...", + success: "Test sent", + error: (error) => { + if (error instanceof Error) { + return error.message; + } + return "Failed to send test"; + }, + }); + await promise; + } catch (error) { + console.error(error); + } + }); + } + + return ( +
+ + + ( + + Name + + + + + + Enter a descriptive name for your notifier. + + + )} + /> + ( + + WhatsApp + + + + + + Enter the phone number to send notifications to. + + + )} + /> +
+ +
+
+ + + ( + + Monitors + + Select the monitors you want to notify. + +
+
+ + { + field.onChange( + checked ? monitors.map((m) => m.id) : [], + ); + }} + /> + + +
+ {monitors.map((item) => ( +
+ + { + const newValue = checked + ? [...(field.value || []), item.id] + : field.value?.filter((id) => id !== item.id); + field.onChange(newValue); + }} + /> + + +
+ ))} +
+ +
+ )} + /> +
+ + + ); +} diff --git a/apps/dashboard/src/components/forms/notifications/form.tsx b/apps/dashboard/src/components/forms/notifications/form.tsx index d258430c0..5a4e0b92b 100644 --- a/apps/dashboard/src/components/forms/notifications/form.tsx +++ b/apps/dashboard/src/components/forms/notifications/form.tsx @@ -32,6 +32,7 @@ const schema = z.object({ "pagerduty", "ntfy", "telegram", + "whatsapp", ]), data: z.record(z.string(), z.string()).or(z.string()), monitors: z.array(z.number()), diff --git a/apps/dashboard/src/data/notifications.client.ts b/apps/dashboard/src/data/notifications.client.ts index f93de98fd..aa8d3b231 100644 --- a/apps/dashboard/src/data/notifications.client.ts +++ b/apps/dashboard/src/data/notifications.client.ts @@ -7,7 +7,8 @@ import { FormSlack } from "@/components/forms/notifications/form-slack"; import { FormSms } from "@/components/forms/notifications/form-sms"; import { FormTelegram } from "@/components/forms/notifications/form-telegram"; import { FormWebhook } from "@/components/forms/notifications/form-webhook"; -import { DiscordIcon, TelegramIcon } from "@openstatus/icons"; +import { FormWhatsApp } from "@/components/forms/notifications/form-whatsapp"; +import { DiscordIcon, TelegramIcon, WhatsappIcon } from "@openstatus/icons"; import { OpsGenieIcon } from "@openstatus/icons"; import { PagerDutyIcon } from "@openstatus/icons"; import { SlackIcon } from "@openstatus/icons"; @@ -17,6 +18,7 @@ import { sendTest as sendTestOpsGenie } from "@openstatus/notification-opsgenie" import { sendTest as sendTestPagerDuty } from "@openstatus/notification-pagerduty"; import { sendTestSlackMessage as sendTestSlack } from "@openstatus/notification-slack"; import { sendTest as sendTestTelegram } from "@openstatus/notification-telegram"; +import { sendTest as sendWhatsAppTest } from "@openstatus/notification-twillio-whatsapp"; import { sendTest as sendTestWebhook } from "@openstatus/notification-webhook"; import { BellIcon, @@ -112,6 +114,12 @@ export const config = { form: FormTelegram, sendTest: sendTestTelegram, }, + whatsapp: { + icon: WhatsappIcon, + label: "WhatsApp", + form: FormWhatsApp, + sendTest: sendWhatsAppTest, + }, }; export type NotifierProvider = keyof typeof config; diff --git a/apps/docs/src/content/docs/reference/notification.mdx b/apps/docs/src/content/docs/reference/notification.mdx index 347ef0d3d..406932474 100644 --- a/apps/docs/src/content/docs/reference/notification.mdx +++ b/apps/docs/src/content/docs/reference/notification.mdx @@ -22,7 +22,14 @@ Require a [discord webhook](https://support.discord.com/hc/en-us/articles/228383 ### SMS -Require a phone number +Require a phone number (international format e.g. +14155552671) + +Due to provider routing, SMS may not be delivered to all countries. Please contact support if you encounter any issues. Or use whatsapp notification instead. + +### WhatsApp + +Require a phone number (international format e.g. +14155552671) + ### Telegram @@ -32,8 +39,6 @@ Requires to know the chat id. The bot's id is `@openstatushq_bot` to make sure you are getting the correct notifications. -Please - ### Webhook - URL (require) diff --git a/apps/web/public/assets/changelog/whatsapp.png b/apps/web/public/assets/changelog/whatsapp.png new file mode 100644 index 0000000000000000000000000000000000000000..e03543a121d6be8885ffb4076c5d4fb341ed8534 GIT binary patch literal 22027 zcmeAS@N?(olHy`uVBq!ia0y~yVA;UHz%-eIje&uI^W?`|5LMt2SsA%|=1FKrYI2Q@oZ8gyPfBt$#fT>j4Q`4Dl|IS`NDjpxR|9tke*s`0y zRL?LmFs$SEp5p&ty$r*Q$t`x>-C|}-A89QNIkZa5EOzz*&4*#j{;se8yV}n>F>G1w z1*01O5|84$PZzmeCwzHahj@#^A{cQn@ZMURNQg|)@|A%{R zp3|0)21W)Zora$R35;i)`x(QUR`Wdcj+kN>Vw_oTqFy7#yE$I424y;^?!b| zq-|WVKxb+M=cb_Et%ta@WAdFP7!wK=4=~NJW)wSM^F<^*&wlTF-udU;n@Q?{~57J6aW68Sbp$+`#b6uYuKoe=kb} z=VZMd51OPnRQJsCH88RBDfsd+q+n@%=)be_*DL+n{w-akJvH{RV)$<1hdJ*vW_YbJ zsoekbZ2rE8yC(%1)_F7OH0%TggERj_Z|?O$f2VxNG7Dlfl`@%I|L^Bv|N38-AG+KB zx>#^Zb2*Rt+>%Qlp4!OBl6uJozS?SyPoRiuN2~L zkd|eH8_N?Lb$+#3&IaDc-UcSSV%F{t-BK6YE>m>k!@uwQ_p^mBUKpDF!p?5K1n=Ug z(6Gr2xsQ9z<>q~!^PI=}-Hyi_-fTMU$FC{FzwuK=-0ajd=Yu<^8od4WuJfwXW{=B1 zx%KyK(DT+h5cR?A*PNqQ>knBm^h8a0zSXssPZXAh@=h?UHg)!Am~6jI_|z7;khS|m ze>?ACb>9EusD9h^z3)m*4|o+^oj3byM(>Su?e!F*IxU(nzy9~_*B=gLT$bfu z?lya0dh93n8mBd{KOcIyzW(p)51sxsNfy6eEPnmp`26jA-?kn8`@a5v;`P{a*=teR zYk%!IYRQuI+je2XHO2DUIl0CEKE8D6o4jrPrt_`I2fpg=7dBe5cTT~pc4hO(yq&B6 z{y7r=H|xj4jmNKXJ-NMh$D<7|)n@aQ?|mKHzBA+b&&z(+xfUOf2)C!6wz(FWKKJ3Z z?YpjCoi}~qJA2!AJ032X>@WB4XMa6U`Oed^ZM^0;7Vxdyug3vPjOHN?Q|(s-@0_x* zTvtLW_td>ZYs74xeC)4zqI|gi|M&XqrxuH4J$?SxG;;c$ygf|WllItMJ}ZCkak=uD z@2B zeP8>2!)d+UJl(rPuBlG&PQAx&_n}cr>)dh4^f`t$uOrV()qR>gpJ(^`eZN0kn!c}u zVaK}Ox{MFMgZ(7!KF>VQqdu?XQbG48#FU9*m&wbA`|JRr0uRnbL zl5nP7@aOlNPh+EC}`S^mn>^!epK|E%8c`TXj`&-Az3ZWpYtzMH*c8%qR3@$zcD14&BtGoJUq5)F%Z zm{q@^pRYf=Up-~lk4N3@&+Y&JJUp-ZUFFsXnKe5e<{Vyq;`66JA75_e+MsiP_PsC5 zq@>>N{eJJTm-bo{*(qBEvSYMPMs-YG|JrNX$M5_9|J`u6?Dkdexi`{g=eB_|Z0U-} z-}imro4Eh)H|cYh&*waxdA=r%caz-D6YhR{7-sd}+>>V~8pmCI_C?L+^PA62tTp^K zr!_RWmu=xqf6nznDSu!3*GE0rbmq#D{kG+ar}JMZ->G=Ix4zQSzV`R+`*PoY{(1iT z{bbS8J1hRo`EdLGzp@*vTcTG^t9r;CZ?UHKeKo(m&A0Z$>fdh^-`LFJTOOG@_1AB; z=j(Yk{=b&kezo9rpTCxh0pzd?AiCH>uXYfT-(0S)aHvK zzhv2sy5GMV&ur(nulZ3Ed93!^-SUdFtK)5LS!5er-5Xdp%s6qOi$(vZ)ODv8Go252 ziqEeNb&vmj>-sDCH$NX+KXmH}59qBkvW;H*KJ0Q+mydO#=ikHs7xK&Q`_QV-vwHo$ zUthMUx7z&tx6FKRCU4BT*9%+nYaUB4KfGGD?8d^jthbAlMcsC;eiVK0(=@9$?|Abo zoYzKpIaK_5xt#yFOtDYxAH)7ipS3loxBYo6|KH-y$G-A4j^TQJQ9mwS(|#D&w2`OL zbE-qz9GgE6`Q`KC^(NgaTO}Tz`a7i~PU^w(Pdb;+$tt`rS#HhSUvWga&HBzmzUw#Q zb~gNe$X{>q=Uw^!)y@${e+rv<%~sf-FPc|yi1V=d{-0-GGH6X@{`vpzyRz+xXHBoq znHSfza{m86&%fqPUa1yQez&y!_r34?E;C*~sXpIErs~DQ%fA&rTfbP){K_b`ZvU^V z>tFp!oqOrT{@n9xs`K~?Uaeft_kHL2+E;hAZd*N1KQ34Orr=KD@vF(jy{6YB%VlcDw(SlK(Wl_{R19|Gp+JF5=sD z_idiJOx+Q|@Y&&Y;RvbLo!eh-*IIK?Yu^07Z_>9u zdMSPXhZ}FeErY|JieXL@bdsIszs}M~)a&B2`|;q{|LW=fukHVR?2liZ`TKSJ|6dQM z@Bj1k!&3d)m3r^X_y4{tWudy~R_5}#uZoRiKCOPrq)Q%B z_s?pXw39ofs#=xD`@*!Wl_6VF|NdJZb71f5*!QdUo(~e8uDC?M;vje1-|zS9e^s3L zv≦;)iS7_ibgHu)g+f_G?)!;VEe=vabC-z|1eR?N-)ms}^;|2cTNwR=L{k?5Uw) zD=(j#Fz5fT_5W8XhHi`15`DC0^Es(`-}gMfnz==qJ!Rj@iqkfm&$fgYJf8abs`9By z^Znb6=De4_Z?PpT)zmWl-$s^q<@-L*Z9gmb^@Eje$Q6H<4)rf&b=SMN`$PyKwQ@adlVn=QLPsPXJBjhr^efA*aI%{4QR zElr)t5b%rjbDBduTyMAUZ20w|-9CnSON)Q@`n})2Y!PPJDxeb@`ciuu*Pb=*ze;WY zee7SK7;tEbjrnao(Q?kcMTJwj_l0Gsj}-zK!%Ea98&!!b4Y-@Eet zyTeYq)=hK!v$D%OVcG}7&u6WAvwR=9Z+&&(+7kP?&$F&yanL!QKTWasA1JNyrq8ea z_Tf#s{cg6D1HIR?RY^U6g0P zo8{?REsLo!>#Lq>zqT}F{QKhl+?H7TpD+E}-)_IXE^O*QTY0XYxyOsvRYh1nU+T2_ z@4Alt)9xq#R8W3?He@~1h8LV07_6?ZN}VRXTRQmiFKJh;u6=)=>Yso8uK4<~B?+r| zQ@v9!U!MQ(OI4zgR8G(7^Fm!c-|}{u3CXCkoHDvooVDWk3$x7s{wJm|X#D#$eZP;s zx3tiVRoh#1R+^o!?pHbvavt>!|1(_`P6{Lw#$kFrzKXr z6ra|$I>bCRB39*!*MZB+WLG}<^<}wztn{koi(cEluk`2LXEeo7Jdg8MkmS_=&u4f% zjhL2vJ9qoqtAR7a+tioM(!QF!(!=^uhw`hV5!19CBjctgXuqDi(DbKWsGwxfjI?(b zo3?uXJ##BrcZoN{w6EPfyU&z^=)E9DC=Y{1@XWNC&TA8=zvOO@V?ut#ctJgPY4|a8&l%(Ea`i zQ#NQE<`foEJ>d0XO84dy-`CeX)qb5Fw03IO`kJk|w!7{uK0SYas-;kc>$Y62<3TH@ z{@n6UFZ0H|wkAI=2X)`-M4?^VlFj!XG)gfDO?=rs&0zJ(n*0^k)l9X!m9FmPoTT>0 zZ^tV(rSSC(^Hl8ZA9%-0>FGUx8awaLla~u$c|Y4TIqMGR^Z75O1E;)v7iwo?@#lnc z+gth1Hy$30w$EUA(6*3?dqMKr^b1Rlz4g`mW9+g%^u&|jud^iXZ8eRWIknD*vqmhW zb!uw<-zDqTve>UZSSj_sPv3uX+uNPOiPdLceBNXuoxi8>Z@skFq(t?VQC-6RHicDE z6JKp?30S$yYx39XJCEhID(MM1EVYl^eK~2GF7LzJTa*)nxZmB{=l?ZIi%n?VQYP)G zK9Wt-6>C>=WRy?sxp*&H_drf#+M&%MnW?o#B1X>4!t<6cEZnV~I@|A=?X8M>waHqy z+@9_@`{6iCOJ>9Te^Jpn8(njy{`aKYEDqbUHJ8!gRzg_-G|KQTWY`GuebQHpt()g z?EWG?+46l%J07k$!1N;G(DW7t?$uSE7E_)*;B*&C(pj_o>W{hX&o41&ue-&1oK-x= zV9yIz{Z)sLi-iPETQ!#@LQ<+ISL^V*y6?NMZ^+rQ{pt(Z_qX85|~))wOu1Lwa+IR>}&oQ;D2PJQ;{ zr&ntDs@um7FS(fN{bTBGgKfJ-(r&Bl@w$3lv0Wx;_Wtu*)*ipUku`a3Q1+L~$*Z

Ptz!C`VoEiRoG?w)8C&zJ-2N2 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, "", "", ""], ["Slack, Discord, Email, Webhook, ntfy.sh", "+", "+", "+"], + ["WhatsApp", "", "+", "+"], ["SMS", "", "+", "+"], ["PagerDuty", "", "+", "+"], ["Number of notification channels", "1", "10", "20"], @@ -52,4 +53,4 @@ Go to the dashboard and get started Create Account ---- \ No newline at end of file +--- diff --git a/apps/workflows/.dockerignore b/apps/workflows/.dockerignore index c304379d7..5dad7bbcf 100644 --- a/apps/workflows/.dockerignore +++ b/apps/workflows/.dockerignore @@ -1,4 +1,4 @@ -# This file is generated by Dofigen v2.5.0 +# This file is generated by Dofigen v2.5.1 # See https://github.com/lenra-io/dofigen node_modules diff --git a/apps/workflows/Dockerfile b/apps/workflows/Dockerfile index a610f4a52..fdfd756a4 100644 --- a/apps/workflows/Dockerfile +++ b/apps/workflows/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1.11 -# This file is generated by Dofigen v2.5.0 +# This file is generated by Dofigen v2.5.1 # See https://github.com/lenra-io/dofigen # ca-certs @@ -41,6 +41,7 @@ RUN \ --mount=type=bind,target=packages/notifications/pagerduty/package.json,source=packages/notifications/pagerduty/package.json \ --mount=type=bind,target=packages/notifications/slack/package.json,source=packages/notifications/slack/package.json \ --mount=type=bind,target=packages/notifications/telegram/package.json,source=packages/notifications/telegram/package.json \ + --mount=type=bind,target=packages/notifications/twillio-whatsapp/package.json,source=packages/notifications/twillio-whatsapp/package.json \ --mount=type=bind,target=packages/notifications/twillio-sms/package.json,source=packages/notifications/twillio-sms/package.json \ --mount=type=bind,target=packages/notifications/webhook/package.json,source=packages/notifications/webhook/package.json \ --mount=type=bind,target=packages/regions/package.json,source=packages/regions/package.json \ @@ -84,7 +85,7 @@ RUN bun install # runtime FROM debian@sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b AS runtime LABEL \ - io.dofigen.version="2.5.0" \ + io.dofigen.version="2.5.1" \ org.opencontainers.image.authors="OpenStatus Team" \ org.opencontainers.image.base.digest="sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b" \ org.opencontainers.image.base.name="docker.io/debian:bullseye-slim" \ diff --git a/apps/workflows/dofigen.lock b/apps/workflows/dofigen.lock index 482430636..ff685d36f 100644 --- a/apps/workflows/dofigen.lock +++ b/apps/workflows/dofigen.lock @@ -12,27 +12,35 @@ effective: | - /packages/error - /packages/tracker builders: - build: + docker: fromImage: path: oven/bun digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 label: org.opencontainers.image.base.name: docker.io/oven/bun:1.3.3 - org.opencontainers.image.stage: build org.opencontainers.image.base.digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 workdir: /app/apps/workflows - env: - NODE_ENV: production copy: - paths: - . target: /app/ - - fromBuilder: install + run: + - bun run src/build-docker.ts + libsql: + fromImage: + path: oven/bun + digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 + label: + org.opencontainers.image.base.name: docker.io/oven/bun:1.3.3 + org.opencontainers.image.base.digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 + workdir: /app/ + copy: + - fromBuilder: docker paths: - - /app/node_modules - target: /app/node_modules + - /app/apps/build-docker/package.json + target: /app/package.json run: - - bun build --compile --target bun --sourcemap --format=cjs src/index.ts --outfile=app --external '@libsql/*' --external libsql + - bun install install: fromImage: path: oven/bun @@ -73,6 +81,8 @@ effective: | source: packages/notifications/slack/package.json - target: packages/notifications/telegram/package.json source: packages/notifications/telegram/package.json + - target: packages/notifications/twillio-whatsapp/package.json + source: packages/notifications/twillio-whatsapp/package.json - target: packages/notifications/twillio-sms/package.json source: packages/notifications/twillio-sms/package.json - target: packages/notifications/webhook/package.json @@ -89,20 +99,27 @@ effective: | source: packages/upstash/package.json - target: packages/theme-store/package.json source: packages/theme-store/package.json - docker: + build: fromImage: path: oven/bun digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 label: org.opencontainers.image.base.name: docker.io/oven/bun:1.3.3 org.opencontainers.image.base.digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 + org.opencontainers.image.stage: build workdir: /app/apps/workflows + env: + NODE_ENV: production copy: - paths: - . target: /app/ + - fromBuilder: install + paths: + - /app/node_modules + target: /app/node_modules run: - - bun run src/build-docker.ts + - bun build --compile --target bun --sourcemap --format=cjs src/index.ts --outfile=app --external '@libsql/*' --external libsql ca-certs: fromImage: path: debian @@ -112,33 +129,18 @@ effective: | org.opencontainers.image.base.digest: sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b run: - apt update && apt install -y ca-certificates && update-ca-certificates - libsql: - fromImage: - path: oven/bun - digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 - label: - org.opencontainers.image.base.name: docker.io/oven/bun:1.3.3 - org.opencontainers.image.base.digest: sha256:fbf8e67e9d3b806c86be7a2f2e9bae801f2d9212a21db4dcf8cc9889f5a3c9c4 - workdir: /app/ - copy: - - fromBuilder: docker - paths: - - /app/apps/build-docker/package.json - target: /app/package.json - run: - - bun install fromImage: path: debian digest: sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b label: + org.opencontainers.image.description: Background job processing and probe scheduling for OpenStatus + org.opencontainers.image.authors: OpenStatus Team + io.dofigen.version: 2.5.1 org.opencontainers.image.source: https://github.com/openstatusHQ/openstatus + org.opencontainers.image.base.digest: sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b org.opencontainers.image.vendor: OpenStatus - org.opencontainers.image.title: OpenStatus Workflows org.opencontainers.image.base.name: docker.io/debian:bullseye-slim - org.opencontainers.image.base.digest: sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b - org.opencontainers.image.authors: OpenStatus Team - io.dofigen.version: 2.5.1 - org.opencontainers.image.description: Background job processing and probe scheduling for OpenStatus + org.opencontainers.image.title: OpenStatus Workflows workdir: /app/ copy: - fromBuilder: build @@ -174,7 +176,7 @@ images: digest: sha256:530a3348fc4b5734ffe1a137ddbcee6850154285251b53c3425c386ea8fac77b resources: dofigen.yml: - hash: 297382ef47be04c67f610380fadff8e2b9333ab28594ff6e0512b75a23d4c3b0 + hash: 7e59e1efd94f649c01720f89c8966945798a8a0c4572e2fb783d6e406e5384c4 content: | ignore: - node_modules @@ -208,6 +210,7 @@ resources: - packages/notifications/pagerduty/package.json - packages/notifications/slack/package.json - packages/notifications/telegram/package.json + - packages/notifications/twillio-whatsapp/package.json - packages/notifications/twillio-sms/package.json - packages/notifications/webhook/package.json - packages/regions/package.json @@ -284,5 +287,5 @@ resources: - fromBuilder: ca-certs source: /etc/ssl/certs/ca-certificates.crt target: /etc/ssl/certs/ - expose: 3000 + expose: "3000" entrypoint: /app/apps/workflows/app diff --git a/apps/workflows/dofigen.yml b/apps/workflows/dofigen.yml index 5795361b2..625c24a7e 100644 --- a/apps/workflows/dofigen.yml +++ b/apps/workflows/dofigen.yml @@ -30,6 +30,7 @@ builders: - packages/notifications/pagerduty/package.json - packages/notifications/slack/package.json - packages/notifications/telegram/package.json + - packages/notifications/twillio-whatsapp/package.json - packages/notifications/twillio-sms/package.json - packages/notifications/webhook/package.json - packages/regions/package.json @@ -106,5 +107,5 @@ copy: - fromBuilder: ca-certs source: /etc/ssl/certs/ca-certificates.crt target: /etc/ssl/certs/ -expose: 3000 +expose: "3000" entrypoint: /app/apps/workflows/app diff --git a/apps/workflows/package.json b/apps/workflows/package.json index 1ec136118..c53ef502b 100644 --- a/apps/workflows/package.json +++ b/apps/workflows/package.json @@ -21,6 +21,7 @@ "@openstatus/notification-slack": "workspace:*", "@openstatus/notification-telegram": "workspace:*", "@openstatus/notification-twillio-sms": "workspace:*", + "@openstatus/notification-twillio-whatsapp": "workspace:*", "@openstatus/notification-webhook": "workspace:*", "@openstatus/regions": "workspace:*", "@openstatus/tinybird": "workspace:*", diff --git a/apps/workflows/src/checker/utils.ts b/apps/workflows/src/checker/utils.ts index 413222fb1..d905920bd 100644 --- a/apps/workflows/src/checker/utils.ts +++ b/apps/workflows/src/checker/utils.ts @@ -44,6 +44,11 @@ import { sendDegraded as sendSmsDegraded, sendRecovery as sendSmsRecovery, } from "@openstatus/notification-twillio-sms"; +import { + sendAlert as sendWhatsappAlert, + sendDegraded as sendWhatsappDegraded, + sendRecovery as sendWhatsappRecovery, +} from "@openstatus/notification-twillio-whatsapp"; import { sendAlert as sendWebhookAlert, sendDegraded as sendWebhookDegraded, @@ -117,6 +122,11 @@ export const providerToFunction = { sendRecovery: sendWebhookRecovery, sendDegraded: sendWebhookDegraded, }, + whatsapp: { + sendAlert: sendWhatsappAlert, + sendRecovery: sendWhatsappRecovery, + sendDegraded: sendWhatsappDegraded, + }, telegram: { sendAlert: sendTelegramAlert, sendRecovery: sendTelegramRecovery, diff --git a/packages/api/package.json b/packages/api/package.json index 7257ad0af..effd32665 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -14,6 +14,7 @@ "@openstatus/emails": "workspace:*", "@openstatus/error": "workspace:*", "@openstatus/notification-telegram": "workspace:*", + "@openstatus/notification-twillio-whatsapp": "workspace:*", "@openstatus/regions": "workspace:*", "@openstatus/tinybird": "workspace:*", "@openstatus/upstash": "workspace:*", diff --git a/packages/api/src/router/notification.ts b/packages/api/src/router/notification.ts index 74598f216..198a4a4ff 100644 --- a/packages/api/src/router/notification.ts +++ b/packages/api/src/router/notification.ts @@ -12,11 +12,13 @@ import { selectMonitorSchema, selectNotificationSchema, telegramDataSchema, + whatsappDataSchema, } from "@openstatus/db/src/schema"; import { Events } from "@openstatus/analytics"; import { SchemaError } from "@openstatus/error"; import { sendTest as sendTelegramTest } from "@openstatus/notification-telegram"; +import { sendTest as sendWhatsAppTest } from "@openstatus/notification-twillio-whatsapp"; import { createTRPCRouter, protectedProcedure } from "../trpc"; export const notificationRouter = createTRPCRouter({ @@ -476,6 +478,18 @@ export const notificationRouter = createTRPCRouter({ return; } + if (opts.input.provider === "whatsapp") { + const _data = whatsappDataSchema.safeParse(opts.input.data); + if (!_data.success) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: SchemaError.fromZod(_data.error, opts.input).message, + }); + } + await sendWhatsAppTest({ phoneNumber: _data.data.whatsapp }); + + return; + } throw new TRPCError({ code: "BAD_REQUEST", diff --git a/packages/db/src/schema/notifications/constants.ts b/packages/db/src/schema/notifications/constants.ts index 4d840ba82..fbc194e1d 100644 --- a/packages/db/src/schema/notifications/constants.ts +++ b/packages/db/src/schema/notifications/constants.ts @@ -1,11 +1,12 @@ export const notificationProvider = [ - "email", "discord", + "email", "ntfy", "pagerduty", "opsgenie", "slack", "sms", - "webhook", "telegram", + "webhook", + "whatsapp", ] as const; diff --git a/packages/db/src/schema/notifications/validation.ts b/packages/db/src/schema/notifications/validation.ts index 33b5e5c3e..861cd95cf 100644 --- a/packages/db/src/schema/notifications/validation.ts +++ b/packages/db/src/schema/notifications/validation.ts @@ -66,16 +66,21 @@ export const telegramDataSchema = z.object({ telegram: z.object({ chatId: z.string() }), }); +export const whatsappDataSchema = z.object({ + whatsapp: phoneSchema, +}); + export const NotificationDataSchema = z.union([ + discordDataSchema, emailDataSchema, + ntfyDataSchema, + opsgenieDataSchema, + pagerdutyDataSchema, phoneDataSchema, + telegramDataSchema, slackDataSchema, - discordDataSchema, - pagerdutyDataSchema, - opsgenieDataSchema, - ntfyDataSchema, webhookDataSchema, - telegramDataSchema, + whatsappDataSchema, ]); export const InsertNotificationWithDataSchema = z.discriminatedUnion( @@ -129,6 +134,12 @@ export const InsertNotificationWithDataSchema = z.discriminatedUnion( data: webhookDataSchema, }), ), + insertNotificationSchema.merge( + z.object({ + provider: z.literal("whatsapp"), + data: whatsappDataSchema, + }), + ), ], ); diff --git a/packages/db/src/schema/plan/config.ts b/packages/db/src/schema/plan/config.ts index 4d045f281..619b8ef48 100644 --- a/packages/db/src/schema/plan/config.ts +++ b/packages/db/src/schema/plan/config.ts @@ -54,6 +54,7 @@ export const allPlans: Record< "audit-log": false, regions: [...FREE_FLY_REGIONS], "private-locations": false, + whatsapp: false, }, }, starter: { @@ -93,6 +94,7 @@ export const allPlans: Record< "audit-log": false, regions: [...AVAILABLE_REGIONS], "private-locations": false, + whatsapp: true, }, }, team: { @@ -132,6 +134,7 @@ export const allPlans: Record< "audit-log": true, regions: [...AVAILABLE_REGIONS], "private-locations": true, + whatsapp: true, }, }, }; diff --git a/packages/db/src/schema/plan/schema.ts b/packages/db/src/schema/plan/schema.ts index d40e58f03..d20e0ac8b 100644 --- a/packages/db/src/schema/plan/schema.ts +++ b/packages/db/src/schema/plan/schema.ts @@ -39,6 +39,7 @@ export const limitsSchema = z.object({ notifications: z.boolean().default(true), pagerduty: z.boolean().default(false), opsgenie: z.boolean().default(false), + whatsapp: z.boolean().default(false), sms: z.boolean().default(false), "sms-limit": z.number().default(0), "notification-channels": z.number().default(1), diff --git a/packages/icons/src/index.tsx b/packages/icons/src/index.tsx index a013b6a86..553ab2bb4 100644 --- a/packages/icons/src/index.tsx +++ b/packages/icons/src/index.tsx @@ -8,3 +8,4 @@ export * from "./fly"; export * from "./railway"; export * from "./koyeb"; export * from "./telegram"; +export * from "./whatsapp"; diff --git a/packages/icons/src/whatsapp.tsx b/packages/icons/src/whatsapp.tsx new file mode 100644 index 000000000..1d4735756 --- /dev/null +++ b/packages/icons/src/whatsapp.tsx @@ -0,0 +1,15 @@ +export function WhatsappIcon(props: React.ComponentProps<"svg">) { + return ( + + WhatsApp + + + ); +} diff --git a/packages/notifications/twillio-whatsapp/.env.example b/packages/notifications/twillio-whatsapp/.env.example new file mode 100644 index 000000000..07a929433 --- /dev/null +++ b/packages/notifications/twillio-whatsapp/.env.example @@ -0,0 +1,2 @@ +TWILLIO_AUTH_TOKEN=your_auth_token +TWILLIO_ACCOUNT_ID=your_account_id diff --git a/packages/notifications/twillio-whatsapp/package.json b/packages/notifications/twillio-whatsapp/package.json new file mode 100644 index 000000000..944ddb8e9 --- /dev/null +++ b/packages/notifications/twillio-whatsapp/package.json @@ -0,0 +1,21 @@ +{ + "name": "@openstatus/notification-twillio-whatsapp", + "version": "0.0.0", + "main": "src/index.ts", + "scripts": { + "test": "bun test" + }, + "dependencies": { + "@openstatus/db": "workspace:*", + "@t3-oss/env-core": "0.7.1", + "validator": "13.12.0", + "zod": "3.25.76" + }, + "devDependencies": { + "@openstatus/tsconfig": "workspace:*", + "@types/node": "22.10.2", + "@types/validator": "13.12.0", + "bun-types": "1.3.1", + "typescript": "5.7.2" + } +} diff --git a/packages/notifications/twillio-whatsapp/src/env.ts b/packages/notifications/twillio-whatsapp/src/env.ts new file mode 100644 index 000000000..49035cca7 --- /dev/null +++ b/packages/notifications/twillio-whatsapp/src/env.ts @@ -0,0 +1,30 @@ +import { createEnv } from "@t3-oss/env-core"; +import { z } from "zod"; + +export const env = createEnv({ + server: { + TWILLIO_AUTH_TOKEN: z.string(), + TWILLIO_ACCOUNT_ID: z.string(), + }, + + /** + * What object holds the environment variables at runtime. This is usually + * `process.env` or `import.meta.env`. + */ + runtimeEnv: process.env, + + /** + * By default, this library will feed the environment variables directly to + * the Zod validator. + * + * This means that if you have an empty string for a value that is supposed + * to be a number (e.g. `PORT=` in a ".env" file), Zod will incorrectly flag + * it as a type mismatch violation. Additionally, if you have an empty string + * for a value that is supposed to be a string with a default value (e.g. + * `DOMAIN=` in an ".env" file), the default value will never be applied. + * + * In order to solve these issues, we recommend that all new projects + * explicitly specify this option as true. + */ + skipValidation: true, +}); diff --git a/packages/notifications/twillio-whatsapp/src/index.test.ts b/packages/notifications/twillio-whatsapp/src/index.test.ts new file mode 100644 index 000000000..7b680a79b --- /dev/null +++ b/packages/notifications/twillio-whatsapp/src/index.test.ts @@ -0,0 +1,132 @@ +import { + afterEach, + beforeEach, + describe, + expect, + jest, + spyOn, + test, +} from "bun:test"; +import { selectNotificationSchema } from "@openstatus/db/src/schema"; +import { sendAlert, sendDegraded, sendRecovery } from "./index"; + +describe("whatsapp Notifications", () => { + // biome-ignore lint/suspicious/noExplicitAny: + let fetchMock: any = undefined; + + beforeEach(() => { + // @ts-expect-error + fetchMock = spyOn(global, "fetch").mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test("Send degraded", async () => { + const monitor = { + id: "monitor-1", + name: "API Health Check", + url: "https://api.example.com/health", + jobType: "http" as const, + periodicity: "5m" as const, + status: "active" as const, // or "down", "degraded" + createdAt: new Date(), + updatedAt: new Date(), + region: "us-east-1", + }; + + const a = { + id: 1, + name: "slack Notification", + provider: "slack", + workspaceId: 1, + createdAt: new Date(), + updatedAt: new Date(), + data: '{"whatsapp":"+33623456789"}', + }; + + const n = selectNotificationSchema.parse(a); + await sendDegraded({ + // @ts-expect-error + monitor, + notification: n, + statusCode: 500, + message: "Something went wrong", + cronTimestamp: Date.now(), + }); + expect(fetchMock).toHaveBeenCalled(); + }); + + test("Send Recovered", async () => { + const monitor = { + id: "monitor-1", + name: "API Health Check", + url: "https://api.example.com/health", + jobType: "http" as const, + periodicity: "5m" as const, + status: "active" as const, // or "down", "degraded" + createdAt: new Date(), + updatedAt: new Date(), + region: "us-east-1", + }; + + const a = { + id: 1, + name: "slack Notification", + provider: "slack", + workspaceId: 1, + createdAt: new Date(), + updatedAt: new Date(), + data: '{"whatsapp":"+33623456789"}', + }; + + const n = selectNotificationSchema.parse(a); + await sendRecovery({ + // @ts-expect-error + monitor, + notification: n, + statusCode: 500, + message: "Something went wrong", + cronTimestamp: Date.now(), + }); + expect(fetchMock).toHaveBeenCalled(); + }); + + test("Send Alert", async () => { + const monitor = { + id: "monitor-1", + name: "API Health Check", + url: "https://api.example.com/health", + jobType: "http" as const, + periodicity: "5m" as const, + status: "active" as const, // or "down", "degraded" + createdAt: new Date(), + updatedAt: new Date(), + region: "us-east-1", + }; + const a = { + id: 1, + name: "slack Notification", + provider: "slack", + workspaceId: 1, + createdAt: new Date(), + updatedAt: new Date(), + data: '{"whatsapp":"+33623456789"}', + }; + + const n = selectNotificationSchema.parse(a); + + await sendAlert({ + // @ts-expect-error + monitor, + notification: n, + statusCode: 500, + message: "Something went wrong", + cronTimestamp: Date.now(), + }); + expect(fetchMock).toHaveBeenCalled(); + }); +}); diff --git a/packages/notifications/twillio-whatsapp/src/index.ts b/packages/notifications/twillio-whatsapp/src/index.ts new file mode 100644 index 000000000..89ae4071b --- /dev/null +++ b/packages/notifications/twillio-whatsapp/src/index.ts @@ -0,0 +1,175 @@ +import type { Monitor, Notification } from "@openstatus/db/src/schema"; +import { whatsappDataSchema } from "@openstatus/db/src/schema"; +import type { Region } from "@openstatus/db/src/schema/constants"; +import { env } from "./env"; + +export const sendAlert = async ({ + monitor, + notification, + statusCode, + message, + // biome-ignore lint/correctness/noUnusedVariables: + incidentId, +}: { + monitor: Monitor; + notification: Notification; + statusCode?: number; + message?: string; + incidentId?: string; + cronTimestamp: number; + latency?: number; + region?: Region; +}) => { + const notificationData = whatsappDataSchema.parse( + JSON.parse(notification.data), + ); + const contentVariables = JSON.stringify({ url: monitor.url }); + + const body = new FormData(); + body.set("To", `whatsapp:${notificationData.whatsapp}`); + body.set("From", "whatsapp:+14807252613"); + body.set("ContentSid", "HX8282106bfaecb7939e69f9c5564babe5"); + body.set("ContentVariables", contentVariables); + + try { + await fetch( + `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, + { + method: "post", + body, + headers: { + Authorization: `Basic ${btoa( + `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, + )}`, + }, + }, + ); + } catch (err) { + console.log(err); + // Do something + } +}; + +export const sendRecovery = async ({ + monitor, + notification, + // biome-ignore lint/correctness/noUnusedVariables: + statusCode, + // biome-ignore lint/correctness/noUnusedVariables: + message, + // biome-ignore lint/correctness/noUnusedVariables: + incidentId, +}: { + monitor: Monitor; + notification: Notification; + statusCode?: number; + message?: string; + incidentId?: string; + cronTimestamp: number; + latency?: number; + region?: Region; +}) => { + const notificationData = whatsappDataSchema.parse( + JSON.parse(notification.data), + ); + const contentVariables = JSON.stringify({ url: monitor.url }); + + const body = new FormData(); + body.set("To", `whatsapp:${notificationData.whatsapp}`); + body.set("From", "whatsapp:+14807252613"); + body.set("ContentSid", "HX8fdeb4201bed18ac8838b3c0135bbf28"); + body.set("ContentVariables", contentVariables); + + try { + await fetch( + `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, + { + method: "post", + body, + headers: { + Authorization: `Basic ${btoa( + `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, + )}`, + }, + }, + ); + } catch (err) { + console.log(err); + // Do something + } +}; + +export const sendDegraded = async ({ + monitor, + notification, + // biome-ignore lint/correctness/noUnusedVariables: + statusCode, + // biome-ignore lint/correctness/noUnusedVariables: + message, +}: { + monitor: Monitor; + notification: Notification; + statusCode?: number; + message?: string; + incidentId?: string; + cronTimestamp: number; + latency?: number; + region?: Region; +}) => { + const notificationData = whatsappDataSchema.parse( + JSON.parse(notification.data), + ); + const contentVariables = JSON.stringify({ url: monitor.url }); + + const body = new FormData(); + body.set("To", `whatsapp:${notificationData.whatsapp}`); + body.set("From", "whatsapp:+14807252613"); + body.set("ContentSid", "HX35589f2e7ac8b8be63f4bd62a60e435f"); + body.set("ContentVariables", contentVariables); + + try { + await fetch( + `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, + { + method: "post", + body, + headers: { + Authorization: `Basic ${btoa( + `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, + )}`, + }, + }, + ); + } catch (err) { + console.log(err); + // Do something + } +}; + +export const sendTest = async ({ phoneNumber }: { phoneNumber: string }) => { + const contentVariables = JSON.stringify({ url: "https://openstat.us" }); + const body = new FormData(); + body.set("To", `whatsapp:${phoneNumber}`); + body.set("ContentSid", "HX36ac9074ebda4376c7d6ddd1690b5291"); + body.set("From", "whatsapp:+14807252613"); + body.set("ContentVariables", contentVariables); + + console.log("send data"); + try { + await fetch( + `https://api.twilio.com/2010-04-01/Accounts/${env.TWILLIO_ACCOUNT_ID}/Messages.json`, + { + method: "post", + body, + headers: { + Authorization: `Basic ${btoa( + `${env.TWILLIO_ACCOUNT_ID}:${env.TWILLIO_AUTH_TOKEN}`, + )}`, + }, + }, + ); + } catch (err) { + console.log(err); + // Do something + } +}; diff --git a/packages/notifications/twillio-whatsapp/tsconfig.json b/packages/notifications/twillio-whatsapp/tsconfig.json new file mode 100644 index 000000000..b4382e67e --- /dev/null +++ b/packages/notifications/twillio-whatsapp/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@openstatus/tsconfig/nextjs.json", + "include": ["src", "*.ts"], + "compilerOptions": { + "types": ["bun-types"] + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f879ee2f..fe4432b30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,9 @@ importers: '@openstatus/notification-telegram': specifier: workspace:* version: link:../../packages/notifications/telegram + '@openstatus/notification-twillio-whatsapp': + specifier: workspace:* + version: link:../../packages/notifications/twillio-whatsapp '@openstatus/notification-webhook': specifier: workspace:* version: link:../../packages/notifications/webhook @@ -1091,6 +1094,9 @@ importers: '@openstatus/notification-twillio-sms': specifier: workspace:* version: link:../../packages/notifications/twillio-sms + '@openstatus/notification-twillio-whatsapp': + specifier: workspace:* + version: link:../../packages/notifications/twillio-whatsapp '@openstatus/notification-webhook': specifier: workspace:* version: link:../../packages/notifications/webhook @@ -1174,6 +1180,9 @@ importers: '@openstatus/notification-telegram': specifier: workspace:* version: link:../notifications/telegram + '@openstatus/notification-twillio-whatsapp': + specifier: workspace:* + version: link:../notifications/twillio-whatsapp '@openstatus/regions': specifier: workspace:* version: link:../regions @@ -1642,6 +1651,37 @@ importers: specifier: 5.7.2 version: 5.7.2 + packages/notifications/twillio-whatsapp: + dependencies: + '@openstatus/db': + specifier: workspace:* + version: link:../../db + '@t3-oss/env-core': + specifier: 0.7.1 + version: 0.7.1(typescript@5.7.2)(zod@3.25.76) + validator: + specifier: 13.12.0 + version: 13.12.0 + zod: + specifier: 3.25.76 + version: 3.25.76 + devDependencies: + '@openstatus/tsconfig': + specifier: workspace:* + version: link:../../tsconfig + '@types/node': + specifier: 22.10.2 + version: 22.10.2 + '@types/validator': + specifier: 13.12.0 + version: 13.12.0 + bun-types: + specifier: 1.3.1 + version: 1.3.1(@types/react@19.2.2) + typescript: + specifier: 5.7.2 + version: 5.7.2 + packages/notifications/webhook: dependencies: '@openstatus/db': @@ -2704,7 +2744,6 @@ packages: '@ericcornelissen/bash-parser@0.5.2': resolution: {integrity: sha512-4pIMTa1nEFfMXitv7oaNEWOdM+zpOZavesa5GaiWTgda6Zk32CFGxjUp/iIaN0PwgUW1yTq/fztSjbpE8SLGZQ==} engines: {node: '>=4'} - deprecated: Support for this package will stop 2025-12-31 '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} @@ -6832,7 +6871,6 @@ packages: '@upstash/kafka@1.3.3': resolution: {integrity: sha512-CIr657FZuK+IMuwcxkj3oCB6xKO+LMlHd4BL4J/Lwbpj6+5YHO+5ZcpdMIQhbcemthJcRtE0gDUfZEnrfb3Rjg==} engines: {node: '>=10'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@upstash/qstash@2.6.2': resolution: {integrity: sha512-aB/1yqMJTRyOt7Go2Db1ZIVnmTPpsc2KGY5jpLVcegNtjksaPTJF6fmITxos5HVvsQhS8IB3gvF/+gQfRQlPLQ==} @@ -13385,7 +13423,7 @@ snapshots: '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.6(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@floating-ui/react-dom@2.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@floating-ui/dom': 1.7.4 react: 19.0.0 @@ -14540,9 +14578,9 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.1.9(@types/react@19.2.2) - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: @@ -14631,14 +14669,14 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) - '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.10)(react@19.0.0) react: 19.0.0 @@ -14675,11 +14713,11 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.1.9(@types/react@19.2.2) - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-slot': 1.2.3(@types/react@19.0.10)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) @@ -14859,11 +14897,11 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.0.10)(react@19.0.0) react: 19.0.0 @@ -14900,14 +14938,14 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) - '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) @@ -14959,10 +14997,10 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.1.9(@types/react@19.2.2) - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.10)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) @@ -15091,22 +15129,22 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) - '@radix-ui/react-menu@2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-direction': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-slot': 1.2.3(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.10)(react@19.0.0) aria-hidden: 1.2.6 @@ -15188,19 +15226,19 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) - '@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-slot': 1.2.3(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) aria-hidden: 1.2.6 @@ -15270,13 +15308,13 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) - '@radix-ui/react-popper@1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@floating-ui/react-dom': 2.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-use-rect': 1.1.1(@types/react@19.0.10)(react@19.0.0) @@ -15298,9 +15336,9 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.1.9(@types/react@19.2.2) - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.10)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) @@ -15358,7 +15396,7 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.10)(react@19.0.0) @@ -15377,7 +15415,7 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.1.9(@types/react@19.2.2) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@19.0.10)(react@19.0.0) react: 19.0.0 @@ -15512,15 +15550,15 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-direction': 1.1.1(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) react: 19.0.0 @@ -15717,15 +15755,15 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) - '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-direction': 1.1.1(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) @@ -15749,14 +15787,14 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.1.9(@types/react@19.2.2) - '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-direction': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) @@ -15775,10 +15813,10 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.1.9(@types/react@19.2.2) - '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) @@ -15826,20 +15864,20 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) - '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-slot': 1.2.3(@types/react@19.0.10)(react@19.0.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: @@ -16010,9 +16048,9 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.1.9(@types/react@19.2.2) - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0)': + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: @@ -16153,13 +16191,13 @@ snapshots: '@babel/traverse': 7.27.0 '@lottiefiles/dotlottie-react': 0.13.3(react@19.0.0) '@radix-ui/colors': 3.0.0 - '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-slot': 1.2.3(@types/react@19.0.10)(react@19.0.0) - '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) - '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@types/node': 22.14.1 '@types/normalize-path': 3.0.2 '@types/react': 19.0.10 @@ -16168,7 +16206,7 @@ snapshots: autoprefixer: 10.4.21(postcss@8.4.38) clsx: 2.1.1 esbuild: 0.25.10 - framer-motion: 12.23.22(@emotion/is-prop-valid@1.4.0)(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + framer-motion: 12.23.22(@emotion/is-prop-valid@1.4.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) json5: 2.2.3 log-symbols: 4.1.0 module-punycode: punycode@2.3.1 @@ -16181,7 +16219,7 @@ snapshots: react-dom: 19.0.0(react@19.0.0) sharp: 0.34.4 socket.io-client: 4.8.1 - sonner: 2.0.3(react-dom@19.0.0(react@19.2.2))(react@19.0.0) + sonner: 2.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) source-map-js: 1.2.1 spamc: 0.0.5 stacktrace-parser: 0.1.11 @@ -19175,7 +19213,7 @@ snapshots: fraction.js@4.3.7: {} - framer-motion@12.23.22(@emotion/is-prop-valid@1.4.0)(react-dom@19.0.0(react@19.2.2))(react@19.0.0): + framer-motion@12.23.22(@emotion/is-prop-valid@1.4.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: motion-dom: 12.23.23 motion-utils: 12.23.6 @@ -22433,7 +22471,7 @@ snapshots: react: 19.2.2 react-dom: 19.2.2(react@19.2.2) - sonner@2.0.3(react-dom@19.0.0(react@19.2.2))(react@19.0.0): + sonner@2.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) -- 2.51.2