Lets try ioredis

This commit is contained in:
Atridad Lahiji 2023-11-21 09:05:09 -07:00
parent 4341c2e7cc
commit 5e57e2b416
No known key found for this signature in database
9 changed files with 90 additions and 103 deletions

View file

@ -3,11 +3,8 @@ DATABASE_URL=""
DATABASE_AUTH_TOKEN="" DATABASE_AUTH_TOKEN=""
# Redis # Redis
UPSTASH_REDIS_REST_URL="" REDIS_URL=""
UPSTASH_REDIS_REST_TOKEN="" REDIS_EXPIRY_SECONDS=""
UPSTASH_REDIS_EXPIRY_SECONDS=""
UPSTASH_RATELIMIT_REQUESTS=""
UPSTASH_RATELIMIT_SECONDS=""
#Auth #Auth
NEXT_PUBLIC_CLERK_SIGN_UP_URL="/sign-up" NEXT_PUBLIC_CLERK_SIGN_UP_URL="/sign-up"

View file

@ -48,10 +48,7 @@ const Navbar = ({ title }: NavbarProps) => {
width={32} width={32}
height={32} height={32}
/> />
<span className="hidden md:inline-flex"> <span className="hidden md:inline-flex">{title}</span>
{title}
{env.NEXT_PUBLIC_APP_ENV === "development" && " >> Staging"}
</span>
</Link> </Link>
</div> </div>

View file

@ -1,13 +1,12 @@
import { Redis } from "@upstash/redis"; import { Redis } from "ioredis";
import { env } from "env.mjs"; import { env } from "env.mjs";
export const redis = Redis.fromEnv(); export const redis = env.REDIS_URL ? new Redis(env.REDIS_URL) : null;
export const setCache = async <T>(key: string, value: T) => { export const setCache = async <T>(key: string, value: T) => {
console.log(env.REDIS_URL);
try { try {
await redis.set(`${env.APP_ENV}_${key}`, value, { await redis?.set(`${env.APP_ENV}_${key}`, JSON.stringify(value));
ex: Number(env.UPSTASH_REDIS_EXPIRY_SECONDS),
});
return true; return true;
} catch { } catch {
return false; return false;
@ -16,8 +15,8 @@ export const setCache = async <T>(key: string, value: T) => {
export const fetchCache = async <T>(key: string) => { export const fetchCache = async <T>(key: string) => {
try { try {
const result = await redis.get(`${env.APP_ENV}_${key}`); const result = (await redis?.get(`${env.APP_ENV}_${key}`)) as string;
return result as T; return JSON.parse(result) as T;
} catch { } catch {
return null; return null;
} }
@ -25,7 +24,7 @@ export const fetchCache = async <T>(key: string) => {
export const invalidateCache = async (key: string) => { export const invalidateCache = async (key: string) => {
try { try {
await redis.del(`${env.APP_ENV}_${key}`); await redis?.del(`${env.APP_ENV}_${key}`);
return true; return true;
} catch { } catch {
return false; return false;

View file

@ -8,6 +8,7 @@ import { EventTypes } from "@/_utils/types";
import { getAuth } from "@clerk/nextjs/server"; import { getAuth } from "@clerk/nextjs/server";
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { env } from "env.mjs";
export async function GET( export async function GET(
request: Request, request: Request,
@ -61,7 +62,8 @@ export async function DELETE(
const success = deletedRoom.length > 0; const success = deletedRoom.length > 0;
if (success) { if (success) {
await invalidateCache(`kv_roomlist_${userId}`); if (env.APP_ENV === "production")
await invalidateCache(`kv_roomlist_${userId}`);
await publishToMultipleChannels( await publishToMultipleChannels(
[`${userId}`, `${params.roomId}`], [`${userId}`, `${params.roomId}`],

View file

@ -8,17 +8,21 @@ import { createId } from "@paralleldrive/cuid2";
import { publishToChannel } from "@/_lib/ably"; import { publishToChannel } from "@/_lib/ably";
import { EventTypes } from "@/_utils/types"; import { EventTypes } from "@/_utils/types";
import { getAuth } from "@clerk/nextjs/server"; import { getAuth } from "@clerk/nextjs/server";
import { env } from "env.mjs";
export async function GET(request: Request) { export async function GET(request: Request) {
const { userId } = getAuth(request as NextRequest); const { userId } = getAuth(request as NextRequest);
const cachedResult = await fetchCache< const cachedResult =
{ env.APP_ENV === "production"
id: string; ? await fetchCache<
createdAt: Date; {
roomName: string; id: string;
}[] createdAt: Date;
>(`kv_roomlist_${userId}`); roomName: string;
}[]
>(`kv_roomlist_${userId}`)
: null;
if (cachedResult) { if (cachedResult) {
return NextResponse.json(cachedResult, { return NextResponse.json(cachedResult, {
@ -30,7 +34,8 @@ export async function GET(request: Request) {
where: eq(rooms.userId, userId || ""), where: eq(rooms.userId, userId || ""),
}); });
await setCache(`kv_roomlist_${userId}`, roomList); if (env.APP_ENV === "production")
await setCache(`kv_roomlist_${userId}`, roomList);
return NextResponse.json(roomList, { return NextResponse.json(roomList, {
status: 200, status: 200,
@ -60,7 +65,8 @@ export async function POST(request: Request) {
const success = room.length > 0; const success = room.length > 0;
if (room) { if (room) {
await invalidateCache(`kv_roomlist_${userId}`); if (env.APP_ENV === "production")
await invalidateCache(`kv_roomlist_${userId}`);
await publishToChannel( await publishToChannel(
`${userId}`, `${userId}`,

View file

@ -5,11 +5,8 @@ export const env = createEnv({
server: { server: {
DATABASE_URL: z.string().url(), DATABASE_URL: z.string().url(),
DATABASE_AUTH_TOKEN: z.string(), DATABASE_AUTH_TOKEN: z.string(),
UPSTASH_REDIS_REST_URL: z.string().url(), REDIS_URL: z.string().url().optional(),
UPSTASH_REDIS_REST_TOKEN: z.string(), REDIS_EXPIRY_SECONDS: z.string().optional(),
UPSTASH_REDIS_EXPIRY_SECONDS: z.string(),
UPSTASH_RATELIMIT_REQUESTS: z.string(),
UPSTASH_RATELIMIT_SECONDS: z.string(),
ABLY_API_KEY: z.string(), ABLY_API_KEY: z.string(),
APP_ENV: z.string(), APP_ENV: z.string(),
UNKEY_ROOT_KEY: z.string(), UNKEY_ROOT_KEY: z.string(),

View file

@ -1,21 +1,9 @@
import { authMiddleware, redirectToSignIn } from "@clerk/nextjs"; import { authMiddleware, redirectToSignIn } from "@clerk/nextjs";
import { validateRequest } from "./app/_lib/unkey"; import { validateRequest } from "./app/_lib/unkey";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
import { env } from "./env.mjs";
const shitList = ["ama.ab.ca"]; const shitList = ["ama.ab.ca"];
const rateLimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(
Number(env.UPSTASH_RATELIMIT_REQUESTS),
`${Number(env.UPSTASH_RATELIMIT_SECONDS)}s`
),
analytics: true,
});
export default authMiddleware({ export default authMiddleware({
ignoredRoutes: ["/"], ignoredRoutes: ["/"],
publicRoutes: [ publicRoutes: [
@ -25,14 +13,7 @@ export default authMiddleware({
], ],
afterAuth: async (auth, req) => { afterAuth: async (auth, req) => {
if (!auth.userId && auth.isPublicRoute) { if (!auth.userId && auth.isPublicRoute) {
const { success } = await rateLimit.limit(req.ip || ""); return NextResponse.next();
if (success) {
return NextResponse.next();
}
return new NextResponse("TOO MANY REQUESTS", {
status: 429,
statusText: "Too many requests!",
});
} }
if (auth.userId) { if (auth.userId) {
@ -54,15 +35,6 @@ export default authMiddleware({
} }
if (req.nextUrl.pathname.includes("/api/internal")) { if (req.nextUrl.pathname.includes("/api/internal")) {
const { success } = await rateLimit.limit(req.ip || "");
if (!success) {
return new NextResponse("TOO MANY REQUESTS", {
status: 429,
statusText: "Too many requests!",
});
}
if (auth.userId) { if (auth.userId) {
return NextResponse.next(); return NextResponse.next();
} else { } else {
@ -74,15 +46,6 @@ export default authMiddleware({
} }
if (req.nextUrl.pathname.includes("/api/external/private")) { if (req.nextUrl.pathname.includes("/api/external/private")) {
const { success } = await rateLimit.limit(req.ip || "");
if (!success) {
return new NextResponse("TOO MANY REQUESTS", {
status: 429,
statusText: "Too many requests!",
});
}
const isValid = await validateRequest(req); const isValid = await validateRequest(req);
if (isValid) { if (isValid) {

View file

@ -21,14 +21,13 @@
"@t3-oss/env-nextjs": "0.7.1", "@t3-oss/env-nextjs": "0.7.1",
"@tanstack/react-query": "5.8.4", "@tanstack/react-query": "5.8.4",
"@unkey/api": "0.12.0", "@unkey/api": "0.12.0",
"@upstash/ratelimit": "0.4.4",
"@upstash/redis": "1.25.1",
"@vercel/analytics": "1.1.1", "@vercel/analytics": "1.1.1",
"ably": "1.2.47", "ably": "1.2.47",
"autoprefixer": "10.4.16", "autoprefixer": "10.4.16",
"csv42": "5.0.0", "csv42": "5.0.0",
"dotenv": "16.3.1", "dotenv": "16.3.1",
"drizzle-orm": "0.29.0", "drizzle-orm": "0.29.0",
"ioredis": "^5.3.2",
"next": "14.0.1", "next": "14.0.1",
"nextjs-cors": "2.1.2", "nextjs-cors": "2.1.2",
"postcss": "8.4.31", "postcss": "8.4.31",

89
pnpm-lock.yaml generated
View file

@ -26,12 +26,6 @@ dependencies:
'@unkey/api': '@unkey/api':
specifier: 0.12.0 specifier: 0.12.0
version: 0.12.0 version: 0.12.0
'@upstash/ratelimit':
specifier: 0.4.4
version: 0.4.4
'@upstash/redis':
specifier: 1.25.1
version: 1.25.1
'@vercel/analytics': '@vercel/analytics':
specifier: 1.1.1 specifier: 1.1.1
version: 1.1.1 version: 1.1.1
@ -50,6 +44,9 @@ dependencies:
drizzle-orm: drizzle-orm:
specifier: 0.29.0 specifier: 0.29.0
version: 0.29.0(@libsql/client@0.4.0-pre.1)(better-sqlite3@9.1.1) version: 0.29.0(@libsql/client@0.4.0-pre.1)(better-sqlite3@9.1.1)
ioredis:
specifier: ^5.3.2
version: 5.3.2
next: next:
specifier: 14.0.1 specifier: 14.0.1
version: 14.0.1(react-dom@18.2.0)(react@18.2.0) version: 14.0.1(react-dom@18.2.0)(react@18.2.0)
@ -800,6 +797,10 @@ packages:
resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==}
dev: true dev: true
/@ioredis/commands@1.2.0:
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
dev: false
/@jridgewell/gen-mapping@0.3.3: /@jridgewell/gen-mapping@0.3.3:
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
@ -1548,25 +1549,6 @@ packages:
resolution: {integrity: sha512-BT2I52rwTQ1j8S9aX9tKH3P3Efvtvgu8dD8pExYvKXG9uWPditNOwO6t8aeXLGCWdyzTqxmueBeR0uD0DN5U7A==} resolution: {integrity: sha512-BT2I52rwTQ1j8S9aX9tKH3P3Efvtvgu8dD8pExYvKXG9uWPditNOwO6t8aeXLGCWdyzTqxmueBeR0uD0DN5U7A==}
dev: false dev: false
/@upstash/core-analytics@0.0.6:
resolution: {integrity: sha512-cpPSR0XJAJs4Ddz9nq3tINlPS5aLfWVCqhhtHnXt4p7qr5+/Znlt1Es736poB/9rnl1hAHrOsOvVj46NEXcVqA==}
engines: {node: '>=16.0.0'}
dependencies:
'@upstash/redis': 1.25.1
dev: false
/@upstash/ratelimit@0.4.4:
resolution: {integrity: sha512-y3q6cNDdcRQ2MRPRf5UNWBN36IwnZ4kAEkGoH3i6OqdWwz4qlBxNsw4/Rpqn9h93+Nx1cqg5IOq7O2e2zMJY1w==}
dependencies:
'@upstash/core-analytics': 0.0.6
dev: false
/@upstash/redis@1.25.1:
resolution: {integrity: sha512-ACj0GhJ4qrQyBshwFgPod6XufVEfKX2wcaihsEvSdLYnY+m+pa13kGt1RXm/yTHKf4TQi/Dy2A8z/y6WUEOmlg==}
dependencies:
crypto-js: 4.2.0
dev: false
/@vercel/analytics@1.1.1: /@vercel/analytics@1.1.1:
resolution: {integrity: sha512-+NqgNmSabg3IFfxYhrWCfB/H+RCUOCR5ExRudNG2+pcRehq628DJB5e1u1xqwpLtn4pAYii4D98w7kofORAGQA==} resolution: {integrity: sha512-+NqgNmSabg3IFfxYhrWCfB/H+RCUOCR5ExRudNG2+pcRehq628DJB5e1u1xqwpLtn4pAYii4D98w7kofORAGQA==}
dependencies: dependencies:
@ -2031,6 +2013,11 @@ packages:
mimic-response: 1.0.1 mimic-response: 1.0.1
dev: false dev: false
/cluster-key-slot@1.1.2:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
dev: false
/color-convert@2.0.1: /color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@ -2097,10 +2084,6 @@ packages:
which: 2.0.2 which: 2.0.2
dev: true dev: true
/crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
dev: false
/css-selector-tokenizer@0.8.0: /css-selector-tokenizer@0.8.0:
resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==} resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==}
dependencies: dependencies:
@ -2184,7 +2167,6 @@ packages:
optional: true optional: true
dependencies: dependencies:
ms: 2.1.2 ms: 2.1.2
dev: true
/decompress-response@6.0.0: /decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
@ -2233,6 +2215,11 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
dev: false dev: false
/denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
dev: false
/dequal@2.0.3: /dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -3371,6 +3358,23 @@ packages:
side-channel: 1.0.4 side-channel: 1.0.4
dev: true dev: true
/ioredis@5.3.2:
resolution: {integrity: sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==}
engines: {node: '>=12.22.0'}
dependencies:
'@ioredis/commands': 1.2.0
cluster-key-slot: 1.1.2
debug: 4.3.4
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
dev: false
/is-array-buffer@3.0.2: /is-array-buffer@3.0.2:
resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==}
dependencies: dependencies:
@ -3692,6 +3696,14 @@ packages:
p-locate: 5.0.0 p-locate: 5.0.0
dev: true dev: true
/lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
dev: false
/lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
dev: false
/lodash.merge@4.6.2: /lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true dev: true
@ -3844,7 +3856,6 @@ packages:
/ms@2.1.2: /ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
/ms@2.1.3: /ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -4363,6 +4374,18 @@ packages:
picomatch: 2.3.1 picomatch: 2.3.1
dev: true dev: true
/redis-errors@1.2.0:
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
engines: {node: '>=4'}
dev: false
/redis-parser@3.0.0:
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
engines: {node: '>=4'}
dependencies:
redis-errors: 1.2.0
dev: false
/reflect.getprototypeof@1.0.4: /reflect.getprototypeof@1.0.4:
resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==} resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -4664,6 +4687,10 @@ packages:
get-source: 2.0.12 get-source: 2.0.12
dev: true dev: true
/standard-as-callback@2.1.0:
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
dev: false
/stoppable@1.1.0: /stoppable@1.1.0:
resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==}
engines: {node: '>=4', npm: '>=6'} engines: {node: '>=4', npm: '>=6'}