Compare commits
8 Commits
fix/react-
...
feat/rate-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4353b9620b | ||
|
|
327089e10e | ||
|
|
c53c4b56c2 | ||
|
|
c94d08b35c | ||
|
|
c9dd45de39 | ||
|
|
718a018480 | ||
|
|
fae903f384 | ||
|
|
bf6a06b7c7 |
@@ -44,6 +44,10 @@ 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)
|
||||
|
||||
@@ -141,6 +141,7 @@ 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` |
|
||||
|
||||
@@ -78,6 +78,7 @@ 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(),
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,9 +2,17 @@ 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());
|
||||
|
||||
export default toNodeHandler(auth.handler);
|
||||
const authHandler = toNodeHandler(auth.handler);
|
||||
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
async (req, res) => {
|
||||
return await authHandler(req, res);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
@@ -44,4 +45,5 @@ export default async function handler(
|
||||
console.error("Error downloading attachment:", error);
|
||||
return res.status(500).json({ message: "Failed to download attachment" });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
@@ -20,4 +21,5 @@ export default async function handler(
|
||||
console.error("Error fetching OSS friends:", error);
|
||||
return res.status(500).json({ message: "Failed to fetch OSS friends" });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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 async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const stripe = createStripeClient();
|
||||
|
||||
if (req.method !== "POST") {
|
||||
@@ -31,4 +31,5 @@ export default async function handler(
|
||||
console.error("Error:", error);
|
||||
return res.status(500).json({ error: "Error creating portal session" });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ 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()
|
||||
@@ -21,10 +22,9 @@ interface CheckoutSessionRequest {
|
||||
stripeCustomerId: string;
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const stripe = createStripeClient();
|
||||
|
||||
if (req.method !== "POST") {
|
||||
@@ -115,4 +115,5 @@ export default async function handler(
|
||||
console.error("Error:", error);
|
||||
return res.status(500).json({ error: "Error creating checkout session" });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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 async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
@@ -48,4 +48,5 @@ export default async function handler(
|
||||
console.error("Trello authentication error:", err);
|
||||
return res.status(400).json({ message: "Trello authentication failed" });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -3,12 +3,14 @@ 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:
|
||||
process.env.NODE_ENV === "development"
|
||||
env.NODE_ENV === "development"
|
||||
? ({ path, error }) => {
|
||||
console.error(
|
||||
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
|
||||
@@ -17,11 +19,16 @@ const nextApiHandler = createNextApiHandler({
|
||||
: undefined,
|
||||
});
|
||||
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(200);
|
||||
return res.end();
|
||||
}
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
return nextApiHandler(req, res);
|
||||
}
|
||||
const result = await nextApiHandler(req, res);
|
||||
return result;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ 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),
|
||||
@@ -19,10 +20,9 @@ type ResponseData =
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<ResponseData>,
|
||||
) {
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
async (req: NextApiRequest, res: NextApiResponse<ResponseData>) => {
|
||||
if (process.env.NEXT_PUBLIC_KAN_ENV !== "cloud") {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
@@ -101,4 +101,5 @@ export default async function handler(
|
||||
}
|
||||
|
||||
return res.status(200).json({ success: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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 async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
@@ -71,4 +71,5 @@ export default async function handler(
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,25 +6,26 @@ import { appRouter } from "@kan/api";
|
||||
import { createRESTContext } from "@kan/api/trpc";
|
||||
|
||||
import { env } from "~/env";
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
await cors(req, res);
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
async (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);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { openApiDocument } from "@kan/api/openapi";
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
|
||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||
res.status(200).send(openApiDocument);
|
||||
};
|
||||
|
||||
export default handler;
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
(req: NextApiRequest, res: NextApiResponse) => {
|
||||
res.status(200).send(openApiDocument);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ 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}
|
||||
|
||||
@@ -18,6 +18,9 @@ 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}
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"./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",
|
||||
@@ -43,6 +47,7 @@
|
||||
"@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:"
|
||||
|
||||
99
packages/api/src/utils/rateLimit.ts
Normal file
99
packages/api/src/utils/rateLimit.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"./repository/*": {
|
||||
"types": "./dist/repository/*.d.ts",
|
||||
"default": "./src/repository/*.ts"
|
||||
},
|
||||
"./redis": {
|
||||
"types": "./dist/redis.d.ts",
|
||||
"default": "./src/redis.ts"
|
||||
}
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
@@ -43,6 +47,7 @@
|
||||
"@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:"
|
||||
|
||||
31
packages/db/src/redis.ts
Normal file
31
packages/db/src/redis.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
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
75
pnpm-lock.yaml
generated
@@ -314,6 +314,9 @@ 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
|
||||
@@ -397,6 +400,9 @@ 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
|
||||
@@ -2568,6 +2574,9 @@ 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}
|
||||
@@ -4631,6 +4640,10 @@ 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'}
|
||||
@@ -4901,6 +4914,10 @@ 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'}
|
||||
@@ -5743,6 +5760,10 @@ 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'}
|
||||
@@ -6128,10 +6149,16 @@ 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==}
|
||||
|
||||
@@ -7226,6 +7253,9 @@ 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'}
|
||||
@@ -7332,6 +7362,14 @@ 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==}
|
||||
|
||||
@@ -7669,6 +7707,9 @@ 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'}
|
||||
@@ -10543,6 +10584,8 @@ snapshots:
|
||||
|
||||
'@inquirer/figures@1.0.13': {}
|
||||
|
||||
'@ioredis/commands@1.5.0': {}
|
||||
|
||||
'@isaacs/balanced-match@4.0.1': {}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
@@ -12893,6 +12936,8 @@ snapshots:
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
cluster-key-slot@1.1.2: {}
|
||||
|
||||
co-body@6.2.0:
|
||||
dependencies:
|
||||
'@hapi/bourne': 3.0.0
|
||||
@@ -13143,6 +13188,8 @@ snapshots:
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
denque@2.1.0: {}
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
deprecation@2.3.1: {}
|
||||
@@ -14292,6 +14339,20 @@ 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: {}
|
||||
@@ -14629,8 +14690,12 @@ 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: {}
|
||||
@@ -16126,6 +16191,8 @@ snapshots:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
rate-limiter-flexible@9.0.1: {}
|
||||
|
||||
raw-body@2.5.2:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@@ -16273,6 +16340,12 @@ 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
|
||||
@@ -16770,6 +16843,8 @@ snapshots:
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
standard-as-callback@2.1.0: {}
|
||||
|
||||
statuses@2.0.1: {}
|
||||
|
||||
std-env@3.10.0: {}
|
||||
|
||||
@@ -126,7 +126,8 @@
|
||||
"BETTER_AUTH_SECRET",
|
||||
"BETTER_AUTH_TRUSTED_ORIGINS",
|
||||
"NOVU_API_KEY",
|
||||
"EMAIL_UNSUBSCRIBE_SECRET"
|
||||
"EMAIL_UNSUBSCRIBE_SECRET",
|
||||
"REDIS_URL"
|
||||
],
|
||||
"globalPassThroughEnv": [
|
||||
"NODE_ENV",
|
||||
|
||||
Reference in New Issue
Block a user