From da4c3290eea9e461c6c7e9ac990edc78e07ff521 Mon Sep 17 00:00:00 2001 From: ToryMic Date: Tue, 29 Sep 2026 23:10:31 -0400 Subject: [PATCH 1/3] fix(backend): repair broken merge artifacts blocking build and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main is currently unbuildable and its test suite is red, so every back-end CI job fails before it reaches the code under review. The breakage all traces back to two bad merges (9a15975c, 7300a48a) that truncated a Prisma model block, dropped a closing brace, renamed an imported limiter and replaced the idempotency replay cache. Prisma schema - close `WalletTenantAssociation` and separate it from `IdempotencyKey` (a missing `}` made `prisma generate` fail, so `@prisma/client` stayed an uninitialised stub and every suite crashed on import) - add `SessionAuditLog`, `Transaction.deletedAt` and `WebhookEndpoint.tenantId`, all of which sessionAudit.ts and the tenant boundary guard already query - add the matching migration and refresh the committed SQLite dev.db Type errors - import `readsLimiter` in vaultEndpoints.ts (used by GET /receipts) - restore the idempotency replay cache as idempotencyStore.ts and re-export it from idempotency.ts, so transferOrchestrator.ts, vaultEndpoints.ts, index.ts and idempotencyRetention.ts resolve - fix the unterminated `if` in diffSchemaShapes and a possibly-undefined `baseline.properties` read in apiContractSnapshots.ts - build the OTel resource through whichever factory the installed @opentelemetry/resources major exposes (v1 `new Resource()`, v2 `resourceFromAttributes()`) - `ZodObject._shape` → `.shape` for Zod 4 - return the strategy-switch 200 response, order receipts by `timestamp`, serialise session metadata, and type the request mocks in src/tests Error contract - restore `summary`, `errors` and 404 `path` on the error envelope Tests - issues711 "newly added required fields" mutated the live schema instead of the baseline snapshot, so it asserted against a scenario that can never produce the expected diff --- backend/prisma/dev.db | Bin 827392 -> 1052672 bytes .../migration.sql | 38 ++ backend/prisma/schema.prisma | 34 ++ backend/schema-snapshots/get-_health.json | 2 +- backend/schema-snapshots/get-_ready.json | 2 +- backend/src/__tests__/issues711.test.ts | 13 +- backend/src/apiContractSnapshots.ts | 6 +- backend/src/idempotency.ts | 16 + backend/src/idempotencyStore.ts | 360 ++++++++++++++++++ backend/src/index.ts | 1 + backend/src/middleware/apiError.ts | 16 + backend/src/middleware/validate.ts | 2 + backend/src/schemaSnapshot.ts | 2 +- backend/src/sessionAudit.ts | 2 +- backend/src/swagger.ts | 1 - backend/src/tests/idempotency.test.ts | 9 +- backend/src/tests/tenantBoundary.test.ts | 4 +- backend/src/tracing.ts | 26 +- backend/src/vaultEndpoints.ts | 8 +- 19 files changed, 518 insertions(+), 24 deletions(-) create mode 100644 backend/prisma/migrations/20260929120000_add_session_audit_and_tenant_scopes/migration.sql create mode 100644 backend/src/idempotencyStore.ts diff --git a/backend/prisma/dev.db b/backend/prisma/dev.db index 4816f30799b05807212a9cc4eb74f4fcca47805e..a319596bdb09c10ad9b07c84f539bce86bb94bc3 100644 GIT binary patch literal 1052672 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 z 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 q z+`V|JtZG%81%LMuI}<^P(K>;L5Vt}6RGQ~!Gufkqer7f;l4CXAAvodkcNUK|;M^az z1V4=b($QLe@CfIKrHSyA6gkg4kvhpy5uz1APEEsF5kFMdyVXRjE6;$o@M!_yB1zDS zP$4!t?pc~EkVY&!SI-aXin9H!jv+(~H<2A76{0?8mEgEq%VrFLxPg#G4ns_9gelg@ zLWXF(BV>hND;{eYLahJ0*B^>R|Dyko`hLInOwao^{1Z;)Mhbu~bCPPdfKZso6>E$Sj+jX&3jb)S*n8v5%V)C40#o_|GdYi?GwGDa zCX2ajYseoB7ayV`#e_qv-KB|_lxmF(MQQHkm2f2-LL`zB4L$4s6pzY} zL_i`S5s(N-1SA3yfmI+dwr*~@!<{4NR)W|ua&9GvZ6fDZg4iB%ZY794A?H*= zxEJKyN)Q`B&aDKo@#EY|5FsDWtppJ<;+#qd_ots*31V0Jxs@RHqMus{Vh8#;l@LbE zIJXkS&h>LELF`vQw-UrI^>Zsh>`gzX62gcf=T?H)(|&Fxh@I@`R)W~Ser_d*UF+vm zLKrdS+)5C;DbB3~5k~RcN)THo&Z&fQ$V4$-cjMD-Nw1@Oc3T&-`qEpLIkh0!V zEaVy@hDuZ;&{i7=2f3!J9+_>Y#cu3MNaI5L&4P|iWjc`Vo$1sL6lB1172Wfwf;>mI zV!Nj8xdyUMd33dOs7Rm=^G`07Z#ylvaaW1xxTyrjrVtEbxE{P$k=))797R_>L$Osg zP`K?F9$erZI|x=ygk1-Q*#BMf=q!9SCf-d|MYr`pL82?rV-Yp*EsY{Q z^s1?_s!)h+<5eg+Z>Zx7Qe;`gI5zy6vkCcF>K8zuIghefVpZS%sMEFm3CARMPHgdsRtEK}P(#w1yHmaA~qtmH_!qhBJ zq+C;U!#27B3RU$g95*WqyP&u)f@3~4qY+une3q<$F6TxWd~OnkQOSj9Njj3?1NY}6|@S4*oa?+ zqSL733tRPc z1=;ItMTe|V;HqRok}{iUI*4@nYU!XgqElo?E44=?(th#5Qx{_tDfLoSHB7kZD3gadM03}q`J?CxfpkS>bL$0h7>}?| z`Pbq)2Zko6d8p1dXcEKPE=34(x@;h#OqLg^qew4?^K^vmN`vu!>`Z*i;evZ0ML#+z c`Vm)Y=m@4%%T&RUtPrza6o=^ux0byYAN?#|>;M1& delta 29393 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 { }); it('detects newly added required fields as breaking changes', () => { - const baseline = zodToJsonShape(HealthResponseSchema); - const current = JSON.parse(JSON.stringify(baseline)) as typeof baseline; - // Simulate an older snapshot that is missing the 'indexer' field - delete current.properties?.checks?.properties?.indexer; - current.properties!.checks!.required = (current.properties!.checks!.required ?? []).filter( + const current = zodToJsonShape(HealthResponseSchema); + const baseline = JSON.parse(JSON.stringify(current)) as typeof current; + // Simulate an older snapshot that is missing the 'indexer' field, so the + // live schema now has a required field the committed snapshot does not know + // about. `baseline` is the committed snapshot and `current` is the live + // schema, so the deletion has to happen on the baseline. + delete baseline.properties?.checks?.properties?.indexer; + baseline.properties!.checks!.required = (baseline.properties!.checks!.required ?? []).filter( (k: string) => k !== 'indexer', ); diff --git a/backend/src/apiContractSnapshots.ts b/backend/src/apiContractSnapshots.ts index acaa02eb4..461460bef 100644 --- a/backend/src/apiContractSnapshots.ts +++ b/backend/src/apiContractSnapshots.ts @@ -249,7 +249,7 @@ export function diffSchemaShapes( } for (const key of baselineRequired) { - if (!(key in baseline.properties ?? {})) { + if (!(key in baselineProps)) { issues.push({ path: at(key), message: 'required field missing from snapshot properties (orphaned reference)' }); } if (!(key in currentProps)) { @@ -260,8 +260,10 @@ export function diffSchemaShapes( } for (const key of currentRequired) { - if (!(key in current.properties ?? {})) { + if (!(key in (current.properties ?? {}))) { issues.push({ path: at(key), message: 'required field missing from live schema properties (invalid schema)' }); + continue; + } if (!baselineRequired.has(key)) { issues.push({ path: at(key), message: 'field is now required — regenerate snapshots with npm run snapshots:write' }); } diff --git a/backend/src/idempotency.ts b/backend/src/idempotency.ts index 1ab4b0448..fcc20199c 100644 --- a/backend/src/idempotency.ts +++ b/backend/src/idempotency.ts @@ -26,6 +26,22 @@ import { prisma } from './prisma'; import { logger } from './middleware/structuredLogging'; import type { Request, Response, NextFunction } from 'express'; +/** + * The replay cache used by money-moving operations (deposits, withdrawals, + * transfers) lives in `idempotencyStore.ts`; it is re-exported here so callers + * keep a single import surface for the whole idempotency feature. + */ +export { + idempotencyStore, + IdempotencyStore, + IdempotencyConflictError, + buildIdempotencyFingerprint, + getIdempotencyHashThreshold, + type IdempotentOperationResult, + type IdempotencyKeyInfo, + type IdempotencyMetrics, +} from './idempotencyStore'; + // ─── Types ────────────────────────────────────────────────────────────────── export interface IdempotencyRecord { diff --git a/backend/src/idempotencyStore.ts b/backend/src/idempotencyStore.ts new file mode 100644 index 000000000..41e4e6d3e --- /dev/null +++ b/backend/src/idempotencyStore.ts @@ -0,0 +1,360 @@ +/** + * @file idempotencyStore.ts + * Idempotency key store backed by Redis (when available) with NodeCache in-process fallback. + * + * Issue #811: Multi-instance deployments previously lost idempotency guarantees on pod + * recycle because responses were stored only in NodeCache (in-process memory). This revision + * persists completed responses to Redis using SET … EX so all replicas share the same store. + * + * Behavior when Redis is unavailable: + * - Falls back to NodeCache automatically (fail-open). + * - A warning is logged so operators are aware of the degraded guarantee. + * + * Existing observability API (inspectKeys, deleteKey, clear, getMetrics) is preserved; + * operations apply to whichever backend holds the key. + * + * Scope: this module owns the *replay cache* used by money-moving operations + * (`idempotencyStore.execute`). The durable, per-key record lifecycle used by the + * `enforceIdempotency` middleware lives in `idempotency.ts` (Prisma-backed). + */ + +import crypto from 'crypto'; +import NodeCache from 'node-cache'; +import { redisClientManager } from './rateLimiter'; + +// ─── Public Types ───────────────────────────────────────────────────────────── + +export interface IdempotentOperationResult { + statusCode: number; + body: T; +} + +/** Metadata attached to every idempotency key entry. */ +export interface IdempotencyKeyMetadata { + /** ISO-8601 timestamp when the key was first stored. */ + createdAt: string; + /** ISO-8601 timestamp of the most recent access (read or write). */ + lastAccessedAt: string; + /** Number of times this key has been replayed (returned cached result). */ + replayCount: number; + /** Current state of the entry. */ + status: 'pending' | 'completed'; +} + +/** Summary returned by GET /admin/idempotency/keys. */ +export interface IdempotencyKeyInfo { + key: string; + metadata: IdempotencyKeyMetadata; +} + +/** Snapshot of store-wide observability counters. */ +export interface IdempotencyMetrics { + hits: number; + conflicts: number; + evictions: number; + activeKeys: number; + pendingKeys: number; +} + +// ─── Internal Types ─────────────────────────────────────────────────────────── + +interface StoredResponse extends IdempotentOperationResult { + fingerprint: string; + metadata: IdempotencyKeyMetadata; +} + +interface PendingOperation { + fingerprint: string; + promise: Promise>; + metadata: IdempotencyKeyMetadata; +} + +// ─── Errors ─────────────────────────────────────────────────────────────────── + +export class IdempotencyConflictError extends Error { + constructor(message = 'Idempotency key already used for a different request body') { + super(message); + this.name = 'IdempotencyConflictError'; + } +} + +// ─── Redis key prefix ───────────────────────────────────────────────────────── + +const REDIS_PREFIX = 'idempotency:'; + +// ─── Store ──────────────────────────────────────────────────────────────────── + +export class IdempotencyStore { + /** Fallback in-process store used when Redis is unavailable. */ + private readonly localCache: NodeCache; + private readonly pendingResponses = new Map>(); + + // Observability counters + private _hits = 0; + private _conflicts = 0; + private _evictions = 0; + + constructor(private readonly ttlMs = 24 * 60 * 60 * 1000) { + const ttlSeconds = Math.max(1, Math.ceil(this.ttlMs / 1000)); + this.localCache = new NodeCache({ stdTTL: ttlSeconds, checkperiod: ttlSeconds }); + this.localCache.on('expired', () => { this._evictions++; }); + } + + // ─── Redis helpers ───────────────────────────────────────────────────────── + + private redisKey(key: string): string { + return `${REDIS_PREFIX}${key}`; + } + + private get redis() { + const client = redisClientManager.getClient(); + return redisClientManager.isReady() && client ? client : null; + } + + private async redisGet(key: string): Promise | null> { + const r = this.redis; + if (!r) return null; + try { + const raw = await r.get(this.redisKey(key)); + return raw ? (JSON.parse(raw) as StoredResponse) : null; + } catch { + return null; + } + } + + private async redisSet(key: string, value: StoredResponse): Promise { + const r = this.redis; + if (!r) return; + try { + const ttlSeconds = Math.max(1, Math.ceil(this.ttlMs / 1000)); + await r.set(this.redisKey(key), JSON.stringify(value), 'EX', ttlSeconds); + } catch (err) { + console.log(JSON.stringify({ level: 'warn', event: 'idempotency_redis_write_fail', key, reason: (err as Error).message })); + } + } + + private async redisDel(key: string): Promise { + const r = this.redis; + if (!r) return false; + try { + return (await r.del(this.redisKey(key))) > 0; + } catch { + return false; + } + } + + // ─── Core execute ────────────────────────────────────────────────────────── + + async execute( + key: string, + fingerprint: string, + operation: () => Promise> + ): Promise<{ result: IdempotentOperationResult; replayed: boolean }> { + const now = new Date().toISOString(); + + // 1. Check Redis first, then local cache + let completed = await this.redisGet(key); + if (!completed) { + completed = this.localCache.get>(key) ?? null; + } + + if (completed) { + if (completed.fingerprint !== fingerprint) { + this._conflicts++; + throw new IdempotencyConflictError(); + } + this._hits++; + completed.metadata.lastAccessedAt = now; + completed.metadata.replayCount++; + // Refresh in both backends; errors are non-fatal + await this.redisSet(key, completed); + this.localCache.set(key, completed); + return { result: { statusCode: completed.statusCode, body: completed.body }, replayed: true }; + } + + // 2. Currently in-flight + const pendingOperation = this.pendingResponses.get(key) as PendingOperation | undefined; + if (pendingOperation) { + if (pendingOperation.fingerprint !== fingerprint) { + this._conflicts++; + throw new IdempotencyConflictError(); + } + this._hits++; + pendingOperation.metadata.lastAccessedAt = now; + pendingOperation.metadata.replayCount++; + const replayed = await pendingOperation.promise; + return { result: { statusCode: replayed.statusCode, body: replayed.body }, replayed: true }; + } + + // 3. First execution + const metadata: IdempotencyKeyMetadata = { createdAt: now, lastAccessedAt: now, replayCount: 0, status: 'pending' }; + + const operationPromise = (async () => { + const result = await operation(); + const stored: StoredResponse = { + ...result, + fingerprint, + metadata: { ...metadata, status: 'completed', lastAccessedAt: new Date().toISOString() }, + }; + // Persist to Redis (primary) and local cache (fallback/fast-path) + await this.redisSet(key, stored); + this.localCache.set(key, stored, this.ttlMs / 1000); + return stored; + })(); + + this.pendingResponses.set(key, { fingerprint, promise: operationPromise, metadata }); + + try { + const stored = await operationPromise; + return { result: { statusCode: stored.statusCode, body: stored.body }, replayed: false }; + } finally { + this.pendingResponses.delete(key); + } + } + + // ─── Inspection ──────────────────────────────────────────────────────────── + + inspectKeys(prefix?: string): IdempotencyKeyInfo[] { + const results: IdempotencyKeyInfo[] = []; + for (const key of this.localCache.keys()) { + if (prefix && !key.startsWith(prefix)) continue; + const entry = this.localCache.get>(key); + if (entry) results.push({ key, metadata: { ...entry.metadata } }); + } + for (const [key, pending] of this.pendingResponses.entries()) { + if (prefix && !key.startsWith(prefix)) continue; + if (!results.some((r) => r.key === key)) { + results.push({ key, metadata: { ...pending.metadata } }); + } + } + return results; + } + + // ─── Targeted deletion ───────────────────────────────────────────────────── + + async deleteKey(key: string): Promise { + const deletedLocal = this.localCache.del(key) > 0; + const deletedPending = this.pendingResponses.delete(key); + const deletedRedis = await this.redisDel(key); + if (deletedLocal || deletedPending || deletedRedis) { + this._evictions++; + return true; + } + return false; + } + + // ─── Global clear (admin only) ───────────────────────────────────────────── + + clear(): void { + const count = this.localCache.keys().length + this.pendingResponses.size; + this._evictions += count; + this.localCache.flushAll(); + this.pendingResponses.clear(); + // Note: Redis keys are prefixed with REDIS_PREFIX; a full Redis FLUSHDB is intentionally + // not issued here to avoid clearing unrelated keys. Use deleteKey() per-key when needed. + } + + // ─── Observability ───────────────────────────────────────────────────────── + + getMetrics(): IdempotencyMetrics { + return { + hits: this._hits, + conflicts: this._conflicts, + evictions: this._evictions, + activeKeys: this.localCache.keys().length, + pendingKeys: this.pendingResponses.size, + }; + } + + // ─── Retention cleanup ───────────────────────────────────────────────────── + + async pruneStaleKeys( + retentionMs: number, + dryRun = false, + ): Promise<{ pruned: number; localPruned: number; redisPruned: number }> { + const cutoff = Date.now() - retentionMs; + let localPruned = 0; + let redisPruned = 0; + + for (const key of this.localCache.keys()) { + const entry = this.localCache.get>(key); + if (!entry) continue; + const createdAt = Date.parse(entry.metadata.createdAt); + if (Number.isNaN(createdAt) || createdAt >= cutoff) continue; + if (!dryRun) { + this.localCache.del(key); + this._evictions++; + } + localPruned++; + } + + const r = this.redis; + if (r) { + let cursor = '0'; + do { + const [nextCursor, keys] = await r.scan(cursor, 'MATCH', `${REDIS_PREFIX}*`, 'COUNT', 100); + cursor = nextCursor; + for (const redisKey of keys) { + try { + const raw = await r.get(redisKey); + if (!raw) continue; + const entry = JSON.parse(raw) as StoredResponse; + const createdAt = Date.parse(entry.metadata?.createdAt ?? ''); + const ttl = await r.ttl(redisKey); + const isStale = (!Number.isNaN(createdAt) && createdAt < cutoff) || ttl === 0; + if (!isStale) continue; + if (!dryRun) { + await r.del(redisKey); + this._evictions++; + } + redisPruned++; + } catch { + if (!dryRun) { + await r.del(redisKey); + this._evictions++; + } + redisPruned++; + } + } + } while (cursor !== '0'); + } + + return { pruned: localPruned + redisPruned, localPruned, redisPruned }; + } +} + +// ─── Singleton ──────────────────────────────────────────────────────────────── + +export const idempotencyStore = new IdempotencyStore( + parseInt(process.env.IDEMPOTENCY_KEY_TTL_MS || '86400000', 10) +); + +// ─── Fingerprint helper ─────────────────────────────────────────────────────── + +export function getIdempotencyHashThreshold(): number { + return parseInt(process.env.IDEMPOTENCY_HASH_THRESHOLD_BYTES || '4096', 10); +} + +export function buildIdempotencyFingerprint(payload: unknown): string { + const stable = stableStringify(payload); + const byteLength = Buffer.byteLength(stable, 'utf-8'); + if (byteLength > getIdempotencyHashThreshold()) { + return `hashv1:${crypto.createHash('sha256').update(stable).digest('hex')}`; + } + return stable; +} + +function stableStringify(value: unknown): string { + if (value === null) return 'null'; + if (value instanceof Date) return JSON.stringify(value.toISOString()); + if (typeof value !== 'object') return JSON.stringify(value); + + if (Array.isArray(value)) { + return `[${value.map((item) => stableStringify(item)).join(',')}]`; + } + + const record = value as Record; + const keys = Object.keys(record).sort(); + const serialized = keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`); + return `{${serialized.join(',')}}`; +} diff --git a/backend/src/index.ts b/backend/src/index.ts index c0fd1750b..a62f485d7 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -5404,6 +5404,7 @@ app.use((req: Request, res: Response) => { status: 404, code: 'ROUTE_NOT_FOUND', message: `Cannot ${req.method} ${req.originalUrl}`, + path: req.originalUrl, details: { path: req.originalUrl }, retryable: false, }); diff --git a/backend/src/middleware/apiError.ts b/backend/src/middleware/apiError.ts index a59b60aa5..469fe5dd2 100644 --- a/backend/src/middleware/apiError.ts +++ b/backend/src/middleware/apiError.ts @@ -10,6 +10,19 @@ export interface ApiErrorOptions { retryable?: boolean; retryAfterSeconds?: number | null; error?: string; + /** + * Short, stable headline for the failure. Part of the published error + * contract (see `ErrorEnvelope` in openapi.json) alongside `message`. + */ + summary?: string; + /** + * Field-level failures, surfaced at the top level for clients that read + * `errors` directly. `details` carries the same payload for the generic + * envelope. + */ + errors?: unknown[]; + /** Request path, echoed on routing failures so clients can log it directly. */ + path?: string; } export function sendApiError( @@ -30,6 +43,9 @@ export function sendApiError( code: options.code, message: options.message, retryable: options.retryable ?? options.status >= 500, + ...(options.summary !== undefined ? { summary: options.summary } : {}), + ...(options.errors !== undefined ? { errors: options.errors } : {}), + ...(options.path !== undefined ? { path: options.path } : {}), ...(options.details !== undefined ? { details: options.details } : {}), ...(correlationId ? { correlationId } : {}), ...(traceId ? { traceId } : {}), diff --git a/backend/src/middleware/validate.ts b/backend/src/middleware/validate.ts index 76e48ecf9..69a2a3b5e 100644 --- a/backend/src/middleware/validate.ts +++ b/backend/src/middleware/validate.ts @@ -248,6 +248,8 @@ export function validate(schemas: ValidateTargets) { status: 400, code: 'VALIDATION_ERROR', message: formatZodError(issues), + summary: 'Request validation failed', + errors: details, details, retryable: false, }); diff --git a/backend/src/schemaSnapshot.ts b/backend/src/schemaSnapshot.ts index fa2fca475..19a5f8882 100644 --- a/backend/src/schemaSnapshot.ts +++ b/backend/src/schemaSnapshot.ts @@ -74,7 +74,7 @@ export function extractSchemaFromZod(zodSchema: z.ZodType): SchemaDefinitio // Handle object schemas if (zodSchema instanceof z.ZodObject) { - const shape = (zodSchema as z.ZodObject)._shape; + const shape = (zodSchema as z.ZodObject).shape; const properties: Record = {}; const required: string[] = []; diff --git a/backend/src/sessionAudit.ts b/backend/src/sessionAudit.ts index 295bb6cdc..ef94cc50a 100644 --- a/backend/src/sessionAudit.ts +++ b/backend/src/sessionAudit.ts @@ -59,7 +59,7 @@ export async function recordSessionEvent(entry: SessionAuditEntry): Promise { it('should reject invalid Idempotency-Key format', () => { const req = createMockRequest({ - get: (header: string) => (header === 'Idempotency-Key' ? 'invalid' : undefined), + get: ((header: string) => + header === 'Idempotency-Key' ? 'invalid' : undefined) as Request['get'], }); const res = createMockResponse(); const next = jest.fn(); @@ -187,7 +188,8 @@ describe('Idempotency', () => { it('should accept valid UUID format', () => { const validUuid = '550e8400-e29b-41d4-a716-446655440000'; const req = createMockRequest({ - get: (header: string) => (header === 'Idempotency-Key' ? validUuid : undefined), + get: ((header: string) => + header === 'Idempotency-Key' ? validUuid : undefined) as Request['get'], }); const res = createMockResponse(); const next = jest.fn(); @@ -202,7 +204,8 @@ describe('Idempotency', () => { it('should accept valid hex nonce', () => { const validHex = 'a'.repeat(32); const req = createMockRequest({ - get: (header: string) => (header === 'Idempotency-Key' ? validHex : undefined), + get: ((header: string) => + header === 'Idempotency-Key' ? validHex : undefined) as Request['get'], }); const res = createMockResponse(); const next = jest.fn(); diff --git a/backend/src/tests/tenantBoundary.test.ts b/backend/src/tests/tenantBoundary.test.ts index c7477b131..2fd8a7964 100644 --- a/backend/src/tests/tenantBoundary.test.ts +++ b/backend/src/tests/tenantBoundary.test.ts @@ -21,10 +21,10 @@ function createMockRequest(overrides?: Partial): Partial { authApiKeyRole: 'viewer', authApiKeyHash: 'hash123', ip: '127.0.0.1', - get: (header: string) => { + get: ((header: string) => { if (header === 'user-agent') return 'test-agent'; return undefined; - }, + }) as Request['get'], ...overrides, }; } diff --git a/backend/src/tracing.ts b/backend/src/tracing.ts index a7d784c66..fc29dc015 100644 --- a/backend/src/tracing.ts +++ b/backend/src/tracing.ts @@ -8,9 +8,9 @@ * OTEL_ENABLED - Set to "false" to disable (default: true) */ -import { NodeSDK } from '@opentelemetry/sdk-node'; +import { NodeSDK, type NodeSDKConfiguration } from '@opentelemetry/sdk-node'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; -import { resourceFromAttributes } from '@opentelemetry/resources'; +import * as otelResources from '@opentelemetry/resources'; import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; @@ -28,6 +28,26 @@ const SERVICE_NAME = process.env.OTEL_SERVICE_NAME || 'yieldvault-backend'; const OTLP_ENDPOINT = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318'; +/** + * Builds the SDK resource from a flat attribute bag. + * + * `@opentelemetry/resources` renamed its factory between the v1 and v2 lines + * (`new Resource(attributes)` → `resourceFromAttributes(attributes)`), and the + * two OpenTelemetry major lines coexist in this workspace's lockfile. Resolve + * whichever factory the installed version exposes so the SDK boots on both. + */ +function buildResource(attributes: Record): NodeSDKConfiguration['resource'] { + const resources = otelResources as unknown as { + resourceFromAttributes?: (attrs: Record) => unknown; + Resource: new (attrs: Record) => unknown; + }; + + if (typeof resources.resourceFromAttributes === 'function') { + return resources.resourceFromAttributes(attributes) as NodeSDKConfiguration['resource']; + } + return new resources.Resource(attributes) as NodeSDKConfiguration['resource']; +} + let sdk: NodeSDK | null = null; export function initTracing(): void { @@ -62,7 +82,7 @@ export function initTracing(): void { const exporter = new OTLPTraceExporter({ url: `${OTLP_ENDPOINT}/v1/traces` }); sdk = new NodeSDK({ - resource: resourceFromAttributes({ + resource: buildResource({ [ATTR_SERVICE_NAME]: SERVICE_NAME, [ATTR_SERVICE_VERSION]: process.env.npm_package_version || '1.0.0', }), diff --git a/backend/src/vaultEndpoints.ts b/backend/src/vaultEndpoints.ts index abbbdf8cb..0b58a694f 100644 --- a/backend/src/vaultEndpoints.ts +++ b/backend/src/vaultEndpoints.ts @@ -3,7 +3,7 @@ import { emailService } from './emailService'; import { logger } from './middleware/structuredLogging'; import { allowlistMiddleware } from './middleware/allowlist'; import { triggerCacheInvalidation, registerInvalidationHook } from './middleware/cache'; -import { depositsLimiter, depositsUserLimiter } from './rateLimiter'; +import { depositsLimiter, depositsUserLimiter, readsLimiter } from './rateLimiter'; import { cacheMiddleware } from './middleware/cache'; import { idempotencyStore, @@ -769,7 +769,7 @@ router.post('/strategy', depositsLimiter, requireFlag('strategy-selection'), val }); }); - res.status(200).json({ message: 'Strategy selection endpoint (v2 preview)' }); + return res.status(200).json({ message: 'Strategy selection endpoint (v2 preview)' }); }); /** @@ -900,7 +900,7 @@ router.get('/receipts', readsLimiter, async (req: Request, res: Response) => { const transactions = await prisma.transaction.findMany({ where, - orderBy: { createdAt: 'desc' }, + orderBy: { timestamp: 'desc' }, take: limit + 1, ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}), }); @@ -916,7 +916,7 @@ router.get('/receipts', readsLimiter, async (req: Request, res: Response) => { status: tx.status, walletAddress: tx.user, explorerUrl: `${EXPLORER_BASE_URL}/${tx.id}`, - timestamp: tx.createdAt.toISOString(), + timestamp: tx.timestamp.toISOString(), })); res.status(200).json({ From f2add84fd8d06b6fb6f7b561822b637aeca4ee83 Mon Sep 17 00:00:00 2001 From: ToryMic Date: Tue, 29 Sep 2026 23:27:33 -0400 Subject: [PATCH 2/3] fix(backend): enforce a hard max limit on GET /api/v1/vaults pagination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An unauthenticated caller could pass `?limit=100000` and have it forwarded straight into Prisma's `take`, forcing the API to materialise 100k vault rows and OOM-kill the 512MB container. Chosen behaviour (documented in the route, the spec and the tests): reject rather than silently clamp. A caller asking for 100000 rows has a bug or is probing, and quietly returning 50 hides that behind a response that looks like a complete page. So `limit > 50` fails fast with `400` and `code: 'LIMIT_EXCEEDED'`, and the body repeats the ceiling. `page` is the opposite case — a benign mistake — so it is clamped into 1..1000 and the effective value is echoed in the pagination envelope. - new `middleware/paginationGuard.ts`: `DEFAULT_PAGE_SIZE` (20), `MAX_PAGE_SIZE` (50), `MAX_PAGE` (1000), `resolvePagination()` and the `enforcePaginationLimits()` middleware - new `routes/vaults.ts`: `GET /api/v1/vaults`, rate limited, reads `limit + 1` rows for the lookahead, and never exposes `tenantId` - `parsePaginationQuery` gains `maxPage` and clamps out-of-range pages; `PaginationQuerySchema.page` accepts a signed integer so those requests reach the clamp instead of being rejected - OpenAPI: reusable `pageSize`/`pageNumber` parameters carrying the ceilings, plus a `GET /api/v1/vaults` path documenting the LIMIT_EXCEEDED response; `openapi.json` regenerated - `GET /api/v1/vaults` joins the committed contract snapshots, so the response shape is now covered by the backward-compatibility check - tests spy on the Prisma client to prove no read is ever issued with a `take` above the ceiling, that an oversized limit is rejected before any query runs, and that the committed `openapi.json` still matches --- backend/openapi.json | 215 ++++++++++- backend/prisma/dev.db | Bin 1052672 -> 1101824 bytes .../schema-snapshots/get-_api_v1_vaults.json | 89 +++++ .../src/__tests__/vaultsListLimits.test.ts | 333 ++++++++++++++++++ backend/src/apiContractSnapshots.ts | 24 ++ backend/src/index.ts | 2 + backend/src/middleware/paginationGuard.ts | 160 +++++++++ backend/src/pagination.ts | 11 +- backend/src/routes/vaults.ts | 156 ++++++++ backend/src/swagger.ts | 188 +++++++++- backend/src/types/express.d.ts | 7 + backend/src/types/validation.ts | 5 +- 12 files changed, 1173 insertions(+), 17 deletions(-) create mode 100644 backend/schema-snapshots/get-_api_v1_vaults.json create mode 100644 backend/src/__tests__/vaultsListLimits.test.ts create mode 100644 backend/src/middleware/paginationGuard.ts create mode 100644 backend/src/routes/vaults.ts diff --git a/backend/openapi.json b/backend/openapi.json index e84421cb3..ce784a18e 100644 --- a/backend/openapi.json +++ b/backend/openapi.json @@ -54,6 +54,31 @@ "type": "string", "format": "uuid" } + }, + "pageSize": { + "name": "limit", + "in": "query", + "required": false, + "description": "Maximum number of items to return. Hard ceiling is 50 (default 20). A larger value is **rejected**, not clamped, with `400` and `code: \"LIMIT_EXCEEDED\"` — the response body repeats the ceiling so clients can self-correct. Use `page` (clamped to 1..1000) to walk the rest.", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 20, + "example": 20 + } + }, + "pageNumber": { + "name": "page", + "in": "query", + "required": false, + "description": "1-based page number for offset pagination. Values outside 1..1000 are clamped rather than rejected, and the effective page is echoed back in `pagination.currentPage`.", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "default": 1 + } } }, "schemas": { @@ -106,6 +131,10 @@ "count": { "type": "integer" }, + "limit": { + "type": "integer", + "maximum": 50 + }, "total": { "type": "integer" }, @@ -118,7 +147,9 @@ "nullable": true }, "currentPage": { - "type": "integer" + "type": "integer", + "minimum": 1, + "maximum": 1000 }, "totalPages": { "type": "integer" @@ -131,6 +162,62 @@ } } }, + "Vault": { + "type": "object", + "required": [ + "id", + "aum", + "tvlUsd", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "aum": { + "type": "number", + "example": 0 + }, + "tvlUsd": { + "type": "string", + "nullable": true, + "example": "1250000.00" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "VaultListResponse": { + "type": "object", + "required": [ + "data", + "pagination", + "timestamp" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Vault" + } + }, + "pagination": { + "$ref": "#/components/schemas/PaginationMeta" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + } + }, "VaultSummary": { "type": "object", "properties": { @@ -143,15 +230,8 @@ "example": 0 }, "apy": { - "type": ["number", "null"], - "example": 8.45, - "description": "Annualised APY as a decimal percentage. null when the vault has insufficient price history (e.g. zero shares or fewer than 2 snapshots)." - }, - "apyStatus": { - "type": "string", - "enum": ["ok", "insufficient_data"], - "example": "ok", - "description": "ok when apy is a valid number; insufficient_data when apy is null." + "type": "number", + "example": 0 }, "timestamp": { "type": "string", @@ -363,11 +443,21 @@ "timestamp": "2024-01-01T00:00:00.000Z", "uptime": 123.4, "environment": "production", + "lastIndexedLedger": 12345678, "checks": { "api": "up", "cache": "up", "stellarRpc": "up", + "databasePrimary": "up", + "databaseReplica": "up", + "prisma": "up", + "jobs": "up", "indexer": "up" + }, + "sorobanCircuitBreaker": { + "state": "closed", + "failures": 0, + "retryAfterMs": 0 } } } @@ -522,16 +612,26 @@ "application/json": { "schema": { "type": "object", - "required": ["apy", "apyStatus", "timestamp"], + "required": [ + "apy", + "apyStatus", + "timestamp" + ], "properties": { "apy": { - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "example": 8.45, "description": "Annualised APY as a decimal percentage. null when insufficient data." }, "apyStatus": { "type": "string", - "enum": ["ok", "insufficient_data"], + "enum": [ + "ok", + "insufficient_data" + ], "example": "ok" }, "timestamp": { @@ -546,6 +646,95 @@ } } }, + "/api/v1/vaults": { + "get": { + "tags": [ + "Vault" + ], + "summary": "List vaults", + "description": "Paginated listing of active (non-deleted) vaults. `limit` is capped at 50 and `page` at 1000; exceeding `limit` fails fast with `400` / `LIMIT_EXCEEDED` instead of silently clamping, so a client that asks for an oversized page is never handed a response that looks complete. Rate limited: global 100 req / 15 min per IP; authenticated APIs 30 req / min per key. `429` responses follow the standard error envelope with a `retryAfter` detail.", + "parameters": [ + { + "$ref": "#/components/parameters/pageSize" + }, + { + "$ref": "#/components/parameters/pageNumber" + }, + { + "$ref": "#/components/parameters/correlationId" + } + ], + "responses": { + "200": { + "description": "One page of vaults", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VaultListResponse" + }, + "example": { + "data": [ + { + "id": "0f1b3a2c-6d4e-4a1b-9f0e-2c5d7e8b9a01", + "aum": 1250000, + "tvlUsd": "1250000.00", + "createdAt": "2026-01-01T00:00:00.000Z", + "updatedAt": "2026-01-02T00:00:00.000Z" + } + ], + "pagination": { + "count": 1, + "limit": 20, + "total": 1, + "nextCursor": null, + "prevCursor": null, + "currentPage": 1, + "totalPages": 1, + "hasNextPage": false, + "hasPrevPage": false + }, + "timestamp": "2026-01-01T00:00:00.000Z" + } + } + } + }, + "400": { + "description": "`limit` above the published ceiling", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + }, + "example": { + "error": "Bad Request", + "status": 400, + "code": "LIMIT_EXCEEDED", + "message": "limit must not exceed 50 (received 100000).", + "retryable": false, + "details": { + "field": "limit", + "requested": 100000, + "maxLimit": 50, + "defaultLimit": 20, + "maxPage": 1000 + } + } + } + } + }, + "429": { + "description": "Rate limited", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, "/api/v1/vault/deposits": { "post": { "tags": [ diff --git a/backend/prisma/dev.db b/backend/prisma/dev.db index a319596bdb09c10ad9b07c84f539bce86bb94bc3..4bf95d871e5089936ab03a0491db042ab986a83a 100644 GIT binary patch delta 82148 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 WXAch~;uQ;36XMDv;u2llCI1&bZ-G<* delta 36072 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≀eZnqXygo51iJa*{ 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 uYIWkK(i62YTvmQ!Z>8ZSv{$b_RGd { + const existing = await prisma.vault.count(); + if (existing >= count) return; + + for (let i = existing; i < count; i += 1) { + await prismaClient.vault.create({ + data: { + tenantId: 'tenant-1430', + aum: 1000 + i, + tvlUsd: `${1000 + i}.00`, + }, + }); + } +} + +describe('Issue #1430 — GET /api/v1/vaults pagination limits', () => { + beforeAll(async () => { + await seedVaults(55); + }); + + afterAll(async () => { + await prisma.vault.deleteMany({ where: { tenantId: 'tenant-1430' } }); + }); + + // ─── Defaults and accepted values ────────────────────────────────────────── + + it('defaults to 20 items per page', async () => { + const res = await request(app).get(VAULTS_PATH); + + expect(res.status).toBe(200); + expect(res.body.pagination.limit).toBe(DEFAULT_PAGE_SIZE); + expect(res.body.data.length).toBeLessThanOrEqual(DEFAULT_PAGE_SIZE); + }); + + it('honours the maximum permitted limit', async () => { + const res = await request(app).get(`${VAULTS_PATH}?limit=${MAX_PAGE_SIZE}`); + + expect(res.status).toBe(200); + expect(res.body.pagination.limit).toBe(MAX_PAGE_SIZE); + }); + + it('falls back to the default when limit is not a positive integer', async () => { + for (const raw of ['abc', '0', '-5', '1.5', '']) { + const res = await request(app).get(`${VAULTS_PATH}?limit=${encodeURIComponent(raw)}`); + expect(res.status).toBe(200); + expect(res.body.pagination.limit).toBe(DEFAULT_PAGE_SIZE); + } + }); + + it('rejects an oversized limit with 400 LIMIT_EXCEEDED', async () => { + const res = await request(app).get(`${VAULTS_PATH}?limit=100000`); + + expect(res.status).toBe(400); + expect(res.body.code).toBe(LIMIT_EXCEEDED_CODE); + expect(res.body.error).toBe('Bad Request'); + expect(res.body.retryable).toBe(false); + expect(res.body.message).toContain(String(MAX_PAGE_SIZE)); + expect(res.body.details).toMatchObject({ + field: 'limit', + requested: 100000, + maxLimit: MAX_PAGE_SIZE, + }); + }); + + it('rejects every limit above the ceiling, including the boundary + 1', async () => { + const res = await request(app).get(`${VAULTS_PATH}?limit=${MAX_PAGE_SIZE + 1}`); + + expect(res.status).toBe(400); + expect(res.body.code).toBe(LIMIT_EXCEEDED_CODE); + }); + + it('never issues a Prisma read with take above the ceiling', async () => { + const findMany = jest.spyOn(prismaClient.vault, 'findMany'); + const count = jest.spyOn(prismaClient.vault, 'count'); + + try { + const requests = [ + `${VAULTS_PATH}?limit=100000`, + `${VAULTS_PATH}?limit=999999`, + `${VAULTS_PATH}?limit=100000&page=3`, + `${VAULTS_PATH}?limit=51`, + `${VAULTS_PATH}`, + `${VAULTS_PATH}?limit=50`, + `${VAULTS_PATH}?limit=1`, + ]; + + for (const target of requests) { + await request(app).get(target); + } + + const takes = findMany.mock.calls.map(([args]) => (args as { take?: number }).take); + expect(takes.length).toBeGreaterThan(0); + for (const take of takes) { + expect(typeof take).toBe('number'); + expect(take!).toBeLessThanOrEqual(MAX_ALLOWED_TAKE); + } + } finally { + findMany.mockRestore(); + count.mockRestore(); + } + }); + + it('rejects the oversized limit before touching the database at all', async () => { + const findMany = jest.spyOn(prismaClient.vault, 'findMany'); + const count = jest.spyOn(prismaClient.vault, 'count'); + + try { + const res = await request(app).get(`${VAULTS_PATH}?limit=100000`); + + expect(res.status).toBe(400); + expect(findMany).not.toHaveBeenCalled(); + expect(count).not.toHaveBeenCalled(); + } finally { + findMany.mockRestore(); + count.mockRestore(); + } + }); + + // ─── Page clamping ───────────────────────────────────────────────────────── + + it('clamps page into 1..1000 instead of rejecting it', async () => { + const high = await request(app).get(`${VAULTS_PATH}?page=100000`); + expect(high.status).toBe(200); + expect(high.body.pagination.currentPage).toBe(MAX_PAGE); + + const low = await request(app).get(`${VAULTS_PATH}?page=-1`); + expect(low.status).toBe(200); + expect(low.body.pagination.currentPage).toBe(1); + }); + + it('caps the database offset derived from page', async () => { + const findMany = jest.spyOn(prismaClient.vault, 'findMany'); + + try { + await request(app).get(`${VAULTS_PATH}?limit=50&page=100000`); + const [args] = findMany.mock.calls[0] as [{ take: number; skip: number }]; + + expect(args.take).toBeLessThanOrEqual(MAX_ALLOWED_TAKE); + expect(args.skip).toBe((MAX_PAGE - 1) * 50); + } finally { + findMany.mockRestore(); + } + }); + + // ─── Response contract ───────────────────────────────────────────────────── + + it('matches the committed vault-list contract snapshot', async () => { + const res = await request(app).get(`${VAULTS_PATH}?limit=5`); + + expect(res.status).toBe(200); + const parsed = VaultListResponseSchema.safeParse(res.body); + expect(parsed.success).toBe(true); + expect(validateResponseAgainstSchema('GET /api/v1/vaults', res.body).success).toBe(true); + }); + + it('never leaks tenantId', async () => { + const res = await request(app).get(VAULTS_PATH); + + for (const vault of res.body.data as Array>) { + expect(vault).not.toHaveProperty('tenantId'); + } + }); + + // ─── Guard unit behaviour ────────────────────────────────────────────────── + + describe('resolvePagination', () => { + it('applies the documented defaults', () => { + expect(resolvePagination({})).toEqual({ limit: DEFAULT_PAGE_SIZE, page: 1 }); + }); + + it('throws PaginationLimitError above the ceiling', () => { + expect(() => resolvePagination({ limit: '100000' })).toThrow(PaginationLimitError); + }); + + it('honours per-route overrides', () => { + expect(resolvePagination({ limit: '75' }, { maxLimit: 100 })).toEqual({ + limit: 75, + page: 1, + }); + expect(() => resolvePagination({ limit: '75' }, { maxLimit: 50 })).toThrow( + PaginationLimitError, + ); + }); + + it('clamps page and ignores non-integer input', () => { + expect(resolvePagination({ page: '0' }).page).toBe(1); + expect(resolvePagination({ page: 'nope' }).page).toBe(1); + expect(resolvePagination({ page: '100000' }).page).toBe(MAX_PAGE); + expect(resolvePagination({ page: '3' }).page).toBe(3); + }); + + it('rejects repeated query parameters rather than guessing', () => { + expect(resolvePagination({ limit: ['10', '20'] }).limit).toBe(DEFAULT_PAGE_SIZE); + }); + }); + + describe('enforcePaginationLimits middleware', () => { + function runMiddleware(query: Record) { + const req = { query } as never; + const json = jest.fn(); + const res = { + status: jest.fn(() => ({ json })), + setHeader: jest.fn(), + } as never; + const next = jest.fn(); + + enforcePaginationLimits()(req, res, next); + return { req: req as { resolvedPagination?: { limit: number; page: number } }, json, next }; + } + + it('attaches the resolved pagination and continues', () => { + const { req, json, next } = runMiddleware({ limit: '25', page: '4' }); + + expect(next).toHaveBeenCalledTimes(1); + expect(json).not.toHaveBeenCalled(); + expect(req.resolvedPagination).toEqual({ limit: 25, page: 4 }); + }); + + it('sends 400 LIMIT_EXCEEDED and does not continue', () => { + const { json, next } = runMiddleware({ limit: '100000' }); + + expect(next).not.toHaveBeenCalled(); + expect(json).toHaveBeenCalledWith( + expect.objectContaining({ status: 400, code: LIMIT_EXCEEDED_CODE }), + ); + }); + }); + + describe('shared parsePaginationQuery', () => { + function parse(query: Record) { + return parsePaginationQuery({ query } as never); + } + + it('clamps page to maxPage but keeps the default limit of 20', () => { + expect(DEFAULT_PAGINATION_CONFIG.defaultLimit).toBe(20); + expect(parse({})).toMatchObject({ limit: 20 }); + expect(parse({ page: '1000000' })).toMatchObject({ page: MAX_PAGE }); + expect(parse({ page: '-1' })).toMatchObject({ page: 1 }); + }); + }); + + // ─── OpenAPI contract ────────────────────────────────────────────────────── + + describe('OpenAPI documentation of the pagination ceiling', () => { + const spec = specs as unknown as { + paths: Record> } }>; + components: { + parameters: Record< + string, + { name: string; schema: { maximum?: number; minimum?: number; default?: number } } + >; + }; + }; + + it('publishes maxLimit and the page ceiling as reusable parameters', () => { + expect(spec.components.parameters.pageSize).toMatchObject({ + name: 'limit', + schema: { minimum: 1, maximum: MAX_PAGE_SIZE, default: DEFAULT_PAGE_SIZE }, + }); + expect(spec.components.parameters.pageNumber).toMatchObject({ + name: 'page', + schema: { minimum: 1, maximum: MAX_PAGE }, + }); + }); + + it('references those parameters from GET /api/v1/vaults', () => { + const parameters = spec.paths[VAULTS_PATH]?.get?.parameters ?? []; + const refs = parameters.map((parameter) => parameter.$ref); + + expect(refs).toContain('#/components/parameters/pageSize'); + expect(refs).toContain('#/components/parameters/pageNumber'); + }); + + it('documents the LIMIT_EXCEEDED failure mode', () => { + const specWithResponses = specs as unknown as { + paths: Record< + string, + { get?: { responses?: Record }> } } + >; + }; + const example = + specWithResponses.paths[VAULTS_PATH]?.get?.responses?.['400']?.content?.[ + 'application/json' + ]?.example; + + expect(example?.code).toBe(LIMIT_EXCEEDED_CODE); + }); + + it('keeps the committed openapi.json in sync with the spec source', () => { + const committed = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', '..', 'openapi.json'), 'utf8'), + ) as typeof specs; + + expect(JSON.stringify(committed)).toBe(JSON.stringify(specs)); + }); + }); +}); diff --git a/backend/src/apiContractSnapshots.ts b/backend/src/apiContractSnapshots.ts index 461460bef..cd93e22a6 100644 --- a/backend/src/apiContractSnapshots.ts +++ b/backend/src/apiContractSnapshots.ts @@ -17,6 +17,7 @@ export const CRITICAL_ENDPOINTS = [ 'GET /ready', 'GET /api/v1/vault/summary', 'GET /api/v1/transactions', + 'GET /api/v1/vaults', ] as const; export type CriticalEndpoint = (typeof CRITICAL_ENDPOINTS)[number]; @@ -110,11 +111,34 @@ export const TransactionsListResponseSchema = z }) .strict(); +/** + * Public projection of a vault row. `tenantId` is intentionally absent — the + * list route never exposes it (Issue #1430). + */ +export const VaultItemSchema = z + .object({ + id: z.string(), + aum: z.number(), + tvlUsd: z.string().nullable(), + createdAt: z.string(), + updatedAt: z.string(), + }) + .strict(); + +export const VaultListResponseSchema = z + .object({ + data: z.array(VaultItemSchema), + pagination: PaginationMetaSchema, + timestamp: z.string(), + }) + .strict(); + export const ENDPOINT_SCHEMAS: Record = { 'GET /health': HealthResponseSchema, 'GET /ready': ReadyResponseSchema, 'GET /api/v1/vault/summary': VaultSummaryResponseSchema, 'GET /api/v1/transactions': TransactionsListResponseSchema, + 'GET /api/v1/vaults': VaultListResponseSchema, }; export function endpointToFilename(endpoint: CriticalEndpoint): string { diff --git a/backend/src/index.ts b/backend/src/index.ts index a62f485d7..8376adc08 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -103,6 +103,7 @@ import { createVersionDiscoveryRouter } from './routes/apiVersions'; import { GracefulShutdownHandler } from './gracefulShutdown'; import { db } from './database'; import vaultRouter from './vaultEndpoints'; +import vaultsListRouter from './routes/vaults'; import walletAliasRouter from './walletAliasEndpoints'; import { walletAliasMappingService } from './walletAliasService'; import transactionRouter from './transactionEndpoints'; @@ -911,6 +912,7 @@ app.use('/api', createVersionDiscoveryRouter()); // Mount routers under /api/v1 apiV1.use('/vault', vaultRouter); +apiV1.use('/vaults', vaultsListRouter); apiV1.use('/wallet-aliases', walletAliasRouter); apiV1.use('/referrals', referralRouter); apiV1.use('/transactions', transactionRouter); diff --git a/backend/src/middleware/paginationGuard.ts b/backend/src/middleware/paginationGuard.ts new file mode 100644 index 000000000..ab626d5a1 --- /dev/null +++ b/backend/src/middleware/paginationGuard.ts @@ -0,0 +1,160 @@ +/** + * @file paginationGuard.ts + * Hard caps for caller-controlled pagination on database-backed list routes. + * + * Issue #1430: `GET /vaults` forwarded `req.query.limit` straight into + * Prisma's `take`, so an unauthenticated caller could ask for + * `?limit=100000` and force the API to materialise 100k rows plus their + * relations — a memory spike large enough to OOM-kill the 512 MB + * container. + * + * Chosen behaviour: **reject, do not silently clamp.** A caller that asks + * for `limit=100000` has a bug (or is probing), and quietly returning 50 + * rows hides that behind a paginated response that looks complete. So an + * over-max `limit` fails fast with `400` and `code: 'LIMIT_EXCEEDED'`, and + * the response body names the ceiling. `page` is different: an out-of-range + * page number is a benign mistake, so it is clamped to `1..MAX_PAGE` and + * the effective value is echoed back in the pagination envelope. + * + * Anything that turns a request parameter into a Prisma `take`/`skip` + * should sit behind `enforcePaginationLimits()`. + */ + +import type { Request, Response, NextFunction } from 'express'; +import { sendApiError } from './apiError'; + +/** Page size used when the caller does not ask for one. */ +export const DEFAULT_PAGE_SIZE = 20; + +/** + * Hard ceiling on rows read per request. Sized so the largest permitted + * page stays comfortably inside the API container's memory budget. + */ +export const MAX_PAGE_SIZE = 50; + +/** + * Hard ceiling on offset pagination depth. `page * limit` is what reaches + * Prisma as `skip`, so an unbounded page number is an unbounded database + * offset scan. + */ +export const MAX_PAGE = 1000; + +export interface PaginationLimitOptions { + /** Rows per page when `limit` is absent. Defaults to {@link DEFAULT_PAGE_SIZE}. */ + defaultLimit?: number; + /** Largest permitted `limit`. Defaults to {@link MAX_PAGE_SIZE}. */ + maxLimit?: number; + /** Largest permitted `page`. Defaults to {@link MAX_PAGE}. */ + maxPage?: number; +} + +export interface ResolvedPagination { + limit: number; + page: number; +} + +/** Error code returned for a `limit` above the configured ceiling. */ +export const LIMIT_EXCEEDED_CODE = 'LIMIT_EXCEEDED'; + +/** + * Parses a raw query-string value that is expected to hold a base-10 + * integer. Returns `null` for anything that is not an exact integer so + * callers can tell "absent" from "garbage". + */ +function parseIntegerQueryValue(raw: unknown): number | null { + if (raw === undefined || raw === null) return null; + if (Array.isArray(raw)) return null; + if (typeof raw !== 'string') return null; + if (!/^-?\d+$/.test(raw.trim())) return null; + const parsed = Number.parseInt(raw.trim(), 10); + return Number.isSafeInteger(parsed) ? parsed : null; +} + +/** + * Resolves the effective `limit` and `page` for a request. + * + * Throws {@link PaginationLimitError} when `limit` exceeds the ceiling, so + * the middleware can turn it into a `400` instead of running a query. + */ +export function resolvePagination( + query: Record, + options: PaginationLimitOptions = {} +): ResolvedPagination { + const defaultLimit = options.defaultLimit ?? DEFAULT_PAGE_SIZE; + const maxLimit = options.maxLimit ?? MAX_PAGE_SIZE; + const maxPage = options.maxPage ?? MAX_PAGE; + + const rawLimit = parseIntegerQueryValue(query.limit); + if (rawLimit !== null && rawLimit > maxLimit) { + throw new PaginationLimitError(rawLimit, maxLimit); + } + + const limit = + rawLimit !== null && rawLimit > 0 ? Math.min(rawLimit, maxLimit) : defaultLimit; + + const rawPage = parseIntegerQueryValue(query.page); + const page = + rawPage === null || rawPage < 1 ? 1 : Math.min(rawPage, maxPage); + + return { limit, page }; +} + +/** Thrown when a caller asks for more rows per page than the route allows. */ +export class PaginationLimitError extends Error { + readonly requested: number; + readonly maxLimit: number; + + constructor(requested: number, maxLimit: number) { + super( + `limit must not exceed ${maxLimit} (received ${requested}). ` + + 'Request at most ' + + `${maxLimit} items per page and use the page/cursor parameters to walk the rest of the collection.`, + ); + this.name = 'PaginationLimitError'; + this.requested = requested; + this.maxLimit = maxLimit; + } +} + +/** + * Express middleware that rejects an over-max `limit` with + * `400` / `code: 'LIMIT_EXCEEDED'` before any database work happens, and + * pins the sanitised `limit`/`page` onto `req.resolvedPagination` for the + * handler. + */ +export function enforcePaginationLimits(options: PaginationLimitOptions = {}) { + const defaultLimit = options.defaultLimit ?? DEFAULT_PAGE_SIZE; + const maxLimit = options.maxLimit ?? MAX_PAGE_SIZE; + const maxPage = options.maxPage ?? MAX_PAGE; + + return (req: Request, res: Response, next: NextFunction): void => { + try { + const resolved = resolvePagination(req.query as Record, { + defaultLimit, + maxLimit, + maxPage, + }); + + req.resolvedPagination = resolved; + next(); + } catch (err) { + if (err instanceof PaginationLimitError) { + sendApiError(req, res, { + status: 400, + code: LIMIT_EXCEEDED_CODE, + message: err.message, + retryable: false, + details: { + field: 'limit', + requested: err.requested, + maxLimit: err.maxLimit, + defaultLimit, + maxPage, + }, + }); + return; + } + next(err); + } + }; +} diff --git a/backend/src/pagination.ts b/backend/src/pagination.ts index da4d1b0ee..71f3e7865 100644 --- a/backend/src/pagination.ts +++ b/backend/src/pagination.ts @@ -76,6 +76,12 @@ export interface PaginationConfig { defaultLimit: number; /** Maximum allowed items per page. */ maxLimit: number; + /** + * Highest page number a caller may request (Issue #1430). The effective + * offset handed to the database is `(page - 1) * limit`, so an unbounded + * page number turns into an unbounded `skip` scan. + */ + maxPage: number; /** Whether to include total count in response. */ includeTotal: boolean; /** Default sort field. */ @@ -89,6 +95,7 @@ export interface PaginationConfig { export const DEFAULT_PAGINATION_CONFIG: PaginationConfig = { defaultLimit: 20, maxLimit: 100, + maxPage: 1000, includeTotal: true, defaultSortOrder: 'desc', }; @@ -123,11 +130,11 @@ export function parsePaginationQuery( query.cursor = req.query.cursor; } - // Parse page (1-based) + // Parse page (1-based, clamped to 1..maxPage — Issue #1430) if (req.query.page !== undefined) { const page = parseInt(req.query.page as string, 10); if (!isNaN(page) && page > 0) { - query.page = page; + query.page = Math.min(page, mergedConfig.maxPage); } else { query.page = 1; } diff --git a/backend/src/routes/vaults.ts b/backend/src/routes/vaults.ts new file mode 100644 index 000000000..fb9c376f4 --- /dev/null +++ b/backend/src/routes/vaults.ts @@ -0,0 +1,156 @@ +/** + * @file routes/vaults.ts + * `GET /api/v1/vaults` — paginated, public listing of active vaults. + * + * Pagination limits (Issue #1430) + * ------------------------------- + * This is an unauthenticated read that maps a caller-supplied `limit` + * straight onto Prisma's `take`, so it is exactly the shape of request that + * let `?limit=100000` OOM the API container. The route therefore sits behind + * `enforcePaginationLimits()`, which: + * + * - defaults `limit` to 20, + * - rejects `limit > 50` with `400` / `code: 'LIMIT_EXCEEDED'` *before* + * any database call (rejecting rather than clamping is deliberate: a + * silent clamp hides a broken or probing client behind a response that + * looks like a complete page), + * - clamps `page` into `1..1000` because the effective offset handed to + * the database is `(page - 1) * limit`. + * + * `take` is always `limit + 1` — one extra row is fetched to learn whether a + * next page exists, so the largest possible read is `MAX_PAGE_SIZE + 1`. + */ + +import { Router, Request, Response } from 'express'; +import { readsLimiter } from '../rateLimiter'; +import { createTimeoutFor } from '../middleware/timeoutMiddleware'; +import { + enforcePaginationLimits, + DEFAULT_PAGE_SIZE, + MAX_PAGE_SIZE, +} from '../middleware/paginationGuard'; +import { + createPaginatedResponse, + createPaginationEnvelope, + encodeCursor, + type PaginatedResponse, +} from '../pagination'; +import { getPrismaClient } from '../prismaClient'; +import { withSpan } from '../tracing'; +import { logger } from '../middleware/structuredLogging'; + +const router = Router(); + +/** Public projection of a vault row. `tenantId` is never exposed. */ +export interface VaultListItem { + id: string; + aum: number; + tvlUsd: string | null; + createdAt: string; + updatedAt: string; +} + +interface VaultRow { + id: string; + aum: number; + tvlUsd: string | null; + createdAt: Date; + updatedAt: Date; +} + +function toVaultListItem(row: VaultRow): VaultListItem { + return { + id: row.id, + aum: row.aum, + tvlUsd: row.tvlUsd, + createdAt: row.createdAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + }; +} + +/** + * Reads one page of active vaults. + * + * Exported separately from the route so unit tests can exercise the query + * construction without an HTTP round trip. + */ +export async function buildVaultsResponse( + limit: number, + page: number +): Promise> { + const prisma = getPrismaClient(); + const take = Math.min(limit, MAX_PAGE_SIZE) + 1; + const skip = (Math.max(1, page) - 1) * limit; + + const where = { deletedAt: null }; + + const [total, rows] = await Promise.all([ + prisma.vault.count({ where }), + prisma.vault.findMany({ + where, + // `id` breaks ties so pages stay stable when `createdAt` collides. + orderBy: [{ createdAt: 'desc' }, { id: 'asc' }], + take, + skip, + }), + ]); + + const hasNextPage = rows.length > limit; + const pageRows = hasNextPage ? rows.slice(0, limit) : rows; + const data = pageRows.map((row) => toVaultListItem(row as unknown as VaultRow)); + + const pagination = createPaginationEnvelope({ + count: data.length, + limit, + total, + currentPage: page, + totalPages: Math.max(1, Math.ceil(total / limit)), + hasNextPage, + hasPrevPage: page > 1, + nextCursor: + hasNextPage && data.length > 0 ? encodeCursor(data[data.length - 1].id) : null, + }); + + return createPaginatedResponse(data, pagination); +} + +/** + * GET /api/v1/vaults + * + * Query parameters: + * - `limit`: items per page, 1..50 (default 20). `> 50` → 400 LIMIT_EXCEEDED. + * - `page`: 1-based page number, clamped to 1..1000. + */ +router.get( + '/', + readsLimiter, + enforcePaginationLimits({ defaultLimit: DEFAULT_PAGE_SIZE, maxLimit: MAX_PAGE_SIZE }), + createTimeoutFor.read(), + async (req: Request, res: Response) => { + const { limit, page } = req.resolvedPagination ?? { + limit: DEFAULT_PAGE_SIZE, + page: 1, + }; + + return withSpan('vaults.list', async (span) => { + span.setAttributes({ 'vaults.limit': limit, 'vaults.page': page }); + + try { + const response = await buildVaultsResponse(limit, page); + res.status(200).json(response); + } catch (error) { + logger.log('error', 'Failed to list vaults', { + error: error instanceof Error ? error.message : String(error), + }); + res.status(500).json({ + error: 'Internal Server Error', + status: 500, + code: 'VAULTS_LIST_FAILED', + message: 'Failed to fetch vaults', + }); + } + }); + }, +); + +export default router; diff --git a/backend/src/swagger.ts b/backend/src/swagger.ts index 684b510bc..5e58d4589 100644 --- a/backend/src/swagger.ts +++ b/backend/src/swagger.ts @@ -1,6 +1,13 @@ import type { Express } from 'express'; import swaggerJsdoc from 'swagger-jsdoc'; import swaggerUi from 'swagger-ui-express'; +import { + DEFAULT_PAGE_SIZE, + MAX_PAGE as VAULT_LIST_MAX_PAGE, + MAX_PAGE_SIZE as VAULT_LIST_MAX_LIMIT, +} from './middleware/paginationGuard'; + +const VAULT_LIST_DEFAULT_LIMIT = DEFAULT_PAGE_SIZE; /** * OpenAPI 3.1 definition for the YieldVault Stellar RWA backend. @@ -11,6 +18,7 @@ import swaggerUi from 'swagger-ui-express'; * It documents: * - authentication (JWT bearer + API key) * - rate limiting behaviour + * - pagination ceilings for database-backed list routes (Issue #1430) * - request/response examples for every public + admin endpoint */ const RATE_LIMIT_NOTE = @@ -76,6 +84,40 @@ const options: swaggerJsdoc.Options = { 'and echoes it back in the response header for distributed tracing.', schema: { type: 'string', format: 'uuid' }, }, + /** + * `limit` for every database-backed list route that enforces a hard + * ceiling (Issue #1430). Documented once here and `$ref`-ed from the + * paths below so the published contract, the implementation constants + * and the snapshot test can never drift apart silently. + */ + pageSize: { + name: 'limit', + in: 'query', + required: false, + description: + 'Maximum number of items to return. ' + + `Hard ceiling is ${VAULT_LIST_MAX_LIMIT} (default ${VAULT_LIST_DEFAULT_LIMIT}). ` + + 'A larger value is **rejected**, not clamped, with `400` and ' + + '`code: "LIMIT_EXCEEDED"` — the response body repeats the ceiling so ' + + 'clients can self-correct. Use `page` (clamped to 1..1000) to walk the rest.', + schema: { + type: 'integer', + minimum: 1, + maximum: VAULT_LIST_MAX_LIMIT, + default: VAULT_LIST_DEFAULT_LIMIT, + example: VAULT_LIST_DEFAULT_LIMIT, + }, + }, + pageNumber: { + name: 'page', + in: 'query', + required: false, + description: + '1-based page number for offset pagination. Values outside 1..1000 are ' + + 'clamped rather than rejected, and the effective page is echoed back in ' + + '`pagination.currentPage`.', + schema: { type: 'integer', minimum: 1, maximum: VAULT_LIST_MAX_PAGE, default: 1 }, + }, }, schemas: { ErrorEnvelope: { @@ -104,15 +146,36 @@ const options: swaggerJsdoc.Options = { type: 'object', properties: { count: { type: 'integer' }, + limit: { type: 'integer', maximum: VAULT_LIST_MAX_LIMIT }, total: { type: 'integer' }, nextCursor: { type: 'string', nullable: true }, prevCursor: { type: 'string', nullable: true }, - currentPage: { type: 'integer' }, + currentPage: { type: 'integer', minimum: 1, maximum: VAULT_LIST_MAX_PAGE }, totalPages: { type: 'integer' }, hasNextPage: { type: 'boolean' }, hasPrevPage: { type: 'boolean' }, }, }, + Vault: { + type: 'object', + required: ['id', 'aum', 'tvlUsd', 'createdAt', 'updatedAt'], + properties: { + id: { type: 'string', format: 'uuid' }, + aum: { type: 'number', example: 0 }, + tvlUsd: { type: 'string', nullable: true, example: '1250000.00' }, + createdAt: { type: 'string', format: 'date-time' }, + updatedAt: { type: 'string', format: 'date-time' }, + }, + }, + VaultListResponse: { + type: 'object', + required: ['data', 'pagination', 'timestamp'], + properties: { + data: { type: 'array', items: { $ref: '#/components/schemas/Vault' } }, + pagination: { $ref: '#/components/schemas/PaginationMeta' }, + timestamp: { type: 'string', format: 'date-time' }, + }, + }, VaultSummary: { type: 'object', properties: { @@ -256,6 +319,129 @@ const options: swaggerJsdoc.Options = { }, }, }, + '/api/v1/vaults/{id}/apy': { + get: { + tags: ['Vault'], + summary: 'Vault APY', + description: + "Returns the annualised APY for the specified vault. Returns `apy: null` with " + + "`apyStatus: 'insufficient_data'` when the vault has zero shares or fewer than two " + + 'price snapshots (e.g. a newly created vault).', + parameters: [ + { + name: 'id', + in: 'path', + required: true, + schema: { type: 'string' }, + description: 'Vault identifier', + }, + ], + responses: { + '200': { + description: 'Vault APY result', + content: { + 'application/json': { + schema: { + type: 'object', + required: ['apy', 'apyStatus', 'timestamp'], + properties: { + apy: { + type: ['number', 'null'], + example: 8.45, + description: + 'Annualised APY as a decimal percentage. null when insufficient data.', + }, + apyStatus: { + type: 'string', + enum: ['ok', 'insufficient_data'], + example: 'ok', + }, + timestamp: { type: 'string', format: 'date-time' }, + }, + }, + }, + }, + }, + }, + }, + }, + '/api/v1/vaults': { + get: { + tags: ['Vault'], + summary: 'List vaults', + description: + 'Paginated listing of active (non-deleted) vaults. ' + + '`limit` is capped at ' + + VAULT_LIST_MAX_LIMIT + + ' and `page` at ' + + VAULT_LIST_MAX_PAGE + + '; exceeding `limit` fails fast with `400` / `LIMIT_EXCEEDED` instead of ' + + 'silently clamping, so a client that asks for an oversized page is never ' + + 'handed a response that looks complete. ' + + RATE_LIMIT_NOTE, + parameters: [ + { $ref: '#/components/parameters/pageSize' }, + { $ref: '#/components/parameters/pageNumber' }, + { $ref: '#/components/parameters/correlationId' }, + ], + responses: { + '200': { + description: 'One page of vaults', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/VaultListResponse' }, + example: { + data: [ + { + id: '0f1b3a2c-6d4e-4a1b-9f0e-2c5d7e8b9a01', + aum: 1250000, + tvlUsd: '1250000.00', + createdAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-01-02T00:00:00.000Z', + }, + ], + pagination: { + count: 1, + limit: 20, + total: 1, + nextCursor: null, + prevCursor: null, + currentPage: 1, + totalPages: 1, + hasNextPage: false, + hasPrevPage: false, + }, + timestamp: '2026-01-01T00:00:00.000Z', + }, + }, + }, + }, + '400': { + description: '`limit` above the published ceiling', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/ErrorEnvelope' }, + example: { + error: 'Bad Request', + status: 400, + code: 'LIMIT_EXCEEDED', + message: `limit must not exceed ${VAULT_LIST_MAX_LIMIT} (received 100000).`, + retryable: false, + details: { + field: 'limit', + requested: 100000, + maxLimit: VAULT_LIST_MAX_LIMIT, + defaultLimit: VAULT_LIST_DEFAULT_LIMIT, + maxPage: VAULT_LIST_MAX_PAGE, + }, + }, + }, + }, + }, + '429': { description: 'Rate limited', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } } }, + }, + }, + }, '/api/v1/vault/deposits': { post: { tags: ['Vault'], diff --git a/backend/src/types/express.d.ts b/backend/src/types/express.d.ts index 1bdeca878..d710c84ec 100644 --- a/backend/src/types/express.d.ts +++ b/backend/src/types/express.d.ts @@ -1,10 +1,17 @@ declare global { + import type { ResolvedPagination } from '../middleware/paginationGuard'; + namespace Express { interface Request { authApiKeyHash?: string; authApiKeyRole?: string; apiVersion?: 'v1' | 'v2'; apiVersionSource?: 'path' | 'legacy' | 'default'; + /** + * Effective, clamped pagination parameters for the current request. + * Populated by `enforcePaginationLimits()`. + */ + resolvedPagination?: ResolvedPagination; } } } diff --git a/backend/src/types/validation.ts b/backend/src/types/validation.ts index 20041be10..88180a744 100644 --- a/backend/src/types/validation.ts +++ b/backend/src/types/validation.ts @@ -26,7 +26,10 @@ export const PaginationQuerySchema = z .object({ limit: z.string().regex(/^\d+$/, 'limit must be a positive integer').optional(), cursor: z.string().optional(), - page: z.string().regex(/^\d+$/, 'page must be a positive integer').optional(), + // Signed so an out-of-range page (`page=-1`, `page=1000000`) reaches + // `parsePaginationQuery`, which clamps it into `1..maxPage`, instead of + // being rejected outright (Issue #1430). + page: z.string().regex(/^-?\d+$/, 'page must be an integer').optional(), sortBy: z.string().optional(), sortOrder: z.string().optional(), dryRun: z.enum(['true', 'false', '1', '0']).optional(), From b0065f59335e0574f23ef94c571f1aa991a3ed09 Mon Sep 17 00:00:00 2001 From: ToryMic Date: Wed, 30 Sep 2026 00:01:11 -0400 Subject: [PATCH 3/3] test(backend): cover the idempotency replay cache, including its Redis path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `transferOrchestrator.test.ts` exercises the happy path end to end, but with no `REDIS_URL` the `redisClientManager` never reports ready, so `IdempotencyStore.redis` returns null and every Redis helper is skipped — the store's Redis branch, and more importantly its error fallback, had no coverage at all. These tests drive the store through a stub client so they reach: the Redis get/set/del round trip, conflict detection across instances, the read/write/delete failure fallbacks, and `pruneStaleKeys` across expired TTL, unparsable payloads and dryRun. Plus the in-process semantics that money-moving code relies on: single execution, replay, fingerprint conflict, in-flight coalescing, and that a rejected operation frees the pending slot for a retry. idempotencyStore.ts: 70.9% -> 94.5% statements, 50.9% -> 81.8% branches. --- .../src/__tests__/idempotencyStore.test.ts | 417 ++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 backend/src/__tests__/idempotencyStore.test.ts diff --git a/backend/src/__tests__/idempotencyStore.test.ts b/backend/src/__tests__/idempotencyStore.test.ts new file mode 100644 index 000000000..e40c6a951 --- /dev/null +++ b/backend/src/__tests__/idempotencyStore.test.ts @@ -0,0 +1,417 @@ +/** + * Unit tests for the idempotency replay cache (`idempotencyStore.ts`). + * + * `transferOrchestrator.test.ts` already exercises the happy path end to end. + * What it cannot reach is the Redis branch: with no `REDIS_URL` the + * `redisClientManager` never reports ready, so `IdempotencyStore.redis` + * returns `null` and every Redis helper is skipped. These tests drive the + * store through a stub client so the Redis code path — and, more importantly, + * its *fallback* when Redis errors — is actually executed. + */ + +import { + IdempotencyStore, + IdempotencyConflictError, + buildIdempotencyFingerprint, + getIdempotencyHashThreshold, + idempotencyStore, +} from '../idempotencyStore'; + +type RedisStub = Record; + +function createRedisStub(overrides: Partial = {}): RedisStub { + const store = new Map(); + + const base: RedisStub = { + get: jest.fn(async (key: string) => store.get(key) ?? null), + set: jest.fn(async (key: string, value: string) => { + store.set(key, value); + return 'OK'; + }), + del: jest.fn(async (key: string) => (store.delete(key) ? 1 : 0)), + exists: jest.fn(async (key: string) => (store.has(key) ? 1 : 0)), + scan: jest.fn(async () => ['0', [] as string[]]), + ttl: jest.fn(async () => -1), + }; + + return { ...base, ...overrides }; +} + +/** Points the module's `redisClientManager` at `client` for the test's duration. */ +async function withRedisClient(client: unknown, fn: () => Promise): Promise { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const rateLimiter = require('../rateLimiter') as typeof import('../rateLimiter'); + const manager = rateLimiter.redisClientManager as unknown as { + getClient: () => unknown; + isReady: () => boolean; + }; + const originalGetClient = manager.getClient; + const originalIsReady = manager.isReady; + + manager.getClient = () => client; + manager.isReady = () => true; + + try { + return await fn(); + } finally { + manager.getClient = originalGetClient; + manager.isReady = originalIsReady; + } +} + +describe('IdempotencyStore', () => { + describe('execute() with the in-process cache', () => { + it('runs the operation once and replays the stored result afterwards', async () => { + const store = new IdempotencyStore(60_000); + const operation = jest.fn(async () => ({ statusCode: 201, body: { hash: 'abc' } })); + + const first = await store.execute('key-1', 'fp-1', operation); + const second = await store.execute('key-1', 'fp-1', operation); + + expect(first).toEqual({ result: { statusCode: 201, body: { hash: 'abc' } }, replayed: false }); + expect(second.replayed).toBe(true); + expect(second.result).toEqual(first.result); + expect(operation).toHaveBeenCalledTimes(1); + }); + + it('throws IdempotencyConflictError when the body changed for the same key', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('key-2', 'fp-1', async () => ({ statusCode: 200, body: 'a' })); + + await expect( + store.execute('key-2', 'fp-2', async () => ({ statusCode: 200, body: 'b' })), + ).rejects.toBeInstanceOf(IdempotencyConflictError); + }); + + it('coalesces concurrent calls into a single operation', async () => { + const store = new IdempotencyStore(60_000); + let resolveOperation: (() => void) | undefined; + const gate = new Promise((resolve) => { + resolveOperation = resolve; + }); + const operation = jest.fn(async () => { + await gate; + return { statusCode: 200, body: 'once' }; + }); + + const inflight = Promise.all([ + store.execute('key-3', 'fp', operation), + store.execute('key-3', 'fp', operation), + ]); + resolveOperation!(); + const [a, b] = await inflight; + + expect(operation).toHaveBeenCalledTimes(1); + expect([a.replayed, b.replayed].filter(Boolean)).toHaveLength(1); + }); + + it('rejects a concurrent call with a different fingerprint', async () => { + const store = new IdempotencyStore(60_000); + let resolveOperation: (() => void) | undefined; + const gate = new Promise((resolve) => { + resolveOperation = resolve; + }); + + const inflight = store.execute('key-4', 'fp-a', async () => { + await gate; + return { statusCode: 200, body: 'a' }; + }); + const conflicting = store.execute('key-4', 'fp-b', async () => ({ statusCode: 200, body: 'b' })); + + resolveOperation!(); + await inflight; + await expect(conflicting).rejects.toBeInstanceOf(IdempotencyConflictError); + }); + + it('does not leave a key pending after the operation rejects', async () => { + const store = new IdempotencyStore(60_000); + await expect( + store.execute('key-5', 'fp', async () => { + throw new Error('boom'); + }), + ).rejects.toThrow('boom'); + + // The pending slot must be free, so a retry runs the operation again. + const retry = await store.execute('key-5', 'fp', async () => ({ statusCode: 200, body: 'ok' })); + expect(retry.replayed).toBe(false); + }); + }); + + describe('observability and maintenance', () => { + it('reports hits, conflicts and active keys', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('m-1', 'fp', async () => ({ statusCode: 200, body: 1 })); + await store.execute('m-1', 'fp', async () => ({ statusCode: 200, body: 1 })); + await expect( + store.execute('m-2', 'other', async () => ({ statusCode: 200, body: 2 })), + ).resolves.toBeDefined(); + + const metrics = store.getMetrics(); + expect(metrics.hits).toBe(1); + expect(metrics.activeKeys).toBe(2); + expect(metrics.pendingKeys).toBe(0); + }); + + it('counts a conflict', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('m-3', 'fp', async () => ({ statusCode: 200, body: 1 })); + await expect( + store.execute('m-3', 'fp-different', async () => ({ statusCode: 200, body: 2 })), + ).rejects.toBeInstanceOf(IdempotencyConflictError); + + expect(store.getMetrics().conflicts).toBe(1); + }); + + it('lists stored keys, optionally filtered by prefix', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('alpha-1', 'fp', async () => ({ statusCode: 200, body: 1 })); + await store.execute('beta-1', 'fp', async () => ({ statusCode: 200, body: 2 })); + + expect(store.inspectKeys().map((entry) => entry.key).sort()).toEqual(['alpha-1', 'beta-1']); + expect(store.inspectKeys('alpha').map((entry) => entry.key)).toEqual(['alpha-1']); + expect(store.inspectKeys('alpha')[0].metadata.status).toBe('completed'); + }); + + it('deletes a single key and reports whether anything was removed', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('d-1', 'fp', async () => ({ statusCode: 200, body: 1 })); + + expect(await store.deleteKey('d-1')).toBe(true); + expect(await store.deleteKey('d-1')).toBe(false); + expect(store.inspectKeys()).toHaveLength(0); + }); + + it('clear() empties the store and counts the evictions', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('c-1', 'fp', async () => ({ statusCode: 200, body: 1 })); + await store.execute('c-2', 'fp', async () => ({ statusCode: 200, body: 2 })); + + const before = store.getMetrics().evictions; + store.clear(); + + expect(store.inspectKeys()).toHaveLength(0); + expect(store.getMetrics().evictions).toBe(before + 2); + }); + + it('prunes entries older than the retention window and honours dryRun', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('p-1', 'fp', async () => ({ statusCode: 200, body: 1 })); + await store.execute('p-2', 'fp', async () => ({ statusCode: 200, body: 2 })); + + // A retention of -1 puts the cutoff in the future, so everything + // currently stored counts as stale. + const dry = await store.pruneStaleKeys(-1, true); + expect(dry.localPruned).toBe(2); + expect(dry.redisPruned).toBe(0); + expect(store.inspectKeys()).toHaveLength(2); + + const applied = await store.pruneStaleKeys(-1, false); + expect(applied.localPruned).toBe(2); + expect(applied.pruned).toBe(2); + expect(store.inspectKeys()).toHaveLength(0); + }); + + it('keeps entries that are newer than the retention window', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('p-3', 'fp', async () => ({ statusCode: 200, body: 1 })); + + const result = await store.pruneStaleKeys(60_000, false); + expect(result.localPruned).toBe(0); + expect(store.inspectKeys()).toHaveLength(1); + }); + }); + + describe('Redis path', () => { + it('reads and writes through Redis when it is available', async () => { + const redis = createRedisStub(); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + const first = await store.execute('r-1', 'fp', async () => ({ statusCode: 200, body: 'v' })); + expect(first.replayed).toBe(false); + expect(redis.set).toHaveBeenCalled(); + + // A fresh store shares no local cache, so only Redis can serve the replay. + const other = new IdempotencyStore(60_000); + const second = await other.execute('r-1', 'fp', async () => ({ statusCode: 200, body: 'v' })); + expect(second.replayed).toBe(true); + expect(redis.get).toHaveBeenCalled(); + }); + }); + + it('detects a conflicting body via Redis', async () => { + const redis = createRedisStub(); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + await store.execute('r-2', 'fp', async () => ({ statusCode: 200, body: 'v' })); + + await expect( + store.execute('r-2', 'different', async () => ({ statusCode: 200, body: 'v' })), + ).rejects.toBeInstanceOf(IdempotencyConflictError); + }); + }); + + it('deletes via Redis and reports a removal', async () => { + const redis = createRedisStub(); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + await store.execute('r-3', 'fp', async () => ({ statusCode: 200, body: 'v' })); + + expect(await store.deleteKey('r-3')).toBe(true); + expect(redis.del).toHaveBeenCalled(); + expect(await store.deleteKey('never-stored')).toBe(false); + }); + }); + + it('survives Redis write failures by falling back to the local cache', async () => { + const redis = createRedisStub({ set: jest.fn(async () => { throw new Error('write fail'); }) }); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + const first = await store.execute('r-4', 'fp', async () => ({ statusCode: 200, body: 'v' })); + const second = await store.execute('r-4', 'fp', async () => ({ statusCode: 200, body: 'v' })); + + expect(first.replayed).toBe(false); + expect(second.replayed).toBe(true); + }); + }); + + it('survives Redis read failures', async () => { + const redis = createRedisStub({ get: jest.fn(async () => { throw new Error('read fail'); }) }); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + const first = await store.execute('r-5', 'fp', async () => ({ statusCode: 200, body: 'v' })); + const second = await store.execute('r-5', 'fp', async () => ({ statusCode: 200, body: 'v' })); + + expect(first.replayed).toBe(false); + expect(second.replayed).toBe(true); + }); + }); + + it('survives Redis delete failures', async () => { + const redis = createRedisStub({ del: jest.fn(async () => { throw new Error('del fail'); }) }); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + await store.execute('r-6', 'fp', async () => ({ statusCode: 200, body: 'v' })); + + // The local entry is still removed, so the key is gone either way. + expect(await store.deleteKey('r-6')).toBe(true); + }); + }); + + it('prunes stale Redis keys, honouring dryRun', async () => { + const keys = new Map(); + const redis = createRedisStub({ + scan: jest.fn(async () => ['0', ['idempotency:r-7']]), + get: jest.fn(async (key: string) => { + if (key !== 'idempotency:r-7') return null; + return ( + keys.get(key) ?? + JSON.stringify({ + statusCode: 200, + body: 'v', + fingerprint: 'fp', + metadata: { + createdAt: new Date(Date.now() - 120_000).toISOString(), + lastAccessedAt: new Date().toISOString(), + replayCount: 0, + status: 'completed', + }, + }) + ); + }), + ttl: jest.fn(async () => -1), + }); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + + const dry = await store.pruneStaleKeys(60_000, true); + expect(dry.redisPruned).toBe(1); + expect(redis.del).not.toHaveBeenCalled(); + + const applied = await store.pruneStaleKeys(60_000, false); + expect(applied.redisPruned).toBe(1); + expect(redis.del).toHaveBeenCalledWith('idempotency:r-7'); + }); + }); + + it('prunes a Redis key whose TTL has run out', async () => { + const redis = createRedisStub({ + scan: jest.fn(async () => ['0', ['idempotency:r-8']]), + get: jest.fn(async () => + JSON.stringify({ + statusCode: 200, + body: 'v', + fingerprint: 'fp', + metadata: { + createdAt: new Date().toISOString(), + lastAccessedAt: new Date().toISOString(), + replayCount: 0, + status: 'completed', + }, + }), + ), + ttl: jest.fn(async () => 0), + }); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + const result = await store.pruneStaleKeys(60_000, false); + expect(result.redisPruned).toBe(1); + }); + }); + + it('prunes a Redis key that cannot be parsed', async () => { + const redis = createRedisStub({ + scan: jest.fn(async () => ['0', ['idempotency:r-9']]), + get: jest.fn(async () => 'not json'), + }); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + const result = await store.pruneStaleKeys(60_000, false); + expect(result.redisPruned).toBe(1); + expect(redis.del).toHaveBeenCalledWith('idempotency:r-9'); + }); + }); + }); + + describe('buildIdempotencyFingerprint()', () => { + it('is stable regardless of key order', () => { + expect(buildIdempotencyFingerprint({ a: 1, b: 2 })).toBe( + buildIdempotencyFingerprint({ b: 2, a: 1 }), + ); + }); + + it('handles nested objects, arrays, dates and null', () => { + const date = new Date('2026-01-01T00:00:00.000Z'); + const fingerprint = buildIdempotencyFingerprint({ z: [1, { y: 2 }], d: date, n: null }); + + expect(fingerprint).toContain('2026-01-01T00:00:00.000Z'); + expect(fingerprint).toContain('null'); + expect(buildIdempotencyFingerprint({ z: [1, { y: 2 }], d: date, n: null })).toBe(fingerprint); + }); + + it('hashes payloads larger than the configured threshold', () => { + const oversized = { blob: 'x'.repeat(getIdempotencyHashThreshold() + 10) }; + const fingerprint = buildIdempotencyFingerprint(oversized); + + expect(fingerprint.startsWith('hashv1:')).toBe(true); + expect(fingerprint).toHaveLength('hashv1:'.length + 64); + }); + + it('does not hash payloads within the threshold', () => { + expect(buildIdempotencyFingerprint({ blob: 'small' })).not.toMatch(/^hashv1:/); + }); + }); + + it('exposes a module-level singleton', () => { + expect(idempotencyStore).toBeInstanceOf(IdempotencyStore); + expect(typeof idempotencyStore.getMetrics().activeKeys).toBe('number'); + }); +});