diff --git a/apps/web/package.json b/apps/web/package.json index 12df8124..5486b5fb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -34,7 +34,6 @@ "@lingui/conf": "^5.3.2", "@lingui/macro": "^5.3.2", "@lingui/react": "^5.3.2", - "@novu/api": "^3.11.0", "@t3-oss/env-nextjs": "^0.11.1", "@tailwindcss/typography": "^0.5.16", "@tanstack/react-query": "catalog:", diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts index ee373512..91e28b56 100644 --- a/apps/web/src/env.ts +++ b/apps/web/src/env.ts @@ -52,11 +52,9 @@ export const env = createEnv({ VK_CLIENT_SECRET: z.string().optional(), LINKEDIN_CLIENT_ID: z.string().optional(), LINKEDIN_CLIENT_SECRET: z.string().optional(), - NOVU_API_KEY: z.string().optional(), SUBSCRIBER_API_URL: z.string().url().optional(), SUBSCRIBER_API_KEY: z.string().optional(), SUBSCRIBER_ENVIRONMENT_ID: z.string().optional(), - EMAIL_UNSUBSCRIBE_SECRET: z.string().optional(), // Generic OIDC Provider OIDC_CLIENT_ID: z.string().optional(), OIDC_CLIENT_SECRET: z.string().optional(), diff --git a/apps/web/src/pages/api/unsubscribe.ts b/apps/web/src/pages/api/unsubscribe.ts deleted file mode 100644 index 783f4d7a..00000000 --- a/apps/web/src/pages/api/unsubscribe.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import { Novu } from "@novu/api"; -import { jwtVerify } from "jose"; -import { z } from "zod"; - -import { withApiLogging } from "@kan/api/utils/apiLogging"; -import { withRateLimit } from "@kan/api/utils/rateLimit"; -import { updateSubscriberPreferences } from "@kan/email"; -import { createLogger } from "@kan/logger"; - -import { env } from "~/env"; - -const log = createLogger("unsubscribe"); - -const requestSchema = z.object({ - token: z.string().min(1), -}); - -const tokenPayloadSchema = z.object({ - subscriberId: z.string(), -}); - -type ResponseData = - | { success: true } - | { success: false; error: string; code?: string }; - -const textEncoder = new TextEncoder(); - -export default withRateLimit( - { points: 100, duration: 60 }, - withApiLogging( - async (req: NextApiRequest, res: NextApiResponse) => { - if (process.env.NEXT_PUBLIC_KAN_ENV !== "cloud") { - return res.status(404).json({ - success: false, - error: "Unsubscribe endpoint is not available.", - code: "UNAVAILABLE", - }); - } - - if (req.method !== "POST") { - res.setHeader("Allow", "POST"); - return res.status(405).json({ - success: false, - error: "Method not allowed.", - code: "METHOD_NOT_ALLOWED", - }); - } - - const parsedBody = requestSchema.safeParse(req.body); - - if (!parsedBody.success) { - return res.status(400).json({ - success: false, - error: "Invalid request payload.", - code: "BAD_REQUEST", - }); - } - - if (!env.EMAIL_UNSUBSCRIBE_SECRET || !env.NOVU_API_KEY) { - return res.status(500).json({ - success: false, - error: "Unsubscribe service is not configured.", - code: "NOT_CONFIGURED", - }); - } - - let payload: z.infer; - - try { - const verified = await jwtVerify( - parsedBody.data.token, - textEncoder.encode(env.EMAIL_UNSUBSCRIBE_SECRET), - { - // We intentionally do not use exp/iat claims – - // tokens are long-lived and validated only by signature + payload. - clockTolerance: "0s", - }, - ); - payload = tokenPayloadSchema.parse(verified.payload); - } catch { - return res.status(401).json({ - success: false, - error: "Your unsubscribe link is invalid or has expired.", - code: "INVALID_TOKEN", - }); - } - - const novu = new Novu({ secretKey: env.NOVU_API_KEY }); - - let novuFailed = false; - try { - await novu.subscribers.preferences.update( - { - channels: { - email: false, - }, - }, - payload.subscriberId, - ); - } catch (error) { - novuFailed = true; - } - - try { - await updateSubscriberPreferences(payload.subscriberId, { - email: false, - }); - } catch (error) { - log.error({ err: error }, "Error updating subscriber preferences"); - } - - if (novuFailed) { - return res.status(502).json({ - success: false, - error: - "We could not update your email preferences right now. Please try again later.", - code: "NOVU_ERROR", - }); - } - - return res.status(200).json({ success: true }); - }, - ), -); diff --git a/apps/web/src/pages/unsubscribe/index.tsx b/apps/web/src/pages/unsubscribe/index.tsx deleted file mode 100644 index 1d75581d..00000000 --- a/apps/web/src/pages/unsubscribe/index.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useRouter } from "next/router"; -import { t } from "@lingui/core/macro"; -import { useEffect, useState } from "react"; - -import Button from "~/components/Button"; -import { PageHead } from "~/components/PageHead"; -import PatternedBackground from "~/components/PatternedBackground"; - -type UnsubscribeStatus = "idle" | "processing" | "success" | "error"; - -export default function UnsubscribePage() { - const router = useRouter(); - const [token, setToken] = useState(""); - const [status, setStatus] = useState("idle"); - const [errorMessage, setErrorMessage] = useState(null); - - useEffect(() => { - if (!router.isReady) return; - const value = router.query.token; - if (typeof value === "string") { - setToken(value); - } else if (Array.isArray(value)) { - setToken(value[0] ?? ""); - } else { - setToken(""); - } - }, [router.isReady, router.query.token]); - - const handleUnsubscribe = async () => { - if (!token) { - setStatus("error"); - setErrorMessage( - t`Your unsubscribe link is missing a token. Please open the latest email and try again.`, - ); - return; - } - - setStatus("processing"); - setErrorMessage(null); - - try { - const response = await fetch("/api/unsubscribe", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ token }), - }); - - if (!response.ok) { - const payload = (await response.json().catch(() => null)) as { - error?: string; - } | null; - - throw new Error( - payload?.error ?? - "We couldn't update your preferences. Please try again.", - ); - } - - setStatus("success"); - } catch (error) { - setStatus("error"); - setErrorMessage( - t`We couldn't update your preferences. Please try again.`, - ); - } - }; - - const title = t`Unsubscribe`; - - return ( - <> - -
- -
-
-

- {t`Do you want to unsubscribe?`} -

-

- {t`Confirm your email preferences:`} -

-
- -
- -
- {status === "success" && ( -

- {t`You have been unsubscribed!`} -

- )} - {status === "error" && ( -

- {errorMessage} -

- )} -
-
- - ); -} diff --git a/cloud/docker-compose.yml b/cloud/docker-compose.yml index c09d2f4a..491cfb75 100644 --- a/cloud/docker-compose.yml +++ b/cloud/docker-compose.yml @@ -61,9 +61,6 @@ services: - SMTP_REJECT_UNAUTHORIZED=${SMTP_REJECT_UNAUTHORIZED} # Notifications - - NOVU_API_KEY=${NOVU_API_KEY} - - DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL} - - EMAIL_UNSUBSCRIBE_SECRET=${EMAIL_UNSUBSCRIBE_SECRET} - SUBSCRIBER_API_URL=${SUBSCRIBER_API_URL} - SUBSCRIBER_API_KEY=${SUBSCRIBER_API_KEY} - SUBSCRIBER_ENVIRONMENT_ID=${SUBSCRIBER_ENVIRONMENT_ID} diff --git a/packages/auth/src/hooks.test.ts b/packages/auth/src/hooks.test.ts index d3aae5d6..d0b765ce 100644 --- a/packages/auth/src/hooks.test.ts +++ b/packages/auth/src/hooks.test.ts @@ -1,4 +1,9 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { env } from "next-runtime-env"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import * as memberRepo from "@kan/db/repository/member.repo"; + +import { createDatabaseHooks } from "./hooks"; vi.mock("next-runtime-env", () => ({ env: vi.fn(), @@ -15,11 +20,11 @@ vi.mock("@kan/db/repository/user.repo", () => ({ })); vi.mock("@kan/email", () => ({ - notificationClient: null, + createSubscriber: vi.fn(), + triggerSubscriberWorkflow: vi.fn(), })); vi.mock("@kan/shared", () => ({ - createEmailUnsubscribeLink: vi.fn(), createS3Client: vi.fn(), })); @@ -27,17 +32,10 @@ vi.mock("@aws-sdk/client-s3", () => ({ PutObjectCommand: vi.fn(), })); -vi.mock("@novu/api/models/components", () => ({ - ChatOrPushProviderEnum: { Discord: "discord" }, -})); - -import { env } from "next-runtime-env"; -import * as memberRepo from "@kan/db/repository/member.repo"; -import { createDatabaseHooks } from "./hooks"; - const mockEnv = env as ReturnType; -const mockGetByEmailAndStatus = - memberRepo.getByEmailAndStatus as ReturnType; +const mockGetByEmailAndStatus = memberRepo.getByEmailAndStatus as ReturnType< + typeof vi.fn +>; const db = {} as Parameters[0]; diff --git a/packages/auth/src/hooks.ts b/packages/auth/src/hooks.ts index 80348c51..c1451bb2 100644 --- a/packages/auth/src/hooks.ts +++ b/packages/auth/src/hooks.ts @@ -1,14 +1,13 @@ import { PutObjectCommand } from "@aws-sdk/client-s3"; -import { ChatOrPushProviderEnum } from "@novu/api/models/components"; import { createAuthMiddleware } from "better-auth/api"; import { env } from "next-runtime-env"; import type { dbClient } from "@kan/db/client"; import * as memberRepo from "@kan/db/repository/member.repo"; import * as userRepo from "@kan/db/repository/user.repo"; -import { createSubscriber, notificationClient } from "@kan/email"; +import { createSubscriber, triggerSubscriberWorkflow } from "@kan/email"; import { createLogger } from "@kan/logger"; -import { createEmailUnsubscribeLink, createS3Client } from "@kan/shared"; +import { createS3Client } from "@kan/shared"; import { downloadImage } from "./utils"; @@ -94,80 +93,49 @@ export function createDatabaseHooks(db: dbClient) { } } - if (notificationClient) { - const [firstName, ...rest] = (user.name || "") - .split(" ") - .filter(Boolean); - const lastName = rest.length ? rest.join(" ") : undefined; + const [firstName, ...rest] = (user.name || "") + .split(" ") + .filter(Boolean); + const lastName = rest.length ? rest.join(" ") : undefined; - try { - const avatarUrl = avatarKey - ? `${env("NEXT_PUBLIC_STORAGE_URL")}/${env("NEXT_PUBLIC_AVATAR_BUCKET_NAME")}/${avatarKey}` - : undefined; + try { + const avatarUrl = avatarKey + ? `${env("NEXT_PUBLIC_STORAGE_URL")}/${env("NEXT_PUBLIC_AVATAR_BUCKET_NAME")}/${avatarKey}` + : undefined; - const unsubscribeUrl = await createEmailUnsubscribeLink(user.id); + await createSubscriber({ + publicId: user.id, + email: user.email, + externalId: user.id, + firstName, + lastName, + name: user.name, + attributes: { + avatarUrl, + emailVerified: user.emailVerified, + stripeCustomerId: user.stripeCustomerId, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }, + }); + } catch (error) { + log.error({ err: error }, "Error creating subscriber"); + } - log.info( - { - workflowId: "user-signup", - userId: user.id, - email: user.email, - }, - "Triggering Novu workflow", - ); - await notificationClient.trigger({ - to: { - subscriberId: user.id, - firstName: firstName, - lastName: lastName, - email: user.email, - avatar: avatarUrl, - data: { - emailVerified: user.emailVerified, - stripeCustomerId: user.stripeCustomerId, - createdAt: user.createdAt, - updatedAt: user.updatedAt, - }, - }, - payload: { - emailUnsubscribeUrl: unsubscribeUrl, - }, - workflowId: "user-signup", - }); - log.info( - { workflowId: "user-signup", userId: user.id }, - "Novu workflow triggered", - ); - - await notificationClient.subscribers.credentials.update( - { - providerId: ChatOrPushProviderEnum.Discord, - credentials: { - webhookUrl: env("DISCORD_WEBHOOK_URL"), - }, - integrationIdentifier: "discord", - }, - user.id, - ); - } catch (error) { - log.error( - { err: error }, - "Error adding user to notification client", - ); - } - - try { - await createSubscriber({ - publicId: user.id, - email: user.email, - externalId: user.id, - firstName, - lastName, - name: user.name, - }); - } catch (error) { - log.error({ err: error }, "Error creating subscriber"); - } + try { + log.info( + { workflowId: "user-signup", userId: user.id, email: user.email }, + "Triggering user-signup workflow", + ); + await triggerSubscriberWorkflow("user-signup", { + publicId: user.id, + }); + log.info( + { workflowId: "user-signup", userId: user.id }, + "user-signup workflow triggered", + ); + } catch (error) { + log.error({ err: error }, "Error triggering user-signup workflow"); } }, }, diff --git a/packages/auth/src/utils.ts b/packages/auth/src/utils.ts index 76b6af1f..b8980075 100644 --- a/packages/auth/src/utils.ts +++ b/packages/auth/src/utils.ts @@ -3,9 +3,8 @@ import type Stripe from "stripe"; import type { dbClient } from "@kan/db/client"; import * as userRepo from "@kan/db/repository/user.repo"; -import { notificationClient } from "@kan/email"; +import { triggerSubscriberWorkflow } from "@kan/email"; import { createLogger } from "@kan/logger"; -import { createEmailUnsubscribeLink } from "@kan/shared"; const log = createLogger("auth"); @@ -24,30 +23,22 @@ export async function triggerWorkflow( cancellationDetails?: Stripe.Subscription.CancellationDetails | null, ) { try { - if (!subscription.stripeCustomerId || !notificationClient) return; + if (!subscription.stripeCustomerId) return; const user = await userRepo.getByStripeCustomerId( db, subscription.stripeCustomerId, ); - if (!user || !notificationClient) return; + if (!user) return; - const unsubscribeUrl = await createEmailUnsubscribeLink(user.id); - - log.info({ workflowId, userId: user.id }, "Triggering Novu workflow"); - await notificationClient.trigger({ - to: { - subscriberId: user.id, - }, - payload: { - ...subscription, - cancellationDetails, - emailUnsubscribeUrl: unsubscribeUrl, - }, + log.info({ workflowId, userId: user.id }, "Triggering workflow"); + await triggerSubscriberWorkflow( workflowId, - }); - log.info({ workflowId, userId: user.id }, "Novu workflow triggered"); + { publicId: user.id }, + { ...subscription, cancellationDetails }, + ); + log.info({ workflowId, userId: user.id }, "Workflow triggered"); } catch (error) { log.error({ err: error, workflowId }, "Error triggering workflow"); } diff --git a/packages/email/package.json b/packages/email/package.json index ed3736d4..c90e7a6b 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -24,7 +24,6 @@ }, "dependencies": { "@kan/logger": "workspace:^", - "@novu/api": "^3.11.0", "@react-email/components": "^1.0.1", "nodemailer": "^7.0.3", "react-email": "^5.0.6" diff --git a/packages/email/src/index.tsx b/packages/email/src/index.tsx index e3397a33..bf5dbbc7 100644 --- a/packages/email/src/index.tsx +++ b/packages/email/src/index.tsx @@ -1,8 +1,8 @@ export const name = "email"; export { sendEmail } from "./sendEmail"; -export { notificationClient } from "./notificationClient"; export { createSubscriber, updateSubscriberPreferences, + triggerSubscriberWorkflow, } from "./subscriberClient"; diff --git a/packages/email/src/notificationClient.tsx b/packages/email/src/notificationClient.tsx deleted file mode 100644 index a637b83d..00000000 --- a/packages/email/src/notificationClient.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { Novu } from "@novu/api"; - -export const notificationClient = - process.env.NEXT_PUBLIC_KAN_ENV === "cloud" && process.env.NOVU_API_KEY - ? new Novu({ secretKey: process.env.NOVU_API_KEY }) - : null; diff --git a/packages/email/src/subscriberClient.ts b/packages/email/src/subscriberClient.ts index 3ef4d8b3..a6e95d5a 100644 --- a/packages/email/src/subscriberClient.ts +++ b/packages/email/src/subscriberClient.ts @@ -14,6 +14,46 @@ export const subscriberClient = } : null; +async function subscriberRequest( + method: string, + path: string, + body: unknown, + errorMessage: string, +) { + if (!subscriberClient) return; + + const url = `${subscriberClient.apiUrl}${path}`; + + log.debug({ method, url, body }, "subscriber.dev request"); + + try { + const response = await fetch(url, { + method, + headers: { + "Content-Type": "application/json", + "X-API-Key": subscriberClient.apiKey, + }, + body: JSON.stringify(body), + }); + + const responseBody = await response.text().catch(() => undefined); + + log.debug( + { method, url, status: response.status, body: responseBody }, + "subscriber.dev response", + ); + + if (!response.ok) { + log.error( + { status: response.status, body: responseBody }, + errorMessage, + ); + } + } catch (error) { + log.error({ err: error }, errorMessage); + } +} + interface CreateSubscriberInput { publicId: string; email: string; @@ -21,36 +61,18 @@ interface CreateSubscriberInput { firstName?: string; lastName?: string; name?: string; + attributes?: Record; } export async function createSubscriber(input: CreateSubscriberInput) { if (!subscriberClient) return; - try { - const response = await fetch( - `${subscriberClient.apiUrl}/environments/${subscriberClient.environmentId}/subscribers`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-Key": subscriberClient.apiKey, - }, - body: JSON.stringify(input), - }, - ); - - if (!response.ok) { - log.error( - { - status: response.status, - body: await response.text().catch(() => undefined), - }, - "Failed to create subscriber.dev subscriber", - ); - } - } catch (error) { - log.error({ err: error }, "Error creating subscriber.dev subscriber"); - } + await subscriberRequest( + "POST", + `/environments/${subscriberClient.environmentId}/subscribers`, + input, + "Failed to create subscriber.dev subscriber", + ); } interface UpdateSubscriberPreferencesInput { @@ -63,29 +85,31 @@ export async function updateSubscriberPreferences( ) { if (!subscriberClient) return; - try { - const response = await fetch( - `${subscriberClient.apiUrl}/environments/${subscriberClient.environmentId}/subscribers/${subscriberId}/preferences`, - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - "X-API-Key": subscriberClient.apiKey, - }, - body: JSON.stringify(input), - }, - ); - - if (!response.ok) { - log.error( - { - status: response.status, - body: await response.text().catch(() => undefined), - }, - "Failed to update subscriber preferences", - ); - } - } catch (error) { - log.error({ err: error }, "Error updating subscriber.dev preferences"); - } + await subscriberRequest( + "PATCH", + `/environments/${subscriberClient.environmentId}/subscribers/${subscriberId}/preferences`, + input, + "Failed to update subscriber preferences", + ); +} + +interface TriggerWorkflowSubscriberInput { + publicId?: string; + externalId?: string; + email?: string; +} + +export async function triggerSubscriberWorkflow( + key: string, + subscriber: TriggerWorkflowSubscriberInput, + payload?: Record, +) { + if (!subscriberClient) return; + + await subscriberRequest( + "POST", + `/environments/${subscriberClient.environmentId}/workflows/trigger`, + { key, subscriber, payload }, + "Failed to trigger subscriber.dev workflow", + ); } diff --git a/packages/shared/src/utils/email.ts b/packages/shared/src/utils/email.ts deleted file mode 100644 index 45def969..00000000 --- a/packages/shared/src/utils/email.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { SignJWT } from "jose"; -import { env } from "next-runtime-env"; - -const encoder = new TextEncoder(); - -/** - * Creates a long‑lived unsubscribe link for a given user/subscriber. - * - * `${NEXT_PUBLIC_BASE_URL}/unsubscribe?token=` - * - * The JWT payload only contains the subscriberId. There is no expiry - * on purpose – unsubscribe links should remain valid indefinitely. - * - */ -export async function createEmailUnsubscribeLink( - userId: string, -): Promise { - const baseUrl = env("NEXT_PUBLIC_BASE_URL"); - const secret = process.env.EMAIL_UNSUBSCRIBE_SECRET; - - if (!baseUrl || !secret) { - // Environment not configured for unsubscribe links. - return null; - } - - const token = await new SignJWT({ subscriberId: userId }) - .setProtectedHeader({ alg: "HS256" }) - // No expiration on purpose; unsubscribe links are long‑lived. - .sign(encoder.encode(secret)); - - return `${baseUrl}/unsubscribe?token=${encodeURIComponent(token)}`; -} diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index 63860da0..c6393365 100644 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -2,7 +2,6 @@ export * from "./generateUID"; export * from "./generateSlug"; export * from "./generateWorkspacePrefix"; export * from "./subscriptions"; -export * from "./email"; export * from "./dueDateFilters"; export * from "./s3"; export * from "./mentions"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee571bf4..e5aa254a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,9 +124,6 @@ importers: '@lingui/react': specifier: ^5.3.2 version: 5.4.1(@lingui/babel-plugin-lingui-macro@5.4.1(typescript@5.9.2))(react@18.3.1) - '@novu/api': - specifier: ^3.11.0 - version: 3.11.0 '@t3-oss/env-nextjs': specifier: ^0.11.1 version: 0.11.1(typescript@5.9.2)(zod@3.25.76) @@ -470,9 +467,6 @@ importers: '@kan/logger': specifier: workspace:^ version: link:../logger - '@novu/api': - specifier: ^3.11.0 - version: 3.11.0 '@react-email/components': specifier: ^1.0.1 version: 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1544,11 +1538,11 @@ packages: '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.is' + deprecated: 'Merged into tsx: https://tsx.hirok.io' '@esbuild-kit/esm-loader@2.6.5': resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.is' + deprecated: 'Merged into tsx: https://tsx.hirok.io' '@esbuild/aix-ppc64@0.19.12': resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} @@ -3001,9 +2995,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@novu/api@3.11.0': - resolution: {integrity: sha512-8u0mB5VThL7MhdxoN0UoA4CS9eu2k3Xa6iulauMhCHENuJNxUNuirJWq5t8jDoH4bFTQTfMN0VRkC0qodKR2qA==} - '@octokit/auth-token@3.0.4': resolution: {integrity: sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==} engines: {node: '>= 14'} @@ -11505,10 +11496,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@novu/api@3.11.0': - dependencies: - zod: 3.25.76 - '@octokit/auth-token@3.0.4': {} '@octokit/core@4.2.4': diff --git a/turbo.json b/turbo.json index 47f7d3be..b508dacc 100644 --- a/turbo.json +++ b/turbo.json @@ -123,7 +123,6 @@ "NEXT_PUBLIC_KAN_ENV", "NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY", "STRIPE_SECRET_KEY", - "DISCORD_WEBHOOK_URL", "STRIPE_WEBHOOK_SECRET", "STRIPE_WEBHOOK_SECRET_LEGACY", "STRIPE_PRO_PLAN_MONTHLY_PRICE_ID", @@ -148,8 +147,6 @@ "PORT", "BETTER_AUTH_SECRET", "BETTER_AUTH_TRUSTED_ORIGINS", - "NOVU_API_KEY", - "EMAIL_UNSUBSCRIBE_SECRET", "REDIS_URL", "LOG_LEVEL", "AXIOM_TOKEN",