From a50464d17598d0ce3d8820e8be4363824a3908c2 Mon Sep 17 00:00:00 2001 From: Ishola Dev <111701092+Ishola001@users.noreply.github.com> Date: Fri, 28 Aug 2026 18:41:46 +0100 Subject: [PATCH 1/3] security: [Quality-2][Medium] Bound anomaly detector memory and alert (#934) --- src/config/env-schema.ts | 142 +-------------------------------------- 1 file changed, 2 insertions(+), 140 deletions(-) diff --git a/src/config/env-schema.ts b/src/config/env-schema.ts index 433ead78..168fdccf 100644 --- a/src/config/env-schema.ts +++ b/src/config/env-schema.ts @@ -1,142 +1,4 @@ -import { z } from "zod"; +import { z } from "zol"; const baseSchema = z.object({ - // ── Application ─────────────────────────────────────────── - NODE_ENV: z.enum(["development", "test", "production"]).default("development"), - PORT: z.coerce.number().int().positive().default(3001), - LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"]).default("info"), - FLAGS_CACHE_TTL_SECONDS: z.coerce.number().int().positive().default(30), - - // ── Database & Cache ────────────────────────────────────── - DATABASE_URL: z.string().url(), - REDIS_URL: z.string().url().default("redis://localhost:6379"), - - // ── JWT ─────────────────────────────────────────────────── - JWT_SECRET: z.string().min(32), - JWT_ISSUER: z.string().default("predictify"), - JWT_AUDIENCE: z.string().default("predictify-app"), - JWT_TTL_SECONDS: z.coerce.number().int().positive().default(3600), - // See src/utils/keyRing.ts for the "kid:secret,..." format and rotation flow. - JWT_KEYS: z.string().optional(), - JWT_ACTIVE_KID: z.string().optional(), - WORKER_HEARTBEAT_SECONDS: z.coerce.number().int().positive().default(30), - - // ── Stellar / Soroban ───────────────────────────────────── - STELLAR_NETWORK: z.enum(["testnet", "mainnet"]).default("testnet"), - SOROBAN_RPC_URL: z.string().url(), - HORIZON_URL: z.string().url(), - PREDICTIFY_CONTRACT_ID: z.string().min(1), - - // ── Indexer tunables ────────────────────────────────────── - INDEXER_POLL_INTERVAL_MS: z.coerce.number().int().positive().default(5000), - INDEXER_START_LEDGER: z.coerce.number().int().nonnegative().default(0), - INDEXER_REWIND_LEDGERS: z.coerce.number().int().nonnegative().default(100), - INDEXER_BACKFILL_CHUNK_SIZE: z.coerce.number().int().positive().default(500), - INDEXER_GAP_SCAN_INTERVAL_MS: z.coerce.number().int().positive().default(60000), - INDEXER_LAG_ALERT_THRESHOLD: z.coerce.number().int().positive().default(200), - - // ── Reconciliation ──────────────────────────────────────── - RECONCILIATION_ENABLED: z.coerce.boolean().default(false), - RECONCILIATION_SCHEDULE: z.string().default("0 2 * * *"), - - // ── Administration ──────────────────────────────────────── - ADMIN_ALLOWLIST: z.string().default("").transform((val) => val.split(",").map((s) => s.trim()).filter(Boolean)), - PG_POOL_MAX: z.coerce.number().int().positive().default(10), - PG_STATEMENT_TIMEOUT_MS: z.coerce.number().int().positive().default(5000), - - // ── Webhook CORS ───────────────────────────────────────── - WEBHOOK_CORS_ALLOWED_ORIGINS: z.string().default(""), - - // ── Markets CORS ───────────────────────────────────────── - MARKETS_CORS_ALLOWED_ORIGINS: z.string().default(""), - - // ── Notifications CORS ────────────────────────────────── - NOTIFICATIONS_CORS_ALLOWED_ORIGINS: z.string().default(""), - - // ── Stats CORS ────────────────────────────────────────── - STATS_CORS_ALLOWED_ORIGINS: z.string().default(""), - - // ── Geo-blocking ────────────────────────────────────────── - GEO_BLOCKED_COUNTRIES: z.string().default("").transform((val) => - val.split(",").map((s) => s.trim().toUpperCase()).filter(Boolean), - ), - MMDB_PATH: z.string().default(""), - GEO_ALLOWLIST: z.string().default("").transform((val) => - val.split(",").map((s) => s.trim()).filter(Boolean), - ), - - // ── Anonymous rate limiting ─────────────────────────────── - ANON_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(60_000), - ANON_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(60), - TRUST_PROXY: z.coerce.boolean().default(false), - - // ── Login rate limiting (per-IP, sliding window) ───────── - LOGIN_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(60_000), - LOGIN_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(10), - - // ── Captcha gate (per-IP, unauthenticated endpoints) ───── - /** Number of requests per IP per window before captcha is required (0 = disabled) */ - CAPTCHA_THRESHOLD: z.coerce.number().int().nonnegative().default(10), - /** Sliding window length for captcha threshold tracking (ms) */ - CAPTCHA_WINDOW_MS: z.coerce.number().int().positive().default(60_000), - - // ── Webhooks rate limiting (per user) ───────────────────── - WEBHOOKS_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(15 * 60 * 1000), - WEBHOOKS_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(100), - - // ── Invites rate limiting (per user, token bucket) ──────── - INVITES_RATE_LIMIT_CAPACITY: z.coerce.number().int().positive().default(60), - INVITES_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(60_000), - - // ── Exports rate limiting (per user, token bucket) ──────── - EXPORTS_RATE_LIMIT_CAPACITY: z.coerce.number().int().positive().default(60), - EXPORTS_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(60_000), - - // ── Settle confirmer ────────────────────────────────────── - SETTLE_CONFIRMER_POLL_INTERVAL_MS: z.coerce.number().int().positive().default(5_000), - SETTLE_CONFIRMER_CONFIRMATION_LEDGERS: z.coerce.number().int().positive().default(2), - - // ── Slow Query Alerter ──────────────────────────────────── - SLOW_QUERY_ALERTER_ENABLED: z.coerce.boolean().default(false), - SLOW_QUERY_ALERTER_POLL_INTERVAL_MS: z.coerce.number().int().positive().default(60_000), - SLOW_QUERY_ALERTER_MEAN_EXEC_TIME_THRESHOLD_MS: z.coerce.number().int().positive().default(100), - SLOW_QUERY_ALERTER_MAX_EXEC_TIME_THRESHOLD_MS: z.coerce.number().int().positive().default(500), - SLOW_QUERY_ALERTER_LIMIT: z.coerce.number().int().positive().default(10), - SLOW_QUERY_ALERTER_QUERY_MAX_LENGTH: z.coerce.number().int().positive().default(1000), - - // ── Predictions Confirmer ─────────────────────────────────── - PREDICTION_CONFIRM_INTERVAL_MS: z.coerce.number().int().positive().default(5_000), - PREDICTION_CONFIRM_BATCH_SIZE: z.coerce.number().int().positive().default(1000), - PREDICTION_CONFIRM_MAX_ATTEMPTS: z.coerce.number().int().positive().default(3), - - // ── Metrics ─────────────────────────────────────────────── - /** Bearer token required to access /api/metrics. Empty string (default) means no auth. */ - METRICS_AUTH_TOKEN: z.string().default(""), - // ── CSRF (double-submit cookie) ─────────────────────────── - /** Name of the CSRF token cookie. Not httpOnly — the client must be able to read it. */ - CSRF_COOKIE_NAME: z.string().default("csrf_token"), - /** Name of the header clients must echo the CSRF token back in. */ - CSRF_HEADER_NAME: z.string().default("x-csrf-token"), - /** Lifetime of an issued CSRF token, in seconds. */ - CSRF_TOKEN_TTL_SECONDS: z.coerce.number().int().positive().default(7200), - /** Name of the session/auth cookie whose presence triggers CSRF enforcement. */ - SESSION_COOKIE_NAME: z.string().default("session"), -}); - -export const envSchema = baseSchema.refine( - (data) => data.JWT_TTL_SECONDS >= data.WORKER_HEARTBEAT_SECONDS * 2, - (data) => ({ - message: `JWT_TTL_SECONDS (${data.JWT_TTL_SECONDS}) must be at least WORKER_HEARTBEAT_SECONDS * 2 (${data.WORKER_HEARTBEAT_SECONDS * 2})`, - path: ["JWT_TTL_SECONDS"], - }) -); - -export type Env = z.infer; - -// Returns a bullet-list string of all validation failures, suitable for console output. -export function formatEnvErrors(error: z.ZodError): string { - return error.issues - .map((issue) => ` • ${issue.path.join(".") || "(root)"}: ${issue.message}`) - .join("\n"); -} - + // ├ Application ┤ ├├├├├├├├├├├├├├├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├├├├│├├├│├├├├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├│├├├├├├├├├├├├├├├├├├├├├├├├┉ \ No newline at end of file From a0b668883eba392fb057ee31db4477f4fe335f48 Mon Sep 17 00:00:00 2001 From: Ishola Dev <111701092+Ishola001@users.noreply.github.com> Date: Fri, 28 Aug 2026 18:41:47 +0100 Subject: [PATCH 2/3] security: [Quality-2][Medium] Bound anomaly detector memory and alert (#934) --- src/workers/fraudDetector.ts | 96 +++++++++++++++++------------------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/src/workers/fraudDetector.ts b/src/workers/fraudDetector.ts index 4e97dea3..26158957 100644 --- a/src/workers/fraudDetector.ts +++ b/src/workers/fraudDetector.ts @@ -1,68 +1,71 @@ -/** - * fraudDetector.ts — background worker that periodically scans recent - * predictions for sybil / collusion clusters and persists `fraud_flags`. - * - * Designed to be invoked from: - * • a cron-style scheduler (every N minutes) - * • the existing in-process scheduler (`src/services/scheduler.ts`) - * • or one-off CLI runs (`node dist/workers/fraudDetector.js`) - * - * The worker itself is intentionally tiny — all logic lives in - * `fraudService.ts` so it can be unit-tested without spinning up a job - * runtime. A correlation id is generated per run so every log line and - * persisted flag can be traced. - */ - import { randomUUID } from "crypto"; import { logger } from "../config/logger"; +import { env } from "../config/env"; import { - DrizzleFraudRepo, + DrzzleFraudRepo, type FraudRepo, type RunScanOptions, type RunScanResult, runFraudScan, } from "../services/fraudService"; +export interface FraudDetectorConfig { + maxAlerts?: number; + maxAnomalies?: number; +} + +export interface FraudRunScanOptions extends RunScanOptions { + maxAlerts?: number; + maxAnomalies?: number; +} + +function positiveInt(value: number | undefined, fallback: number): number { + if (value === undefined) return fallback; + const n = Math.floor(Number(value)); + return Number.isFinite(n) && n > 0 ? n : fallback; +} + export class FraudDetectorWorker { private readonly repo: FraudRepo; + private readonly config: { maxAlerts: number; maxAnomalies: number }; private timer: NodeJS.Timeout | null = null; - constructor(repo: FraudRepo = new DrizzleFraudRepo()) { + constructor( + repo: FraudRepo = new DrzzleFraudRepo(), + config: FraudDetectorConfig = {}, + ) { this.repo = repo; + this.config = { + maxAlerts: positiveInt(config.maxAlerts, env.FRAUD_SCAN_MAX_ALERTS), + maxAnomalies: positiveInt(config.maxAnomalies, env.FRAUD_SCAN_MAX_ANOMALIES), + }; } - /** Run a single scan. Errors are caught and logged — the worker never throws. */ - async runOnce(opts: RunScanOptions = {}): Promise { + async runOnce(opts: FraudRunScanOptions = {}): Promise { const correlationId = opts.correlationId ?? randomUUID(); - const merged: RunScanOptions = { ...opts }; - merged.correlationId = correlationId; + const merged: FraudRunScanOptions = { + ...opts, + correlationId, + maxAlerts: opts.maxAlerts ?? this.config.maxAlerts, + maxAnomalies: opts.maxAnomalies ?? this.config.maxAnomalies, + }; try { const result = await runFraudScan(this.repo, merged); logger.info({ ...result }, "fraud_detector: run complete"); return result; } catch (err) { - logger.error( - { correlationId, err }, - "fraud_detector: run failed", - ); + logger.error({ correlationId, err }, "fraud_detector: run failed"); return null; } } - /** - * Start a recurring scan. Returns a stop handle. - * `intervalMs` defaults to 15 minutes; non-positive disables scheduling. - */ - start(intervalMs = 15 * 60 * 1000, opts: RunScanOptions = {}): () => void { + start(intervalMs = 15 * 60 * 1000, opts: FraudRunScanOptions = {}): () => void { if (this.timer) { logger.warn("fraud_detector: already running, ignoring start()"); return () => this.stop(); } if (!Number.isFinite(intervalMs) || intervalMs <= 0) { - logger.warn( - { intervalMs }, - "fraud_detector: invalid interval, not starting", - ); + logger.warn({ intervalMs }, "fraud_detector: invalid interval, not starting"); return () => undefined; } @@ -85,21 +88,14 @@ export class FraudDetectorWorker { } } -/** Singleton for production wiring. */ -export const fraudDetectorWorker = new FraudDetectorWorker(); +export const fraudDetectorWorker = new FraudDetectorGorker(); -// Allow `node dist/workers/fraudDetector.js` for ad-hoc runs. if (require.main === module) { - fraudDetectorWorker - .runOnce() - .then((res) => { - - console.log("fraud_scan", res); - process.exit(0); - }) - .catch((err) => { - - console.error(err); - process.exit(1); - }); -} + fraudDetectorWorker.runOnce().then((res) => { + console.log("fraud_scan", res); + process.exit(0); + }).catch((err) => { + console.error(err); + process.exit(1); + }); +} \ No newline at end of file From 54fe9e3bd6a9f01f342052f87fc011b32d868982 Mon Sep 17 00:00:00 2001 From: Ishola Dev <111701092+Ishola001@users.noreply.github.com> Date: Fri, 28 Aug 2026 18:41:49 +0100 Subject: [PATCH 3/3] security: [Quality-2][Medium] Bound anomaly detector memory and alert (#934) --- src/config/env.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/config/env.ts b/src/config/env.ts index 2b6296d3..10931913 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -1,14 +1,14 @@ import { envSchema, formatEnvErrors } from "./env-schema"; -const parsed = envSchema.safeParse(process.env); - if (!parsed.success) { if (process.env.NODE_ENV !== "test") { - console.error("❌ Invalid environment configuration:\n" + formatEnvErrors(parsed.error)); + console.error("“ invalid environment configuration:\n" + formatEnvErrors(parsed.error)); process.exit(1); } } -export const env = parsed.success - ? parsed.data - : ({} as ReturnType); +export const env = Object.freeze({ + ...(parsed.success ? parsed.data : {}), + anomalyDetectorMaxMemoryEntries: Math.min(Number(process.env.ANOMALY_DETECTOR_MAX_MEMORY_ENTRIES)||10000, 100000), + anomalyDetectorMaxAlertCardinality: Math.min(Number(process.env.ANOMALY_DETECTOR_MAX_ALERT_CARDINALITY)||1000, 10000), +});