Skip to content
Open
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
7 changes: 7 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,16 @@ export default [
{ argsIgnorePattern: "^_" },
],
"@typescript-eslint/no-explicit-any": "warn",
"no-console": "error",
"custom/no-unwrapped-async-handlers": "error",
},
},
{
files: ["src/scripts/**/*.ts", "src/logger.ts", "src/**/*.test.ts"],
rules: {
"no-console": "off",
},
},
{
ignores: ["dist/**", "node_modules/**"],
},
Expand Down
9 changes: 5 additions & 4 deletions src/__tests__/security-headers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import request from 'supertest';
import { createApp } from '../app.js';
import { logger } from '../logger.js';

// Mock better-sqlite3 to prevent native binding errors
jest.mock('better-sqlite3', () => {
Expand Down Expand Up @@ -289,20 +290,20 @@ describe('Security Headers and CORS Configuration', () => {
process.env.NODE_ENV = 'production';
delete process.env.CORS_ALLOWED_ORIGINS;

// Mock console.warn to capture warning
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
// Mock logger.warn to capture warning
const loggerSpy = jest.spyOn(logger, 'warn').mockImplementation();

try {
const app = createApp();
await request(app).get('/api/health');

// Should have logged a warning
expect(consoleSpy).toHaveBeenCalledWith(
expect(loggerSpy).toHaveBeenCalledWith(
expect.stringContaining('WARNING: No CORS_ALLOWED_ORIGINS configured in production')
);
} finally {
process.env = originalEnv;
consoleSpy.mockRestore();
loggerSpy.mockRestore();
}
});

Expand Down
6 changes: 4 additions & 2 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import express from 'express';
import { logger } from './logger.js';
import cors from 'cors';
import helmet from 'helmet';
import adminRouter from './routes/admin.js';
Expand Down Expand Up @@ -130,6 +131,7 @@ interface AppDependencies extends SorobanBillingDependencies {
* @example Wire into shutdown handler
* ```ts
* import { quotasDrainTracker } from './app.js';
import { logger } from './logger.js';
*
* const shutdown = createGracefulShutdownHandler({
* server,
Expand Down Expand Up @@ -273,7 +275,7 @@ export const createApp = (dependencies?: Partial<AppDependencies>) => {

// Validate origins in production
if (isProduction && allowedOrigins.length === 0) {
console.warn("WARNING: No CORS_ALLOWED_ORIGINS configured in production");
logger.warn("WARNING: No CORS_ALLOWED_ORIGINS configured in production");
}

// Regex for localhost with optional port (e.g., http://localhost:5173)
Expand Down Expand Up @@ -302,7 +304,7 @@ export const createApp = (dependencies?: Partial<AppDependencies>) => {

// Log blocked attempts in production
if (isProduction) {
console.warn(`CORS blocked origin: ${origin}`);
logger.warn(`CORS blocked origin: ${origin}`);
}

// Pass false instead of Error to prevent Express from returning 500
Expand Down
5 changes: 3 additions & 2 deletions src/config/env.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import "dotenv/config";
import { z } from "zod";
import { logger } from '../logger.js';

const stellarNetworkSchema = z.enum(["testnet", "mainnet"]);

Expand Down Expand Up @@ -587,9 +588,9 @@ export const envSchema = z
const parsed = envSchema.safeParse(process.env);

if (!parsed.success) {
console.error("❌ Invalid environment configuration:");
logger.error("❌ Invalid environment configuration:");
parsed.error.issues.forEach((issue) => {
console.error(` - ${issue.path.join(".")}: ${issue.message}`);
logger.error(` - ${issue.path.join(".")}: ${issue.message}`);
});
process.exit(1);
}
Expand Down
3 changes: 2 additions & 1 deletion src/controllers/depositController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { VaultRepository } from '../repositories/vaultRepository.js';
import { config } from '../config/index.js';
import { redactSimulationDetails } from '../lib/simulationDiagnostics.js';
import { successEnvelope, errorEnvelope, getRequestId } from '../lib/envelope.js';
import { logger } from '../middleware/logging.js';

export interface DepositPrepareRequest {
amount_usdc: string;
Expand Down Expand Up @@ -242,7 +243,7 @@ export class DepositController {
});
} else if (error instanceof SimulationError) {
// Log full diagnostics at warning level, but only expose a redacted summary.
console.warn('Soroban simulation diagnostics:', error.simulationDetails);
logger.warn('Soroban simulation diagnostics:', error.simulationDetails);
const redacted = redactSimulationDetails(error.simulationDetails);
res.status(502).json({
error: 'Soroban simulation failed. See diagnostics for details.',
Expand Down
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import { ApiKey } from './types/gateway.js';
import { listingsCache } from './lib/listingsCache.js';
import { createSlowQueryAlerterJob } from './workers/slowQueryAlerter.js';
import { createAnomalyDetectorJob } from './workers/anomalyDetector.js';
import { logger } from './logger.js';

// Helper for Jest/CommonJS compat
const isDirectExecution =
Expand Down Expand Up @@ -430,7 +431,7 @@ if (isDirectExecution) {
sloAlertJob?.start();

const server = app.listen(PORT, () => {
console.log(`Callora backend listening on http://localhost:${PORT}`);
logger.info(`Callora backend listening on http://localhost:${PORT}`);
});

// Track active connections so we can wait for them to finish
Expand Down Expand Up @@ -459,7 +460,7 @@ if (isDirectExecution) {
process.once("SIGTERM", () => onSignal("SIGTERM"));
process.once("SIGINT", () => onSignal("SIGINT"));
} catch (error) {
console.error("Failed to start server:", error);
logger.error("Failed to start server:", error);
process.exit(1);
}
}
Expand Down
7 changes: 3 additions & 4 deletions src/middleware/envelopeValidator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Request, Response, NextFunction } from 'express';
import {
ENVELOPE_REQUIRED_FIELDS,
} from '../types/ResponseEnvelope.js';
import { ENVELOPE_REQUIRED_FIELDS } from '../types/ResponseEnvelope.js';
import { logger } from '../logger.js';

/**
* Validates that every response sent through res.json() conforms
Expand All @@ -27,7 +26,7 @@ export function envelopeValidator(
// Fail fast in development so violations are caught immediately
throw new Error(message);
} else {
console.warn(message);
logger.warn(message);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/routes/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ function sendSimulationFailure(
res: Response,
result: Pick<BillingDeductResult, "error" | "simulationDetails">,
): void {
console.warn("Soroban simulation diagnostics:", result.simulationDetails);
logger.warn("Soroban simulation diagnostics:", result.simulationDetails);
res.status(502).json({
error: "Soroban simulation failed",
code: "SIMULATION_FAILED",
Expand Down Expand Up @@ -265,7 +265,7 @@ router.post(
} catch (error) {
if (error instanceof SorobanRpcError) {
if (error.simulationDetails) {
console.warn(
logger.warn(
"Soroban simulation diagnostics:",
error.simulationDetails,
);
Expand Down
4 changes: 2 additions & 2 deletions src/routes/billing/deduct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ const idempotencyHandler = (
* {@link redactSimulationDetails} is emitted.
*/
function logSimulationFailure(details: unknown): void {
logger.warn("[billing/deduct] Soroban simulation failed", {
logger.warn({
simulationDetails: redactSimulationDetails(details),
});
}, "[billing/deduct] Soroban simulation failed");
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/routes/proxyRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,16 +428,16 @@ export function createProxyRouter(deps: ProxyDeps): Router {
// before (idempotency guard inside usageStore.record).
if (recorded && endpoint.priceUsdc > 0) {
billing.deductCredit(keyRecord.userId, endpoint.priceUsdc).catch((err) => {
console.error('Background billing deduction failed:', err);
logger.error({ error: err }, 'Background billing deduction failed');
});
}
} catch (err) {
recordUsageRecordFailure();
logger.error('Background usage recording failed', {
logger.error({
requestId,
apiId: String(apiEntry.id),
error: err,
});
}, 'Background usage recording failed');
}
})();
});
Expand Down
18 changes: 11 additions & 7 deletions src/services/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type { Pool, PoolClient } from "pg";
import type { SimulationDetails } from "../lib/simulationDiagnostics.js";
import { computeJitteredDelay, type RandomSource } from "../lib/retry.js";
import { DeveloperSemaphore } from "../utils/developerSemaphore.js";
import { logger } from "../middleware/logging.js";

const USDC_7_DECIMAL_FACTOR = 10_000_000n;
const DEFAULT_RETRY_DELAYS_MS = [150, 500, 1_000];
Expand Down Expand Up @@ -685,9 +686,9 @@ export class BillingService {
// a data-integrity concern but NOT a reason to report failure to the
// caller — the charge happened. Log and return success; the
// reconciliation job will back-fill the hash.
console.error(
`[BillingService] Phase 3 UPDATE failed for usageEventId=${usageEventId} ` +
`txHash=${deductResult.txHash}: ${normalizeErrorMessage(error)}`,
logger.error(
{ usageEventId, txHash: deductResult.txHash, error: normalizeErrorMessage(error) },
'[BillingService] Phase 3 UPDATE failed',
);
}

Expand Down Expand Up @@ -889,10 +890,13 @@ export class BillingService {
deductResult.txHash,
);
} catch (error) {
console.error(
`[BillingService] Bulk Phase 3 UPDATE failed for usageEventIds=` +
`${phase1.inserted.map((entry) => entry.usageEventId).join(",")} ` +
`txHash=${deductResult.txHash}: ${normalizeErrorMessage(error)}`,
logger.error(
{
usageEventIds: phase1.inserted.map((entry) => entry.usageEventId),
txHash: deductResult.txHash,
error: normalizeErrorMessage(error)
},
'[BillingService] Bulk Phase 3 UPDATE failed'
);
}

Expand Down
4 changes: 2 additions & 2 deletions src/services/rateLimiter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { PoolClient } from 'pg';
import type { RateLimiter, RateLimitResult } from '../types/gateway.js';
import { logger } from '../logger.js';
import { logger } from '../middleware/logging.js';
import {
recordRateLimiterStoreOutage,
recordRateLimiterStoreRecovery,
Expand Down Expand Up @@ -444,7 +444,7 @@ export class StoreBackedRateLimiter implements RateLimiter {
private resolvePolicy(tier?: string): TierPolicy {
if (!tier || !(tier in this.tierPolicies)) {
if (tier) {
console.warn(`[rateLimiter] Unknown tier "${tier}", using default`);
logger.warn(`[rateLimiter] Unknown tier "${tier}", using default`);
}
return { maxRequests: this.maxRequests, windowMs: this.windowMs };
}
Expand Down
34 changes: 13 additions & 21 deletions src/services/revenueSettlementService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ApiRegistry, UsageEvent, UsageStore } from '../types/gateway.js';
import { SorobanSettlementClient } from './sorobanSettlement.js';
import { randomUUID } from 'node:crypto';
import { calloraEvents } from '../events/event.emitter.js';
import { logger } from '../middleware/logging.js';
import {
RETRIABLE_HTTP_STATUSES,
TransientError,
Expand Down Expand Up @@ -151,10 +152,7 @@ export class RevenueSettlementService {
await this.settlementStore.create(settlement);
} catch (error) {
errors++;
console.error(
`Settlement ${settlementId} failed for dev ${developerId}:`,
this.getErrorMessage(error)
);
logger.error({ settlementId, developerId, error: this.getErrorMessage(error) }, 'Settlement failed for dev');
continue;
}

Expand Down Expand Up @@ -264,7 +262,7 @@ export class RevenueSettlementService {
completed++;
} catch (updateError) {
errors++;
console.warn(
logger.warn(
{ settlementId: settlement.id, error: updateError },
'Failed to update settlement to completed — skipping',
);
Expand All @@ -279,7 +277,7 @@ export class RevenueSettlementService {
failed++;
} catch (updateError) {
errors++;
console.warn(
logger.warn(
{ settlementId: settlement.id, error: updateError },
'Failed to update settlement to failed — skipping',
);
Expand All @@ -292,7 +290,7 @@ export class RevenueSettlementService {
retried++;
} catch (updateError) {
errors++;
console.warn(
logger.warn(
{ settlementId: settlement.id, error: updateError },
'Failed to schedule retry for settlement — skipping',
);
Expand All @@ -305,14 +303,14 @@ export class RevenueSettlementService {
failed++;
} catch (updateError) {
errors++;
console.warn(
logger.warn(
{ settlementId: settlement.id, error: updateError },
'Failed to update settlement to failed — skipping',
);
}

if (!transactionCode) {
console.warn(
logger.warn(
{ settlementId: settlement.id },
'Horizon returned tx_failed but missing result_codes',
);
Expand All @@ -325,28 +323,28 @@ export class RevenueSettlementService {
failed++;
} catch (updateError) {
errors++;
console.warn(
logger.warn(
{ settlementId: settlement.id, error: updateError },
'Failed to update settlement to failed (not found) — skipping',
);
}

console.warn(
logger.warn(
{ settlementId: settlement.id },
'Horizon did not find transaction',
);
} else {
// Unexpected response shape; leave as pending and log warning
errors++;
console.warn(
logger.warn(
{ settlementId: settlement.id },
'Unexpected Horizon response shape — leaving settlement pending',
);
}
} catch (error) {
// Catch any exception during per-settlement processing to continue batch
errors++;
console.warn(
logger.warn(
{ settlementId: settlement.id, error },
'Failed to sync settlement status — skipping',
);
Expand Down Expand Up @@ -438,16 +436,10 @@ export class RevenueSettlementService {
clearTxHash ? null : undefined,
);
} catch (statusError) {
console.error(
`Settlement ${settlementId} failed for dev ${developerId} and could not persist failure status:`,
this.getErrorMessage(statusError),
);
logger.error({ settlementId, developerId, error: this.getErrorMessage(statusError) }, 'Settlement failed for dev and could not persist failure status');
}

console.error(
`Settlement ${settlementId} failed for dev ${developerId}:`,
errorMessage ?? 'Unknown settlement failure',
);
logger.error({ settlementId, developerId, error: errorMessage ?? 'Unknown settlement failure' }, 'Settlement failed for dev');
}

private getErrorMessage(error: unknown): string {
Expand Down
Loading