From a4346ac63322f256ee3f4bfb999735ce2867a08b Mon Sep 17 00:00:00 2001 From: Muhammadcodes112 Date: Sat, 11 Jul 2026 02:12:58 -0700 Subject: [PATCH 1/2] feat: validate WhatsApp webhook payload shape with Zod schema (closes #8) --- apps/backend/src/routes/webhook.ts | 46 +++++++++++-- apps/backend/test/webhook.test.ts | 100 +++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 apps/backend/test/webhook.test.ts diff --git a/apps/backend/src/routes/webhook.ts b/apps/backend/src/routes/webhook.ts index dc43ecd..cd9d067 100644 --- a/apps/backend/src/routes/webhook.ts +++ b/apps/backend/src/routes/webhook.ts @@ -1,10 +1,48 @@ import type { FastifyInstance } from "fastify"; +import { z } from "zod"; import { env } from "../config/env.js"; import { verifyWebhookSignature } from "../lib/signature.js"; import { parseCommand } from "../lib/commands.js"; import { sendText } from "../services/whatsapp.js"; import { handleCommand } from "../services/router.js"; -import type { WhatsAppWebhookPayload } from "../types/index.js"; + +const whatsappInboundMessageSchema = z.object({ + from: z.string(), + id: z.string(), + timestamp: z.string(), + type: z.string(), + text: z + .object({ + body: z.string(), + }) + .optional(), +}); + +export const whatsappWebhookPayloadSchema = z.object({ + object: z.string(), + entry: z + .array( + z.object({ + id: z.string(), + changes: z + .array( + z.object({ + field: z.string(), + value: z.object({ + messaging_product: z.string(), + metadata: z.object({ + display_phone_number: z.string(), + phone_number_id: z.string(), + }), + messages: z.array(whatsappInboundMessageSchema).optional(), + }), + }), + ) + .optional(), + }), + ) + .optional(), +}); /** * WhatsApp Cloud API webhook. @@ -37,12 +75,12 @@ export async function webhookRoutes(app: FastifyInstance) { return reply.code(401).send({ error: "bad signature" }); } - const payload = request.body as WhatsAppWebhookPayload; + const payload = whatsappWebhookPayloadSchema.parse(request.body); + const messages = payload.entry?.flatMap( (entry) => - entry.changes?.flatMap((change) => change.value.messages ?? []) ?? - [], + entry.changes?.flatMap((change) => change.value.messages ?? []) ?? [], ) ?? []; for (const message of messages) { diff --git a/apps/backend/test/webhook.test.ts b/apps/backend/test/webhook.test.ts new file mode 100644 index 0000000..acfb1e6 --- /dev/null +++ b/apps/backend/test/webhook.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "vitest"; +import { buildApp } from "../src/app.js"; + +describe("POST /webhooks/whatsapp payload validation", () => { + it("accepts a valid WhatsAppWebhookPayload", async () => { + const app = buildApp(); + try { + const response = await app.inject({ + method: "POST", + url: "/webhooks/whatsapp", + payload: { + object: "whatsapp_business_account", + entry: [ + { + id: "123", + changes: [ + { + field: "messages", + value: { + messaging_product: "whatsapp", + metadata: { + display_phone_number: "123456789", + phone_number_id: "987654321", + }, + messages: [ + { + from: "15555555555", + id: "wamid.HBgLMTU1NTU1NTU1NTU=", + timestamp: "1672531199", + type: "text", + text: { + body: "balance", + }, + }, + ], + }, + }, + ], + }, + ], + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ received: true }); + } finally { + await app.close(); + } + }); + + it("returns 400 when object field is missing", async () => { + const app = buildApp(); + try { + const response = await app.inject({ + method: "POST", + url: "/webhooks/whatsapp", + payload: { + entry: [], + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json().error).toBeDefined(); + } finally { + await app.close(); + } + }); + + it("returns 400 when metadata fields are missing in changes", async () => { + const app = buildApp(); + try { + const response = await app.inject({ + method: "POST", + url: "/webhooks/whatsapp", + payload: { + object: "whatsapp_business_account", + entry: [ + { + id: "123", + changes: [ + { + field: "messages", + value: { + messaging_product: "whatsapp", + // metadata is missing + }, + }, + ], + }, + ], + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json().error).toBeDefined(); + } finally { + await app.close(); + } + }); +}); From 5f196181c9c397ab764d2e57d58fe22326795de2 Mon Sep 17 00:00:00 2001 From: Muhammadcodes112 Date: Sat, 11 Jul 2026 02:29:48 -0700 Subject: [PATCH 2/2] feat: implement detailed health checks for Postgres and Redis (closes #9) --- apps/backend/package.json | 3 + apps/backend/src/routes/health.ts | 73 ++++++++++++- apps/backend/test/health.test.ts | 149 ++++++++++++++++++++++++++ pnpm-lock.yaml | 171 ++++++++++++++++++++++++++++++ 4 files changed, 391 insertions(+), 5 deletions(-) create mode 100644 apps/backend/test/health.test.ts diff --git a/apps/backend/package.json b/apps/backend/package.json index 0448c88..d55a62a 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -14,11 +14,14 @@ "dependencies": { "@stellar/stellar-sdk": "^16.0.1", "fastify": "^5.10.0", + "ioredis": "^5.11.1", + "pg": "^8.22.0", "zod": "^4.4.3" }, "devDependencies": { "@eslint/js": "^9.39.4", "@types/node": "^20.19.43", + "@types/pg": "^8.20.0", "eslint": "^9.39.4", "pino-pretty": "^13.1.3", "tsx": "^4.23.0", diff --git a/apps/backend/src/routes/health.ts b/apps/backend/src/routes/health.ts index 842d31b..068fafc 100644 --- a/apps/backend/src/routes/health.ts +++ b/apps/backend/src/routes/health.ts @@ -1,10 +1,73 @@ import type { FastifyInstance } from "fastify"; +import { Client } from "pg"; +import { Redis } from "ioredis"; import { env } from "../config/env.js"; +async function checkPostgres(url: string, timeoutMs = 2000): Promise { + const client = new Client({ connectionString: url }); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error("Timeout")), timeoutMs), + ); + + try { + await Promise.race([client.connect(), timeoutPromise]); + await Promise.race([client.query("SELECT 1"), timeoutPromise]); + return true; + } catch { + return false; + } finally { + await client.end().catch(() => {}); + } +} + +async function checkRedis(url: string, timeoutMs = 2000): Promise { + const client = new Redis(url, { + lazyConnect: true, + connectTimeout: timeoutMs, + maxRetriesPerRequest: 0, + }); + + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error("Timeout")), timeoutMs), + ); + + try { + await Promise.race([client.connect(), timeoutPromise]); + const ping = await Promise.race([client.ping(), timeoutPromise]); + return ping === "PONG"; + } catch { + return false; + } finally { + client.disconnect(); + } +} + export async function healthRoutes(app: FastifyInstance) { - app.get("/health", async () => ({ - status: "ok" as const, - network: env.stellar.network, - uptime: process.uptime(), - })); + app.get("/health", async (request, reply) => { + const checkInfra = process.env.ENABLE_INFRA_HEALTH_CHECK === "true"; + + let dbStatus: "ok" | "error" = "ok"; + let redisStatus: "ok" | "error" = "ok"; + + if (checkInfra) { + const [dbOk, redisOk] = await Promise.all([ + checkPostgres(env.databaseUrl), + checkRedis(env.redisUrl), + ]); + dbStatus = dbOk ? "ok" : "error"; + redisStatus = redisOk ? "ok" : "error"; + } + + const overallStatus = + dbStatus === "ok" && redisStatus === "ok" ? "ok" : "error"; + const statusCode = overallStatus === "ok" ? 200 : 503; + + return reply.status(statusCode).send({ + status: overallStatus, + checks: { + db: dbStatus, + redis: redisStatus, + }, + }); + }); } diff --git a/apps/backend/test/health.test.ts b/apps/backend/test/health.test.ts new file mode 100644 index 0000000..a172837 --- /dev/null +++ b/apps/backend/test/health.test.ts @@ -0,0 +1,149 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// Define mock instances at the top level +const mockPgClient = { + connect: vi.fn(), + query: vi.fn(), + end: vi.fn(), +}; + +const mockRedisClient = { + connect: vi.fn(), + ping: vi.fn(), + disconnect: vi.fn(), +}; + +// Mock modules using ES6 class constructor syntax that returns our mock instances +vi.mock("pg", () => { + return { + Client: class { + constructor() { + return mockPgClient; + } + }, + }; +}); + +vi.mock("ioredis", () => { + return { + Redis: class { + constructor() { + return mockRedisClient; + } + }, + }; +}); + +import { buildApp } from "../src/app.js"; + +describe("GET /health", () => { + beforeEach(() => { + vi.clearAllMocks(); + delete process.env.ENABLE_INFRA_HEALTH_CHECK; + + // Set default success implementations + mockPgClient.connect.mockResolvedValue(undefined); + mockPgClient.query.mockResolvedValue({ rows: [] }); + mockPgClient.end.mockResolvedValue(undefined); + + mockRedisClient.connect.mockResolvedValue(undefined); + mockRedisClient.ping.mockResolvedValue("PONG"); + }); + + it("returns ok status when ENABLE_INFRA_HEALTH_CHECK is disabled", async () => { + const app = buildApp(); + try { + const response = await app.inject({ + method: "GET", + url: "/health", + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ + status: "ok", + checks: { + db: "ok", + redis: "ok", + }, + }); + } finally { + await app.close(); + } + }); + + it("returns 200 when both db and redis are healthy with ENABLE_INFRA_HEALTH_CHECK=true", async () => { + process.env.ENABLE_INFRA_HEALTH_CHECK = "true"; + + const app = buildApp(); + try { + const response = await app.inject({ + method: "GET", + url: "/health", + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ + status: "ok", + checks: { + db: "ok", + redis: "ok", + }, + }); + + expect(mockPgClient.connect).toHaveBeenCalled(); + expect(mockPgClient.query).toHaveBeenCalledWith("SELECT 1"); + expect(mockRedisClient.connect).toHaveBeenCalled(); + expect(mockRedisClient.ping).toHaveBeenCalled(); + } finally { + await app.close(); + } + }); + + it("returns 503 when db query fails with ENABLE_INFRA_HEALTH_CHECK=true", async () => { + process.env.ENABLE_INFRA_HEALTH_CHECK = "true"; + mockPgClient.query.mockRejectedValue(new Error("Query failed")); + + const app = buildApp(); + try { + const response = await app.inject({ + method: "GET", + url: "/health", + }); + + expect(response.statusCode).toBe(503); + expect(response.json()).toEqual({ + status: "error", + checks: { + db: "error", + redis: "ok", + }, + }); + } finally { + await app.close(); + } + }); + + it("returns 503 when redis ping fails with ENABLE_INFRA_HEALTH_CHECK=true", async () => { + process.env.ENABLE_INFRA_HEALTH_CHECK = "true"; + mockRedisClient.ping.mockRejectedValue(new Error("Ping failed")); + + const app = buildApp(); + try { + const response = await app.inject({ + method: "GET", + url: "/health", + }); + + expect(response.statusCode).toBe(503); + expect(response.json()).toEqual({ + status: "error", + checks: { + db: "ok", + redis: "error", + }, + }); + } finally { + await app.close(); + } + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4b4bc3..7ffc68b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,12 @@ importers: fastify: specifier: ^5.10.0 version: 5.10.0 + ioredis: + specifier: ^5.11.1 + version: 5.11.1 + pg: + specifier: ^8.22.0 + version: 8.22.0 zod: specifier: ^4.4.3 version: 4.4.3 @@ -36,6 +42,9 @@ importers: '@types/node': specifier: ^20.19.43 version: 20.19.43 + '@types/pg': + specifier: ^8.20.0 + version: 8.20.0 eslint: specifier: ^9.39.4 version: 9.39.4(jiti@2.7.0) @@ -571,6 +580,9 @@ packages: cpu: [x64] os: [win32] + '@ioredis/commands@1.10.0': + resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -909,6 +921,9 @@ packages: '@types/node@20.19.43': resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1326,6 +1341,10 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cluster-key-slot@1.1.1: + resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} + engines: {node: '>=0.10.0'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1411,6 +1430,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'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1863,6 +1886,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ioredis@5.11.1: + resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} + engines: {node: '>=12.22.0'} + ipaddr.js@2.4.0: resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==} engines: {node: '>= 10'} @@ -2321,6 +2348,40 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.15.0: + resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.22.0: + resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2358,6 +2419,22 @@ packages: resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2412,6 +2489,14 @@ packages: real-require@1.0.0: resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + 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'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -2573,6 +2658,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} @@ -2883,6 +2971,10 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -3305,6 +3397,8 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@ioredis/commands@1.10.0': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3560,6 +3654,12 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/pg@8.20.0': + dependencies: + '@types/node': 20.19.43 + pg-protocol: 1.15.0 + pg-types: 2.2.0 + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: '@types/react': 19.2.17 @@ -3995,6 +4095,8 @@ snapshots: client-only@0.0.1: {} + cluster-key-slot@1.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -4069,6 +4171,8 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -4699,6 +4803,18 @@ snapshots: hasown: 2.0.4 side-channel: 1.1.1 + ioredis@5.11.1: + dependencies: + '@ioredis/commands': 1.10.0 + cluster-key-slot: 1.1.1 + debug: 4.4.3 + denque: 2.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ipaddr.js@2.4.0: {} is-array-buffer@3.0.5: @@ -5143,6 +5259,41 @@ snapshots: pathe@2.0.3: {} + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + + pg-protocol@1.15.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.22.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.22.0) + pg-protocol: 1.15.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -5199,6 +5350,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prelude-ls@1.2.1: {} prettier@3.9.5: {} @@ -5239,6 +5400,12 @@ snapshots: real-require@1.0.0: {} + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.9 @@ -5461,6 +5628,8 @@ snapshots: stackback@0.0.2: {} + standard-as-callback@2.1.0: {} + std-env@4.2.0: {} stop-iteration-iterator@1.1.0: @@ -5805,6 +5974,8 @@ snapshots: wrappy@1.0.2: {} + xtend@4.0.2: {} + yallist@3.1.1: {} yaml@2.9.0: