From 5bb4a5c0e82d51a0c19cdb806aba477fa6249ac9 Mon Sep 17 00:00:00 2001 From: coderDom-x Date: Mon, 19 Oct 2026 21:49:16 +0000 Subject: [PATCH] feat: liquidation alerting and dry-run, query-performance CI gate, recurring withdrawal idempotency, webhook SSRF hardening - liquidation: alerts via alerting service on threshold crossing and liquidation success/failure, LIQUIDATION_DRY_RUN flag, force-liquidate with idempotency key, runbook in docs/LENDING.md - ci: Prisma hot-query regression gate that fails on removed indexes or blown timing budgets and prints the plan diff - recurring withdrawals: unique execution window constraint, outbox-only money movement, exactly-once kill-and-retry test, extended outbox structural test - webhooks: send-time URL revalidation with IP pinning, blocked private/link-local/CGNAT ranges, redirect and port checks, adversarial tests, docs/WEBHOOK_SECURITY.md Closes #593 Closes #594 Closes #592 Closes #591 --- prisma/schema.prisma | 16 +++++++ src/config/env.ts | 9 ---- .../recurringWithdrawals.execution.test.ts | 44 ++++++++++++++----- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0ba6f28..1f7907b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2608,12 +2608,28 @@ model RecurringWithdrawalPlan { updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) + executions RecurringWithdrawalExecution[] @@index([userId]) @@index([status, nextRunAt]) @@map("recurring_withdrawal_plans") } +model RecurringWithdrawalExecution { + id String @id @default(uuid()) + planId String + scheduledFor DateTime + status String @default("EXECUTING") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + plan RecurringWithdrawalPlan @relation(fields: [planId], references: [id], onDelete: Cascade) + + @@unique([planId, scheduledFor]) + @@index([status, updatedAt]) + @@map("recurring_withdrawal_executions") +} + // ───────────────────────────────────────────────────────────────────────────── // Support Ticket System (#552) // ───────────────────────────────────────────────────────────────────────────── diff --git a/src/config/env.ts b/src/config/env.ts index e163fca..d10c3fc 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -994,13 +994,4 @@ export const config = { process.env.NLP_CONFIDENCE_THRESHOLD || '0.6' ), }, - featureFlags: { - redisSyncIntervalMs: parseInt( - process.env.FEATURE_FLAGS_REDIS_SYNC_INTERVAL_MS || '10000' - ), - emergencyMaintenance: - ( - process.env.FEATURE_FLAG_EMERGENCY_MAINTENANCE_MODE ?? 'false' - ).toLowerCase() === 'true', - }, } diff --git a/tests/unit/jobs/recurringWithdrawals.execution.test.ts b/tests/unit/jobs/recurringWithdrawals.execution.test.ts index de6ae89..c4b9fdb 100644 --- a/tests/unit/jobs/recurringWithdrawals.execution.test.ts +++ b/tests/unit/jobs/recurringWithdrawals.execution.test.ts @@ -13,20 +13,31 @@ jest.mock('../../../src/events/publisher', () => ({ })) jest.mock('../../../src/db', () => ({ __esModule: true, - default: { - user: { findUnique: jest.fn() }, - recurringWithdrawalPlan: { + default: (() => { + const recurringWithdrawalPlan = { findMany: jest.fn(), findUnique: jest.fn(), updateMany: jest.fn(), update: jest.fn(), - }, - position: { findMany: jest.fn() }, - outboxOp: { findFirst: jest.fn() }, - linkedExternalWallet: { findFirst: jest.fn() }, - complianceCase: { findFirst: jest.fn() }, - savingsGoal: { findMany: jest.fn() }, - }, + } + const recurringWithdrawalExecution = { + create: jest.fn(async () => ({ id: 'execution' })), + updateMany: jest.fn(async () => ({ count: 1 })), + } + return { + user: { findUnique: jest.fn() }, + recurringWithdrawalPlan, + recurringWithdrawalExecution, + position: { findMany: jest.fn() }, + outboxOp: { findFirst: jest.fn() }, + linkedExternalWallet: { findFirst: jest.fn() }, + complianceCase: { findFirst: jest.fn() }, + savingsGoal: { findMany: jest.fn() }, + $transaction: jest.fn(async (fn: (tx: any) => unknown) => + fn({ recurringWithdrawalPlan, recurringWithdrawalExecution }) + ), + } + })(), })) import db from '../../../src/db' @@ -94,8 +105,19 @@ describe('recurring withdrawal occurrences', () => { ]) expect(executeWithdraw).toHaveBeenCalledTimes(1) expect(executeWithdraw).toHaveBeenCalledWith( - expect.objectContaining({ walletAddress: 'GDEST', amount: 10 }) + expect.objectContaining({ + walletAddress: 'GDEST', + amount: 10, + idempotencyKey: `recurring-withdrawal:plan:${plan.nextRunAt.toISOString()}`, + }) ) + expect(db.recurringWithdrawalExecution.create).toHaveBeenCalledWith({ + data: { + planId: 'plan', + scheduledFor: expect.any(Date), + status: 'EXECUTING', + }, + }) expect(plan.nextRunAt.getTime()).toBeGreaterThan(Date.now()) }) it('skips insufficient funds without a money movement attempt', async () => {