Compare commits

..

1 Commits

Author SHA1 Message Date
Henry
da66a7f5df feat(cloud): reset workspace slug on subscription cancelled 2026-01-25 20:01:10 +00:00
23 changed files with 85 additions and 322 deletions

View File

@@ -44,10 +44,6 @@ NEXT_API_BODY_SIZE_LIMIT= # e.g. 50mb (defaults to 1mb)
TRELLO_APP_API_KEY=
TRELLO_APP_SECRET=
# Redis (optional - for rate limiting)
# If not provided, rate limiting will use in-memory storage
REDIS_URL= # e.g. redis://default:your_password@your_host:6379
# OAuth providers (optional)
BETTER_AUTH_TRUSTED_ORIGINS=
# Optional: Restrict OIDC/Social sign-ins to specific email domains (comma-separated)

View File

@@ -141,7 +141,6 @@ pnpm dev
| Variable | Description | Required | Example |
| ----------------------------------------- | --------------------------------------------------------- | ------------------------------------- | ----------------------------------------------------------- |
| `POSTGRES_URL` | PostgreSQL connection URL | To use external database | `postgres://user:pass@localhost:5432/db` |
| `REDIS_URL` | Redis connection URL | For rate limiting (optional) | `redis://localhost:6379` or `redis://redis:6379` (Docker) |
| `EMAIL_FROM` | Sender email address | For Email | `"Kan <hello@mail.kan.bn>"` |
| `SMTP_HOST` | SMTP server hostname | For Email | `smtp.resend.com` |
| `SMTP_PORT` | SMTP server port | For Email | `465` |

View File

@@ -78,7 +78,6 @@ export const env = createEnv({
S3_ENDPOINT: z.string().optional(),
S3_FORCE_PATH_STYLE: z.string().optional(),
EMAIL_FROM: z.string().optional(),
REDIS_URL: z.string().url().optional(),
},
/**

View File

@@ -2,17 +2,9 @@ import { toNodeHandler } from "better-auth/node";
import { initAuth } from "@kan/auth/server";
import { createDrizzleClient } from "@kan/db/client";
import { withRateLimit } from "@kan/api/utils/rateLimit";
export const config = { api: { bodyParser: false } };
export const auth = initAuth(createDrizzleClient());
const authHandler = toNodeHandler(auth.handler);
export default withRateLimit(
{ points: 100, duration: 60 },
async (req, res) => {
return await authHandler(req, res);
},
);
export default toNodeHandler(auth.handler);

View File

@@ -1,10 +1,9 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { withRateLimit } from "@kan/api/utils/rateLimit";
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "GET") {
return res.status(405).json({ message: "Method not allowed" });
}
@@ -45,5 +44,4 @@ export default withRateLimit(
console.error("Error downloading attachment:", error);
return res.status(500).json({ message: "Failed to download attachment" });
}
},
);
}

View File

@@ -1,10 +1,9 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { withRateLimit } from "@kan/api/utils/rateLimit";
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "GET") {
return res.status(405).json({ message: "Method not allowed" });
}
@@ -21,5 +20,4 @@ export default withRateLimit(
console.error("Error fetching OSS friends:", error);
return res.status(500).json({ message: "Failed to fetch OSS friends" });
}
},
);
}

View File

@@ -3,11 +3,11 @@ import { env } from "next-runtime-env";
import { createNextApiContext } from "@kan/api/trpc";
import { createStripeClient } from "@kan/stripe";
import { withRateLimit } from "@kan/api/utils/rateLimit";
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const stripe = createStripeClient();
if (req.method !== "POST") {
@@ -31,5 +31,4 @@ export default withRateLimit(
console.error("Error:", error);
return res.status(500).json({ error: "Error creating portal session" });
}
},
);
}

View File

@@ -6,7 +6,6 @@ import { createNextApiContext } from "@kan/api/trpc";
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { createStripeClient } from "@kan/stripe";
import { withRateLimit } from "@kan/api/utils/rateLimit";
const workspaceSlugSchema = z
.string()
@@ -22,9 +21,10 @@ interface CheckoutSessionRequest {
stripeCustomerId: string;
}
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const stripe = createStripeClient();
if (req.method !== "POST") {
@@ -115,5 +115,4 @@ export default withRateLimit(
console.error("Error:", error);
return res.status(500).json({ error: "Error creating checkout session" });
}
},
);
}

View File

@@ -3,11 +3,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { createNextApiContext } from "@kan/api/trpc";
import { integrations } from "@kan/db/schema";
import { addYears } from "date-fns";
import { withRateLimit } from "@kan/api/utils/rateLimit";
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
@@ -48,5 +48,4 @@ export default withRateLimit(
console.error("Trello authentication error:", err);
return res.status(400).json({ message: "Trello authentication failed" });
}
},
);
}

View File

@@ -3,14 +3,12 @@ import { createNextApiHandler } from "@trpc/server/adapters/next";
import { appRouter } from "@kan/api/root";
import { createTRPCContext } from "@kan/api/trpc";
import { env } from "~/env";
import { withRateLimit } from "@kan/api/utils/rateLimit";
const nextApiHandler = createNextApiHandler({
router: appRouter,
createContext: createTRPCContext,
onError:
env.NODE_ENV === "development"
process.env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
@@ -19,16 +17,11 @@ const nextApiHandler = createNextApiHandler({
: undefined,
});
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "OPTIONS") {
res.writeHead(200);
res.end();
return;
}
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "OPTIONS") {
res.writeHead(200);
return res.end();
}
const result = await nextApiHandler(req, res);
return result;
},
);
return nextApiHandler(req, res);
}

View File

@@ -4,7 +4,6 @@ import { jwtVerify } from "jose";
import { z } from "zod";
import { env } from "~/env";
import { withRateLimit } from "@kan/api/utils/rateLimit";
const requestSchema = z.object({
token: z.string().min(1),
@@ -20,9 +19,10 @@ type ResponseData =
const textEncoder = new TextEncoder();
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse<ResponseData>) => {
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>,
) {
if (process.env.NEXT_PUBLIC_KAN_ENV !== "cloud") {
return res.status(404).json({
success: false,
@@ -101,5 +101,4 @@ export default withRateLimit(
}
return res.status(200).json({ success: true });
},
);
}

View File

@@ -6,13 +6,13 @@ import { env as nextRuntimeEnv } from "next-runtime-env";
import { createNextApiContext } from "@kan/api/trpc";
import { env } from "~/env";
import { withRateLimit } from "@kan/api/utils/rateLimit";
const allowedContentTypes = ["image/jpeg", "image/png"];
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
@@ -71,5 +71,4 @@ export default withRateLimit(
} catch (error) {
return res.status(500).json({ error: (error as Error).message });
}
},
);
}

View File

@@ -6,26 +6,25 @@ import { appRouter } from "@kan/api";
import { createRESTContext } from "@kan/api/trpc";
import { env } from "~/env";
import { withRateLimit } from "@kan/api/utils/rateLimit";
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
await cors(req, res);
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
await cors(req, res);
const openApiHandler = createOpenApiNextHandler({
router: appRouter,
createContext: createRESTContext,
onError:
env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ REST failed on ${path ?? "<no-path>"}: ${error.message}`,
);
}
: undefined,
});
const openApiHandler = createOpenApiNextHandler({
router: appRouter,
createContext: createRESTContext,
onError:
env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ REST failed on ${path ?? "<no-path>"}: ${error.message}`,
);
}
: undefined,
});
return await openApiHandler(req, res);
},
);
return await openApiHandler(req, res);
}

View File

@@ -1,11 +1,9 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { openApiDocument } from "@kan/api/openapi";
import { withRateLimit } from "@kan/api/utils/rateLimit";
export default withRateLimit(
{ points: 100, duration: 60 },
(req: NextApiRequest, res: NextApiResponse) => {
res.status(200).send(openApiDocument);
},
);
const handler = (req: NextApiRequest, res: NextApiResponse) => {
res.status(200).send(openApiDocument);
};
export default handler;

View File

@@ -21,7 +21,6 @@ services:
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- POSTGRES_URL=${POSTGRES_URL}
- NEXT_PUBLIC_USE_STANDALONE_OUTPUT=${NEXT_PUBLIC_USE_STANDALONE_OUTPUT}
- REDIS_URL=${REDIS_URL}
# Stripe
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}

View File

@@ -18,9 +18,6 @@ services:
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- POSTGRES_URL=${POSTGRES_URL}
# Redis (optional - for rate limiting)
- REDIS_URL=${REDIS_URL}
# Admin API key (optional)
- KAN_ADMIN_API_KEY=${KAN_ADMIN_API_KEY}

View File

@@ -23,10 +23,6 @@
"./openapi": {
"types": "./dist/openapi.d.ts",
"default": "./src/openapi.ts"
},
"./utils/rateLimit": {
"types": "./dist/utils/rateLimit.d.ts",
"default": "./src/utils/rateLimit.ts"
}
},
"license": "GPL-3.0",
@@ -47,7 +43,6 @@
"@kan/shared": "workspace:^",
"@kan/stripe": "workspace:^",
"@trpc/server": "catalog:",
"rate-limiter-flexible": "^9.0.1",
"superjson": "2.2.1",
"trpc-to-openapi": "^2.3.2",
"zod": "catalog:"

View File

@@ -1,99 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import {
RateLimiterRedis,
RateLimiterMemory,
} from "rate-limiter-flexible";
import { getRedisClient } from "@kan/db/redis";
export interface RateLimitOptions {
points?: number;
duration?: number;
identifier?: (req: NextApiRequest) => string | Promise<string>;
errorMessage?: string;
}
const defaultIdentifier = (req: NextApiRequest): string => {
// Try to identify the IP address of the request
const forwardedFor = req.headers["x-forwarded-for"];
const realIp = req.headers["x-real-ip"];
const cfConnectingIp = req.headers["cf-connecting-ip"];
const ip =
(typeof forwardedFor === "string"
? forwardedFor.split(",")[0]?.trim()
: null) ??
(typeof realIp === "string" ? realIp : null) ??
(typeof cfConnectingIp === "string" ? cfConnectingIp : null) ??
req.socket.remoteAddress ??
"unknown";
return ip;
};
const DEFAULT_OPTIONS = {
points: 100,
duration: 60,
errorMessage: "Too many requests, please try again later.",
identifier: defaultIdentifier,
} as const;
function createRateLimiter(options: RateLimitOptions = {}) {
const redis = getRedisClient();
const points = options.points ?? DEFAULT_OPTIONS.points;
const duration = options.duration ?? DEFAULT_OPTIONS.duration;
// Use Redis if available, otherwise fall back to in-memory storage
if (redis) {
console.log("Using Redis for rate limiting");
return new RateLimiterRedis({
storeClient: redis,
points,
duration,
});
}
console.log("Using in-memory for rate limiting");
return new RateLimiterMemory({
points,
duration,
});
}
export function withRateLimit(
options: RateLimitOptions,
handler: (
req: NextApiRequest,
res: NextApiResponse,
) => Promise<unknown> | unknown,
) {
const rateLimiter = createRateLimiter(options);
const identifier = options.identifier ?? DEFAULT_OPTIONS.identifier;
const errorMessage = options.errorMessage ?? DEFAULT_OPTIONS.errorMessage;
return async (req: NextApiRequest, res: NextApiResponse) => {
try {
const id = await identifier(req);
const key = `ratelimit_${id}`;
await rateLimiter.consume(key);
return await handler(req, res);
} catch (error) {
// rate-limiter-flexible throws an error with msBeforeNext or remainingPoints
// when limit is exceeded. Check for these properties directly.
if (
error &&
typeof error === "object" &&
("msBeforeNext" in error || "remainingPoints" in error)
) {
return res.status(429).json({
message: errorMessage,
});
}
return await handler(req, res);
}
};
}

View File

@@ -6,6 +6,7 @@ import type { dbClient } from "@kan/db/client";
import * as memberRepo from "@kan/db/repository/member.repo";
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { generateUID } from "@kan/shared/utils";
import { sendEmail } from "@kan/email";
import { createStripeClient } from "@kan/stripe";
@@ -132,8 +133,23 @@ export function createPlugins(db: dbClient) {
if (workspace?.id) {
await memberRepo.pauseAllMembers(db, workspace.id);
// Reset slug to publicId, or generate a UID if publicId is taken
let newSlug = workspace.publicId;
if (workspace.slug !== workspace.publicId) {
const isPublicIdAvailable = await workspaceRepo.isWorkspaceSlugAvailable(
db,
workspace.publicId,
);
if (!isPublicIdAvailable) {
newSlug = generateUID();
}
}
await workspaceRepo.update(db, subscription.referenceId, {
plan: "free",
slug: newSlug,
});
}
},

View File

@@ -23,10 +23,6 @@
"./repository/*": {
"types": "./dist/repository/*.d.ts",
"default": "./src/repository/*.ts"
},
"./redis": {
"types": "./dist/redis.d.ts",
"default": "./src/redis.ts"
}
},
"license": "GPL-3.0",
@@ -47,7 +43,6 @@
"@kan/shared": "workspace:^",
"drizzle-orm": "^0.42.0",
"drizzle-zod": "^0.5.1",
"ioredis": "^5.9.2",
"pg": "^8.11.3",
"uuid": "^11.1.0",
"zod": "catalog:"

View File

@@ -1,31 +0,0 @@
import Redis from "ioredis";
let redisClient: Redis | null = null;
export function getRedisClient(): Redis | null {
if (redisClient) {
return redisClient;
}
const redisUrl = process.env.REDIS_URL;
if (!redisUrl) {
return null;
}
redisClient = new Redis(redisUrl, {
maxRetriesPerRequest: 3,
enableReadyCheck: true,
lazyConnect: true,
});
return redisClient;
}
export async function closeRedisClient(): Promise<void> {
if (redisClient) {
await redisClient.quit();
redisClient = null;
}
}

75
pnpm-lock.yaml generated
View File

@@ -314,9 +314,6 @@ importers:
'@trpc/server':
specifier: 'catalog:'
version: 11.5.0(typescript@5.9.2)
rate-limiter-flexible:
specifier: ^9.0.1
version: 9.0.1
superjson:
specifier: 2.2.1
version: 2.2.1
@@ -400,9 +397,6 @@ importers:
drizzle-zod:
specifier: ^0.5.1
version: 0.5.1(drizzle-orm@0.42.0(@electric-sql/pglite@0.3.7)(@types/pg@8.15.5)(kysely@0.28.8)(pg@8.16.3))(zod@3.25.76)
ioredis:
specifier: ^5.9.2
version: 5.9.2
pg:
specifier: ^8.11.3
version: 8.16.3
@@ -2574,9 +2568,6 @@ packages:
resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==}
engines: {node: '>=18'}
'@ioredis/commands@1.5.0':
resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==}
'@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22}
@@ -4640,10 +4631,6 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
cluster-key-slot@1.1.2:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
co-body@6.2.0:
resolution: {integrity: sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==}
engines: {node: '>=8.0.0'}
@@ -4914,10 +4901,6 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -5760,10 +5743,6 @@ packages:
resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
engines: {node: '>= 0.10'}
ioredis@5.9.2:
resolution: {integrity: sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==}
engines: {node: '>=12.22.0'}
ip-address@10.0.1:
resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==}
engines: {node: '>= 12'}
@@ -6149,16 +6128,10 @@ packages:
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
lodash.get@4.4.2:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
deprecated: This package is deprecated. Use the optional chaining (?.) operator instead.
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
@@ -7253,9 +7226,6 @@ packages:
randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
rate-limiter-flexible@9.0.1:
resolution: {integrity: sha512-sO+QdoGPCxroi4VkO2FIVjfUGuexhRkBc9ROHqu5eVEEz+oPHzQqvCc25ajFfMUBosbNGb6qpNa8xmxH9YNZsg==}
raw-body@2.5.2:
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
engines: {node: '>= 0.8'}
@@ -7362,14 +7332,6 @@ packages:
resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==}
engines: {node: '>= 0.10'}
redis-errors@1.2.0:
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
engines: {node: '>=4'}
redis-parser@3.0.0:
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
engines: {node: '>=4'}
redux@4.2.1:
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
@@ -7707,9 +7669,6 @@ packages:
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
standard-as-callback@2.1.0:
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
@@ -10584,8 +10543,6 @@ snapshots:
'@inquirer/figures@1.0.13': {}
'@ioredis/commands@1.5.0': {}
'@isaacs/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0':
@@ -12936,8 +12893,6 @@ snapshots:
clsx@2.1.1: {}
cluster-key-slot@1.1.2: {}
co-body@6.2.0:
dependencies:
'@hapi/bourne': 3.0.0
@@ -13188,8 +13143,6 @@ snapshots:
delayed-stream@1.0.0: {}
denque@2.1.0: {}
depd@2.0.0: {}
deprecation@2.3.1: {}
@@ -14339,20 +14292,6 @@ snapshots:
interpret@1.4.0: {}
ioredis@5.9.2:
dependencies:
'@ioredis/commands': 1.5.0
cluster-key-slot: 1.1.2
debug: 4.4.3
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0
redis-errors: 1.2.0
redis-parser: 3.0.0
standard-as-callback: 2.1.0
transitivePeerDependencies:
- supports-color
ip-address@10.0.1: {}
iron-webcrypto@1.2.1: {}
@@ -14690,12 +14629,8 @@ snapshots:
lodash.debounce@4.0.8: {}
lodash.defaults@4.2.0: {}
lodash.get@4.4.2: {}
lodash.isarguments@3.1.0: {}
lodash.isplainobject@4.0.6: {}
lodash.merge@4.6.2: {}
@@ -16191,8 +16126,6 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
rate-limiter-flexible@9.0.1: {}
raw-body@2.5.2:
dependencies:
bytes: 3.1.2
@@ -16340,12 +16273,6 @@ snapshots:
dependencies:
resolve: 1.22.10
redis-errors@1.2.0: {}
redis-parser@3.0.0:
dependencies:
redis-errors: 1.2.0
redux@4.2.1:
dependencies:
'@babel/runtime': 7.28.3
@@ -16843,8 +16770,6 @@ snapshots:
stackback@0.0.2: {}
standard-as-callback@2.1.0: {}
statuses@2.0.1: {}
std-env@3.10.0: {}

View File

@@ -126,8 +126,7 @@
"BETTER_AUTH_SECRET",
"BETTER_AUTH_TRUSTED_ORIGINS",
"NOVU_API_KEY",
"EMAIL_UNSUBSCRIBE_SECRET",
"REDIS_URL"
"EMAIL_UNSUBSCRIBE_SECRET"
],
"globalPassThroughEnv": [
"NODE_ENV",