Skip to content
Closed
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
41 changes: 40 additions & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import app, { createGracefulShutdownHandler, createInFlightDrainTracker } from '
jest.mock('./db/index.js', () => ({
db: {},
initializeDb: jest.fn(),
schema: {},
schema: {
apis: { deleted_at: 'deleted_at' },
},
}));
describe('Health API', () => {
it('should return ok status', async () => {
Expand Down Expand Up @@ -348,3 +350,40 @@ describe('refresh-token drain tracker', () => {
expect(tracker.subsystem.name).toBe('refresh-token');
});
});

import fs from 'node:fs';
import path from 'node:path';

describe('entrypoint hygiene — src/index.ts (#1200)', () => {
const indexSource = fs.readFileSync(path.join(process.cwd(), 'src', 'index.ts'), 'utf8');

it('has no duplicate top-level module import paths', () => {
const importRegex = /^import\s+[\s\S]*?from\s+["']([^"']+)["'];/gm;
const seenModules = new Set<string>();
const duplicates: string[] = [];
let match: RegExpExecArray | null;

while ((match = importRegex.exec(indexSource)) !== null) {
const mod = match[1];
if (seenModules.has(mod)) {
duplicates.push(mod);
}
seenModules.add(mod);
}

expect(duplicates).toEqual([]);
});

it('imports createApiKeyRouter, createWebhooksRouter, and defaultApiRepository', () => {
expect(indexSource).toMatch(/import\s+\{\s*createApiKeyRouter\s*\}\s+from\s+["']\.\/routes\/apiKeyRoutes\.js["']/);
expect(indexSource).toMatch(/import\s+\{\s*createWebhooksRouter\s*\}\s+from\s+["']\.\/routes\/webhooks\.js["']/);
expect(indexSource).toMatch(/import\s+\{\s*defaultApiRepository\s*\}\s+from\s+["']\.\/repositories\/apiRepository\.js["']/);
});

it('mounts /api/admin and /api/admin/usage/anomalies only once', () => {
const adminMounts = indexSource.match(/app\.use\(\s*["']\/api\/admin["']/g) ?? [];
const anomaliesMounts = indexSource.match(/app\.use\(\s*["']\/api\/admin\/usage\/anomalies["']/g) ?? [];
expect(adminMounts).toHaveLength(1);
expect(anomaliesMounts).toHaveLength(1);
});
});
42 changes: 12 additions & 30 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,21 @@ import {
createInFlightDrainTracker,
type DrainableSubsystem,
} from "./lifecycle/shutdown.js";
import { quotasDrainTracker } from "./routes/quotas/counts.js";
import type { Socket } from "net";

import { createDeveloperRouter } from "./routes/developerRoutes.js";
import { createGatewayRouter } from "./routes/gatewayRoutes.js";
import { createProxyRouter } from "./routes/proxyRoutes.js";
import { createApiKeyRouter } from "./routes/apiKeyRoutes.js";
import { createWebhooksRouter } from "./routes/webhooks.js";
import { createRefreshTokenRouter } from "./routes/refresh-token.js";
import adminRouter from "./routes/admin.js";
import logsRouter from "./routes/logs.js";
import { createUsageAnomaliesRouter } from "./routes/admin/usage/anomalies.js";
import refundsRouter from "./routes/refunds.js";
import { AuthController } from "./controllers/authController.js";
import { RefreshTokenService } from "./services/refreshTokenService.js";
import { DatabaseRefreshTokenRepository } from "./repositories/refreshTokenRepository.js";
import { defaultApiRepository } from "./repositories/apiRepository.js";
import { defaultDeveloperRepository } from "./repositories/developerRepository.js";
import { createBillingService } from "./services/billingService.js";
import {
Expand All @@ -47,7 +51,7 @@ import { createIdempotencySweeperJob } from "./services/idempotencySweeper.js";
import { createPostgresUsageStore } from "./services/usageStore.js";
import { createPostgresSettlementStore } from "./services/settlementStore.js";
import { createApiRegistry } from "./data/apiRegistry.js";
import { ApiKey } from "./types/gateway.js";
import type { ApiKey } from "./types/gateway.js";
import { listingsCache } from "./lib/listingsCache.js";
import { createSlowQueryAlerterJob } from "./workers/slowQueryAlerter.js";
import { createAnomalyDetectorJob } from "./workers/anomalyDetector.js";
Expand All @@ -58,29 +62,6 @@ import {
import { createSloAlertJob } from "./workers/sloAlertJob.js";
import { createMonthlyInvoiceJob } from "./workers/monthlyInvoiceJob.js";
import { createSettlementReconWorker } from "./workers/settlementRecon.js";
import { createDeveloperRouter } from './routes/developerRoutes.js';
import { createGatewayRouter } from './routes/gatewayRoutes.js';
import { createProxyRouter } from './routes/proxyRoutes.js';
import { createRefreshTokenRouter } from './routes/refresh-token.js';
import { AuthController } from './controllers/authController.js';
import { RefreshTokenService } from './services/refreshTokenService.js';
import { DatabaseRefreshTokenRepository } from './repositories/refreshTokenRepository.js';
import { defaultDeveloperRepository } from './repositories/developerRepository.js';
import { createBillingService } from './services/billingService.js';
import { createRateLimiter } from './services/rateLimiter.js';
import { PgUsageEventsRepository } from './repositories/usageEventsRepository.pg.js';
import { createRevenueLedgerIndexerJob } from './services/revenueLedgerIndexer.js';
import { RevenueSettlementService } from './services/revenueSettlementService.js';
import { createSettlementStatusSyncJob } from './services/settlementStatusSyncJob.js';
import { createSettlementReconciliationJob } from './services/settlementReconciliationJob.js';
import { createIdempotencySweeperJob } from './services/idempotencySweeper.js';
import { createPostgresUsageStore } from './services/usageStore.js';
import { createPostgresSettlementStore } from './services/settlementStore.js';
import { createApiRegistry } from './data/apiRegistry.js';
import { ApiKey } from './types/gateway.js';
import { listingsCache } from './lib/listingsCache.js';
import { createSlowQueryAlerterJob } from './workers/slowQueryAlerter.js';
import { createAnomalyDetectorJob } from './workers/anomalyDetector.js';

// Helper for Jest/CommonJS compat
const isDirectExecution =
Expand Down Expand Up @@ -273,9 +254,6 @@ if (isDirectExecution) {
app.use("/api/admin", adminRouter);
app.use("/api/refunds", refundsRouter);
app.use("/api/logs", logsRouter);
app.use('/api/admin/usage/anomalies', createUsageAnomaliesRouter({ pool }));

app.use('/api/admin', adminRouter);

// Legacy gateway route (existing)
const gatewayRouter = createGatewayRouter({
Expand All @@ -285,7 +263,7 @@ if (isDirectExecution) {
upstreamUrl: config.proxy.upstreamUrl,
apiKeys,
});
app.use("/api/gateway", createGatewayIpAllowlist(), gatewayRouter);
app.use("/api/gateway", createGatewayIpAllowlist(), legacyV1DeprecationMiddleware, gatewayRouter);

// New proxy route: /v1/call/:apiSlugOrId/*
//
Expand All @@ -309,11 +287,14 @@ if (isDirectExecution) {
// during the graceful shutdown window.
drainState: { isDraining: proxyDrainTracker.isDraining },
});
app.use("/v1/call", proxyDrainTracker.middleware, proxyRouter);

const keysDrainTracker = createInFlightDrainTracker("api-keys");
const apiKeyRouter = createApiKeyRouter({
apiRepository: defaultApiRepository,
developerRepository: defaultDeveloperRepository,
});
app.use("/api", keysDrainTracker.middleware, apiKeyRouter);

// --- Refresh-token drain tracker ---
// Tracks in-flight POST /api/refresh-token requests so that a SIGTERM during
Expand All @@ -335,6 +316,7 @@ if (isDirectExecution) {

const shutdownSubsystems: DrainableSubsystem[] = [
proxyDrainTracker.subsystem,
keysDrainTracker.subsystem,
// Drain in-flight refresh-token requests before the process exits.
refreshTokenDrainTracker.subsystem,
{
Expand Down