Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Once running:
| `GET /health` | None | Liveness check — returns `{ "status": "ok" }` immediately. Use this to verify the process is up. |
| `GET /healthz/dependencies` | None | Shallow dependency probe — Postgres, Soroban RPC, Horizon, webhook queue (Redis). Cached for 5 s. Returns 200/207/503. |
| `GET /api/health/ready` | None | **Deep readiness check** — runs four parallel probes with 1-second timeouts each. Returns 200 when ready, 503 when unready. |
| `GET /api/indexer/health` | None | Indexer liveness — compares the persisted cursor against the chain tip. Returns `"ok"` / `"degraded"` / `"down"` in `data.status`. Supports [ETag / conditional GET](#etag--conditional-get-caching). |
| `GET /api/indexer/health` | None | Indexer healthprobes external dependencies (Postgres + Soroban RPC) and compares the persisted cursor against the chain tip. Returns `"ok"` / `"degraded"` / `"down"` with dependency statuses in `dependencies` and lag data in `data`. Always HTTP 200. Supports [ETag / conditional GET](#etag--conditional-get-caching). |

### `GET /api/health/ready` response

Expand Down
3 changes: 2 additions & 1 deletion src/__tests__/routes/markets/health.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import request from "supertest";
import express from "express";
import type { Request, Response, NextFunction } from "express";
import { createMarketsHealthRouter } from "../../../../src/routes/markets/health";

const MOCK_HEALTH = {
Expand Down Expand Up @@ -63,7 +64,7 @@ describe("Markets Health Router (v7)", () => {
}));

// Add a basic error handler to catch it
app.use((err: any, req: any, res: any, next: any) => {
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
res.status(500).json({ error: err.message });
});

Expand Down
5 changes: 3 additions & 2 deletions src/__tests__/routes/subscriptions.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { describe, it, expect, beforeEach, jest } from "@jest/globals";
import request from "supertest";
import express from "express";
import type { Request, Response, NextFunction } from "express";
import { subscriptionsRouter } from "../../routes/subscriptions";
import { db } from "../../db/client";
import { generateETag } from "../../middleware/etag";

jest.mock("../../middleware/requireAdmin", () => ({
requireAdmin: (req: any, res: any, next: any) => next(),
requireAdmin: (_req: Request, _res: Response, next: NextFunction) => next(),
}));

jest.mock("../../db/client", () => {
Expand Down Expand Up @@ -79,7 +80,7 @@ describe("Subscriptions Routes", () => {
it("should handle db errors", async () => {
(db.from as jest.Mock).mockRejectedValueOnce(new Error("Database error"));
// Mute the express default error handler output in tests
app.use((err: any, req: any, res: any, next: any) => {
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
res.status(500).json({ error: "Internal Error" });
});

Expand Down
3 changes: 0 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,11 @@ import { createDocsRouter } from "./routes/docs";
import { sessionsRouter } from "./routes/me/sessions";
import { notificationsRouter } from "./routes/notifications";
import { socialRouter } from "./routes/social";
import { webhooksRouter } from "./routes/webhooks";
import { webhooksHealthRouter } from "./routes/webhooks/health";
import { adminAuditRouter } from "./routes/admin/audit";
import { adminAuditExportRouter } from "./routes/admin/audit/export";
import { auditCountsRouter } from "./routes/audit/counts";
import { adminMarketsRouter } from "./routes/admin/markets";
import { adminReconciliationRouter } from "./routes/admin/reconciliation";
import { adminSchemaVersionsRouter } from "./routes/admin/schema-versions";
import { errorHandler } from "./middleware/errorHandler";
import { requestContextStorage } from "./lib/requestContext";
Expand All @@ -53,7 +51,6 @@ import { backupVerificationWorker } from "./workers/backupVerificationWorker";
import { reconciliationWorker } from "./workers/reconciliationWorker";
import { rateLimitRouter } from "./routes/rate-limit";
import { adminRateLimitInspectRouter } from "./routes/admin/rate-limit/inspect";
import { forceResolveRouter } from "./routes/admin/force-resolve";
import { quotaRequestsRouter } from "./routes/quota/requests";
import { startSlowQueryAlerter, stopSlowQueryAlerter } from "./workers/slowQueryAlerter";
import { reportsRouter } from "./routes/reports";
Expand Down
21 changes: 21 additions & 0 deletions src/metrics/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ export const indexerPollsTotal = new Counter({
registers: [register],
});

export const indexerLagLedgers = new Gauge({
name: "indexer_lag_ledgers",
help: "Current lag in ledgers between the indexer cursor and the chain tip",
registers: [register],
});

export const webhookDeliveriesTotal = new Counter({
name: "webhook_deliveries_total",
help: "Total number of webhook deliveries, segmented by outcome status (success, failed)",
Expand Down Expand Up @@ -105,3 +111,18 @@ export const webhooksEndpointDuration = new Histogram({
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
registers: [register],
});

export const usersEndpointRequestsTotal = new Counter({
name: "users_endpoint_requests_total",
help: "Total number of requests to /api/users endpoints, segmented by method, route, and status",
labelNames: ["method", "route", "status"] as const,
registers: [register],
});

export const usersEndpointDuration = new Histogram({
name: "users_endpoint_duration_seconds",
help: "Request duration in seconds for /api/users endpoints, segmented by method, route, and status",
labelNames: ["method", "route", "status"] as const,
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
registers: [register],
});
17 changes: 16 additions & 1 deletion src/middleware/rateLimit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import rateLimit, { type Options, type RateLimitRequestHandler } from "express-r
import type { NextFunction, Request, Response } from "express";
import { v4 as uuidv4 } from "uuid";
import { createAuditLog, type RateLimitContext } from "../services/auditService";
import { logger } from "../config/logger";
import { env } from "../config/env";

declare global {
Expand Down Expand Up @@ -281,6 +280,22 @@ export function createPerUserRateLimiter(options: Partial<Options> = {}): RateLi
});
}

export function createUserRateLimiter(options: Partial<Options> = {}): RateLimitRequestHandler {
return createRateLimiter({
windowMs: 15 * 60 * 1000,
limit: 100,
keyGenerator: (req) => {
const authReq = req as AuthenticatedRequest;
const address = authReq.user?.address ?? authReq.user?.sub;
if (typeof address === "string" && address.trim().length > 0) {
return `user:${address}`;
}
return `ip:${getClientIp(req)}`;
},
...options,
});
}

/**
* Pre-configured per-user rate limiter for `/api/webhooks` routes.
*
Expand Down
1 change: 0 additions & 1 deletion src/middleware/timeout.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Request, Response, NextFunction } from "express";
import { logger } from "../config/logger";

export interface RequestTimeoutOptions {
/** HTTP status code to send when the timeout is exceeded. Defaults to 408. */
Expand Down
1 change: 0 additions & 1 deletion src/routes/admin/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { z } from "zod";
import { requireAdmin } from "../../middleware/requireAdmin";
import { getAuditLogs } from "../../repositories/auditLogRepo";
import { RouteErrorFactory } from "../../errors";
import { startAuditSpan, endAuditSpan, recordErrorOnSpan } from "../../otel/spans";
import { searchAuditLogsHandler } from "./audit/search";

export interface AdminAuditRouterOptions {
Expand Down
3 changes: 1 addition & 2 deletions src/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Router, Request, Response, NextFunction } from "express";
import { Router } from "express";
import { z } from "zod";
import { and, eq, isNull, gt, desc, or, lt } from "drizzle-orm";
import { StrKey } from "@stellar/stellar-sdk";
import { conditionalGet } from "../middleware/etag";
import { createPerUserRateLimiter } from "../middleware/rateLimit";
Expand Down
Loading
Loading