@cedros/login-sidecar 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -1,52 +1,58 @@
1
1
  # Privacy Cash Sidecar Configuration
2
2
  #
3
3
  # This sidecar handles Privacy Cash operations for SSS embedded wallets.
4
- # User keypairs are reconstructed from SSS shares and passed to endpoints.
4
+ # Most settings are stored in the database and managed via admin UI.
5
5
 
6
- # Port to listen on (default: 3100)
7
- PORT=3100
6
+ # =============================================================================
7
+ # Required Environment Variables
8
+ # =============================================================================
8
9
 
9
- # Host/interface to bind to (default: 127.0.0.1)
10
- # Use 0.0.0.0 if you need to accept connections from outside the machine/container.
11
- HOST=127.0.0.1
10
+ # PostgreSQL connection string (same database as main server)
11
+ DATABASE_URL=postgres://user:password@localhost:5432/cedros_login
12
12
 
13
- # API key for authentication (required)
13
+ # API key for authenticating requests from the main server
14
14
  # Generate with: openssl rand -hex 32
15
15
  SIDECAR_API_KEY=your-secure-api-key-here
16
16
 
17
- # Solana network: 'devnet' or 'mainnet-beta' (default: devnet). 'mainnet' is accepted as an alias.
18
- SOLANA_NETWORK=devnet
17
+ # =============================================================================
18
+ # Optional Environment Variables (for local dev only)
19
+ # =============================================================================
19
20
 
20
- # Solana RPC URL (optional, defaults based on network)
21
- # SOLANA_RPC_URL=https://api.devnet.solana.com
21
+ # Port to listen on (default: 3100)
22
+ PORT=3100
22
23
 
23
- # Privacy Cash program ID (required)
24
- # Mainnet: 9fhQBbumKEFuXtMBDw8AaQyAjCorLGJQiS3skWZdQyQD
25
- PRIVACY_CASH_PROGRAM_ID=9fhQBbumKEFuXtMBDw8AaQyAjCorLGJQiS3skWZdQyQD
24
+ # Host/interface to bind to (default: 127.0.0.1)
25
+ # Use 0.0.0.0 if you need to accept connections from outside the machine/container
26
+ HOST=127.0.0.1
26
27
 
27
- # Company wallet address for withdrawals (required)
28
- # This is where funds are sent when withdrawing from user Privacy Cash accounts
29
- COMPANY_WALLET_ADDRESS=
28
+ # =============================================================================
29
+ # Settings Managed via Admin UI (stored in database)
30
+ # =============================================================================
31
+ #
32
+ # The following settings are configured in the admin dashboard:
33
+ #
34
+ # Credit System > Deposits:
35
+ # - Solana RPC URL (solana_rpc_url)
36
+ # - Solana Network (solana_network)
37
+ #
38
+ # Credit System > Treasury:
39
+ # - Treasury Wallet Address (treasury_wallet_address)
40
+ #
41
+ # Login Server > Integrations:
42
+ # - Jupiter API Key (jupiter_api_key)
30
43
 
31
44
  # =============================================================================
32
- # Jupiter Ultra API (for gasless SPL token swaps)
45
+ # Infrastructure Settings (hardcoded with optional env override)
33
46
  # =============================================================================
34
- # Jupiter API URL (default: https://api.jup.ag/ultra/v1)
35
- # JUPITER_API_URL=https://api.jup.ag/ultra/v1
47
+ # These have sensible defaults and typically don't need changing.
48
+ # Only set if you need to override the defaults.
36
49
 
37
- # Jupiter API key (REQUIRED - get free key at https://portal.jup.ag)
38
- JUPITER_API_KEY=
50
+ # Privacy Cash Solana program ID (default: mainnet program)
51
+ # PRIVACY_CASH_PROGRAM_ID=9fhQBbumKEFuXtMBDw8AaQyAjCorLGJQiS3skWZdQyQD
39
52
 
40
- # Minimum swap amount in USD for gasless swaps (default: 10)
41
- # Jupiter requires ~$10 minimum for gasless swaps
42
- JUPITER_MIN_SWAP_USD=10
53
+ # Jupiter minimum swap amount in USD (default: 10, Jupiter's gasless minimum)
54
+ # JUPITER_MIN_SWAP_USD=10
43
55
 
44
- # Jupiter Ultra API rate limit (requests per 10 seconds)
45
- # Used for swap queue throttling. Ultra API uses dynamic rate limiting
46
- # that scales with your 24-hour swap volume:
47
- # $0 volume: 50 requests / 10 seconds
48
- # $10,000 volume: 51 requests / 10 seconds
49
- # $100,000 volume: 61 requests / 10 seconds
50
- # $1M volume: 165 requests / 10 seconds
51
- # Note: This is NOT tied to Pro plan tiers - it scales automatically
52
- JUPITER_RATE_LIMIT=50
56
+ # Jupiter Ultra API rate limit - requests per 10 seconds (default: 50 = 5 RPS)
57
+ # Ultra API scales dynamically based on your 24-hour swap volume
58
+ # JUPITER_RATE_LIMIT=50
package/dist/config.d.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  /**
2
- * Sidecar configuration loaded from environment variables
2
+ * Sidecar configuration
3
+ *
4
+ * Minimal env vars (API key, port, host, database URL) plus database-sourced settings.
5
+ * All configurable settings are stored in the database and managed via admin UI.
3
6
  */
4
7
  export interface Config {
5
8
  port: number;
6
9
  host: string;
7
10
  apiKey: string;
11
+ databaseUrl: string;
8
12
  solanaRpcUrl: string;
9
13
  solanaNetwork: 'devnet' | 'mainnet-beta';
10
14
  privacyCashProgramId: string;
@@ -20,4 +24,18 @@ export interface Config {
20
24
  rateLimit: number;
21
25
  };
22
26
  }
23
- export declare function loadConfig(): Config;
27
+ export declare function resolveJupiterRateLimit(): number;
28
+ /**
29
+ * Load configuration from environment and database
30
+ *
31
+ * Required env vars:
32
+ * - DATABASE_URL: PostgreSQL connection string
33
+ * - SIDECAR_API_KEY: API key for authenticating requests to sidecar
34
+ *
35
+ * Optional env vars (for local dev/testing only):
36
+ * - PORT: Listen port (default: 3100)
37
+ * - HOST: Listen host (default: 127.0.0.1)
38
+ *
39
+ * All other settings are loaded from the database (system_settings table)
40
+ */
41
+ export declare function loadConfig(): Promise<Config>;
package/dist/config.js CHANGED
@@ -1,9 +1,14 @@
1
1
  "use strict";
2
2
  /**
3
- * Sidecar configuration loaded from environment variables
3
+ * Sidecar configuration
4
+ *
5
+ * Minimal env vars (API key, port, host, database URL) plus database-sourced settings.
6
+ * All configurable settings are stored in the database and managed via admin UI.
4
7
  */
5
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.resolveJupiterRateLimit = resolveJupiterRateLimit;
6
10
  exports.loadConfig = loadConfig;
11
+ const settings_1 = require("./db/settings");
7
12
  function getEnvRequired(key) {
8
13
  const value = process.env[key];
9
14
  if (!value) {
@@ -14,27 +19,73 @@ function getEnvRequired(key) {
14
19
  function getEnvOptional(key, defaultValue) {
15
20
  return process.env[key] || defaultValue;
16
21
  }
17
- function loadConfig() {
18
- const rawNetwork = getEnvOptional('SOLANA_NETWORK', 'devnet');
19
- const network = rawNetwork === 'mainnet' ? 'mainnet-beta' : rawNetwork;
20
- if (network !== 'devnet' && network !== 'mainnet-beta') {
21
- throw new Error(`Invalid SOLANA_NETWORK: ${rawNetwork}. Must be 'devnet' or 'mainnet-beta' (or 'mainnet' alias)`);
22
+ function resolveJupiterRateLimit() {
23
+ // Backward compatibility:
24
+ // - JUPITER_RATE_LIMIT is the canonical key used by runtime config
25
+ // - JUPITER_RPS is a legacy/docs alias supported for compatibility
26
+ const raw = process.env.JUPITER_RATE_LIMIT ||
27
+ process.env.JUPITER_RPS ||
28
+ '50';
29
+ const parsed = parseInt(raw, 10);
30
+ if (!Number.isFinite(parsed) || parsed <= 0) {
31
+ return 50;
22
32
  }
33
+ // Conservative upper bound to avoid accidental traffic spikes from bad config.
34
+ return Math.min(parsed, 500);
35
+ }
36
+ /**
37
+ * Load configuration from environment and database
38
+ *
39
+ * Required env vars:
40
+ * - DATABASE_URL: PostgreSQL connection string
41
+ * - SIDECAR_API_KEY: API key for authenticating requests to sidecar
42
+ *
43
+ * Optional env vars (for local dev/testing only):
44
+ * - PORT: Listen port (default: 3100)
45
+ * - HOST: Listen host (default: 127.0.0.1)
46
+ *
47
+ * All other settings are loaded from the database (system_settings table)
48
+ */
49
+ async function loadConfig() {
50
+ const databaseUrl = getEnvRequired('DATABASE_URL');
51
+ const apiKey = getEnvRequired('SIDECAR_API_KEY');
52
+ const port = parseInt(getEnvOptional('PORT', '3100'), 10);
53
+ // SC-08: Validate port is a usable number
54
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
55
+ throw new Error(`Invalid PORT value: must be 1-65535`);
56
+ }
57
+ const host = getEnvOptional('HOST', '127.0.0.1');
58
+ // Fetch settings from database
59
+ const dbSettings = await (0, settings_1.fetchDbSettings)(databaseUrl);
60
+ // Always mainnet
61
+ const network = 'mainnet-beta';
62
+ // Validate required database settings
63
+ if (!dbSettings.companyWalletAddress) {
64
+ throw new Error('Missing required setting: treasury_wallet_address (set via admin UI)');
65
+ }
66
+ // Infrastructure settings with hardcoded defaults (env can override)
67
+ const privacyCashProgramId = getEnvOptional('PRIVACY_CASH_PROGRAM_ID', '9fhQBbumKEFuXtMBDw8AaQyAjCorLGJQiS3skWZdQyQD');
68
+ const _jupiterMinSwapUsdRaw = parseFloat(getEnvOptional('JUPITER_MIN_SWAP_USD', '10'));
69
+ // SC-13: Reject non-finite or non-positive values; fall back to safe default.
70
+ const jupiterMinSwapUsd = Number.isFinite(_jupiterMinSwapUsdRaw) && _jupiterMinSwapUsdRaw > 0
71
+ ? _jupiterMinSwapUsdRaw
72
+ : 10;
73
+ // Ultra API rate limit: starts at 5 RPS, scales with 24-hour swap volume
74
+ const jupiterRateLimit = resolveJupiterRateLimit();
23
75
  return {
24
- port: parseInt(getEnvOptional('PORT', '3100'), 10),
25
- host: getEnvOptional('HOST', '127.0.0.1'),
26
- apiKey: getEnvRequired('SIDECAR_API_KEY'),
27
- solanaRpcUrl: getEnvOptional('SOLANA_RPC_URL', network === 'devnet'
28
- ? 'https://api.devnet.solana.com'
29
- : 'https://api.mainnet-beta.solana.com'),
76
+ port,
77
+ host,
78
+ apiKey,
79
+ databaseUrl,
80
+ solanaRpcUrl: dbSettings.solanaRpcUrl || 'https://api.mainnet-beta.solana.com',
30
81
  solanaNetwork: network,
31
- privacyCashProgramId: getEnvRequired('PRIVACY_CASH_PROGRAM_ID'),
32
- companyWalletAddress: getEnvRequired('COMPANY_WALLET_ADDRESS'),
82
+ privacyCashProgramId,
83
+ companyWalletAddress: dbSettings.companyWalletAddress,
33
84
  jupiter: {
34
- apiUrl: getEnvOptional('JUPITER_API_URL', 'https://api.jup.ag/ultra/v1'),
35
- apiKey: process.env.JUPITER_API_KEY || null,
36
- minSwapUsd: parseFloat(getEnvOptional('JUPITER_MIN_SWAP_USD', '10')),
37
- rateLimit: parseInt(getEnvOptional('JUPITER_RATE_LIMIT', '50'), 10),
85
+ apiUrl: 'https://api.jup.ag/ultra/v1',
86
+ apiKey: dbSettings.jupiterApiKey,
87
+ minSwapUsd: jupiterMinSwapUsd,
88
+ rateLimit: jupiterRateLimit,
38
89
  },
39
90
  };
40
91
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Database settings loader for sidecar configuration
3
+ *
4
+ * Fetches runtime settings from the system_settings table in the main database.
5
+ * Falls back to defaults if database is unavailable or settings are missing.
6
+ */
7
+ export interface DbSettings {
8
+ solanaRpcUrl: string | null;
9
+ companyWalletAddress: string | null;
10
+ jupiterApiKey: string | null;
11
+ }
12
+ /**
13
+ * Fetch settings from the database
14
+ * Returns null values for any settings not found
15
+ */
16
+ export declare function fetchDbSettings(databaseUrl: string): Promise<DbSettings>;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ /**
3
+ * Database settings loader for sidecar configuration
4
+ *
5
+ * Fetches runtime settings from the system_settings table in the main database.
6
+ * Falls back to defaults if database is unavailable or settings are missing.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.fetchDbSettings = fetchDbSettings;
10
+ const pg_1 = require("pg");
11
+ /**
12
+ * Fetch settings from the database
13
+ * Returns null values for any settings not found
14
+ */
15
+ async function fetchDbSettings(databaseUrl) {
16
+ const pool = new pg_1.Pool({
17
+ connectionString: databaseUrl,
18
+ max: 1,
19
+ connectionTimeoutMillis: 5_000,
20
+ });
21
+ try {
22
+ const result = await pool.query(`SELECT key, value FROM system_settings
23
+ WHERE key IN (
24
+ 'solana_rpc_url',
25
+ 'treasury_wallet_address',
26
+ 'jupiter_api_key'
27
+ )`);
28
+ const settings = {};
29
+ for (const row of result.rows) {
30
+ settings[row.key] = row.value;
31
+ }
32
+ return {
33
+ solanaRpcUrl: settings['solana_rpc_url'] || null,
34
+ companyWalletAddress: settings['treasury_wallet_address'] || null,
35
+ jupiterApiKey: settings['jupiter_api_key'] || null,
36
+ };
37
+ }
38
+ finally {
39
+ // CI-05: Wrap pool cleanup in try/catch to prevent unhandled rejection
40
+ try {
41
+ await pool.end();
42
+ }
43
+ catch (cleanupErr) {
44
+ console.warn('Failed to close settings pool:', cleanupErr);
45
+ }
46
+ }
47
+ }
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ const express_1 = __importDefault(require("express"));
22
22
  const config_js_1 = require("./config.js");
23
23
  const auth_js_1 = require("./middleware/auth.js");
24
24
  const rateLimit_js_1 = require("./middleware/rateLimit.js");
25
+ const requestId_js_1 = require("./middleware/requestId.js");
25
26
  const solana_js_1 = require("./services/solana.js");
26
27
  const privacy_cash_js_1 = require("./services/privacy-cash.js");
27
28
  const jupiter_js_1 = require("./services/jupiter.js");
@@ -29,12 +30,13 @@ const health_js_1 = require("./routes/health.js");
29
30
  const deposit_js_1 = require("./routes/deposit.js");
30
31
  const withdraw_js_1 = require("./routes/withdraw.js");
31
32
  const batch_js_1 = require("./routes/batch.js");
33
+ const transfer_js_1 = require("./routes/transfer.js");
32
34
  const verify_js_1 = require("./routes/verify.js");
33
35
  const redactRpcUrl_js_1 = require("./utils/redactRpcUrl.js");
34
36
  async function main() {
35
37
  console.log('[Sidecar] Starting Cedros login sidecar...');
36
- // Load configuration
37
- const config = (0, config_js_1.loadConfig)();
38
+ // Load configuration from env + database
39
+ const config = await (0, config_js_1.loadConfig)();
38
40
  console.log(`[Sidecar] Network: ${config.solanaNetwork}`);
39
41
  console.log(`[Sidecar] RPC: ${(0, redactRpcUrl_js_1.redactRpcUrl)(config.solanaRpcUrl)}`);
40
42
  console.log(`[Sidecar] Host: ${config.host}`);
@@ -54,6 +56,7 @@ async function main() {
54
56
  // Create Express app
55
57
  const app = (0, express_1.default)();
56
58
  // Middleware
59
+ app.use((0, requestId_js_1.createRequestIdMiddleware)());
57
60
  app.use(express_1.default.json({ limit: '1mb' }));
58
61
  // SIDE-02: Basic in-memory rate limiting (protects sidecar from accidental overload)
59
62
  app.use((0, rateLimit_js_1.createRateLimitMiddleware)({ windowMs: 10_000, maxRequests: 200 }));
@@ -67,17 +70,19 @@ async function main() {
67
70
  next();
68
71
  });
69
72
  app.use((0, auth_js_1.createAuthMiddleware)(config.apiKey));
70
- // Request logging (simple, non-blocking)
73
+ // Request logging with correlation ID (simple, non-blocking)
71
74
  app.use((req, _res, next) => {
72
- console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
75
+ const rid = req.headers['x-request-id'] || '-';
76
+ console.log(`[${new Date().toISOString()}] [${rid}] ${req.method} ${req.path}`);
73
77
  next();
74
78
  });
75
79
  // Routes
76
80
  app.use((0, health_js_1.createHealthRoutes)(solanaService, privacyCashService));
77
81
  app.use((0, deposit_js_1.createDepositRoutes)(privacyCashService, jupiterService));
78
- app.use((0, withdraw_js_1.createWithdrawRoutes)(privacyCashService, jupiterService));
82
+ app.use((0, withdraw_js_1.createWithdrawRoutes)(privacyCashService));
79
83
  app.use((0, verify_js_1.createVerifyRoutes)(solanaService));
80
- app.use('/batch', (0, batch_js_1.createBatchRouter)(config));
84
+ app.use((0, transfer_js_1.createTransferRoutes)(solanaService));
85
+ app.use('/batch', (0, batch_js_1.createBatchRouter)(jupiterService));
81
86
  // 404 handler
82
87
  app.use((_req, res) => {
83
88
  res.status(404).json({ error: 'Not found' });
@@ -96,6 +101,23 @@ async function main() {
96
101
  server.keepAliveTimeout = 5_000;
97
102
  server.headersTimeout = 15_000;
98
103
  server.requestTimeout = 30_000;
104
+ // SC-03: Graceful shutdown — drain active connections before exiting
105
+ const SHUTDOWN_TIMEOUT_MS = 30_000;
106
+ for (const signal of ['SIGTERM', 'SIGINT']) {
107
+ process.on(signal, () => {
108
+ console.log(`[Sidecar] Received ${signal}, shutting down gracefully...`);
109
+ server.close(() => {
110
+ console.log('[Sidecar] All connections drained. Exiting.');
111
+ process.exit(0);
112
+ });
113
+ // SC-19: unref() so the timer doesn't keep the event loop alive if everything
114
+ // else has already exited cleanly before the timeout fires.
115
+ setTimeout(() => {
116
+ console.error('[Sidecar] Forced shutdown after timeout');
117
+ process.exit(1);
118
+ }, SHUTDOWN_TIMEOUT_MS).unref();
119
+ });
120
+ }
99
121
  }
100
122
  main().catch((error) => {
101
123
  console.error('[Sidecar] Fatal error:', error);
@@ -6,6 +6,10 @@
6
6
  */
7
7
  import { Request, Response, NextFunction } from 'express';
8
8
  /**
9
- * Create auth middleware with the given API key
9
+ * Create auth middleware with the given API key.
10
+ *
11
+ * Both keys are SHA-256 hashed before comparison. This eliminates
12
+ * the length-leak timing side-channel (S-03r) while keeping
13
+ * constant-time equality via timingSafeEqual on fixed 32-byte digests.
10
14
  */
11
15
  export declare function createAuthMiddleware(apiKey: string): (req: Request, res: Response, next: NextFunction) => void;
@@ -8,14 +8,23 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.createAuthMiddleware = createAuthMiddleware;
10
10
  const crypto_1 = require("crypto");
11
+ const paths_js_1 = require("./paths.js");
12
+ /** Hash a key with SHA-256 so all comparisons use fixed-length buffers */
13
+ function hashKey(key) {
14
+ return (0, crypto_1.createHash)('sha256').update(key).digest();
15
+ }
11
16
  /**
12
- * Create auth middleware with the given API key
17
+ * Create auth middleware with the given API key.
18
+ *
19
+ * Both keys are SHA-256 hashed before comparison. This eliminates
20
+ * the length-leak timing side-channel (S-03r) while keeping
21
+ * constant-time equality via timingSafeEqual on fixed 32-byte digests.
13
22
  */
14
23
  function createAuthMiddleware(apiKey) {
15
- const apiKeyBuffer = Buffer.from(apiKey);
24
+ const apiKeyHash = hashKey(apiKey);
16
25
  return (req, res, next) => {
17
26
  // Skip auth for health check
18
- if (req.path === '/health') {
27
+ if (req.path === paths_js_1.HEALTH_PATH) {
19
28
  return next();
20
29
  }
21
30
  const authHeader = req.headers.authorization;
@@ -28,11 +37,9 @@ function createAuthMiddleware(apiKey) {
28
37
  res.status(401).json({ error: 'Invalid Authorization header format. Expected: Bearer <token>' });
29
38
  return;
30
39
  }
31
- const providedKey = match[1];
32
- const providedBuffer = Buffer.from(providedKey);
33
- // Timing-safe comparison to prevent timing attacks
34
- if (providedBuffer.length !== apiKeyBuffer.length ||
35
- !(0, crypto_1.timingSafeEqual)(providedBuffer, apiKeyBuffer)) {
40
+ const providedHash = hashKey(match[1]);
41
+ // Constant-time comparison on fixed-length SHA-256 digests
42
+ if (!(0, crypto_1.timingSafeEqual)(providedHash, apiKeyHash)) {
36
43
  res.status(401).json({ error: 'Invalid API key' });
37
44
  return;
38
45
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * SC-18: Shared path constants for middleware to avoid duplication.
3
+ */
4
+ /** Health check path — exempt from auth and rate limiting. */
5
+ export declare const HEALTH_PATH = "/health";
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ /**
3
+ * SC-18: Shared path constants for middleware to avoid duplication.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HEALTH_PATH = void 0;
7
+ /** Health check path — exempt from auth and rate limiting. */
8
+ exports.HEALTH_PATH = '/health';
@@ -1,16 +1,53 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createRateLimitMiddleware = createRateLimitMiddleware;
4
+ const paths_js_1 = require("./paths.js");
5
+ /** Run deterministic cleanup at most once per interval. */
6
+ const CLEANUP_INTERVAL_MS = 30_000;
7
+ /** Hard cap to prevent unbounded memory growth under high-cardinality traffic. */
8
+ const MAX_BUCKETS = 10_000;
4
9
  function createRateLimitMiddleware({ windowMs, maxRequests }) {
5
10
  const buckets = new Map();
11
+ let lastCleanupMs = 0;
12
+ function cleanupExpired(now) {
13
+ for (const [ip, entry] of buckets) {
14
+ if (entry.resetAtMs <= now) {
15
+ buckets.delete(ip);
16
+ }
17
+ }
18
+ }
19
+ function evictOldestBucket() {
20
+ let oldestKey = null;
21
+ let oldestResetAt = Number.POSITIVE_INFINITY;
22
+ for (const [ip, entry] of buckets) {
23
+ if (entry.resetAtMs < oldestResetAt) {
24
+ oldestResetAt = entry.resetAtMs;
25
+ oldestKey = ip;
26
+ }
27
+ }
28
+ if (oldestKey) {
29
+ buckets.delete(oldestKey);
30
+ }
31
+ }
6
32
  return (req, res, next) => {
7
- if (req.path === '/health') {
33
+ if (req.path === paths_js_1.HEALTH_PATH) {
8
34
  next();
9
35
  return;
10
36
  }
11
37
  const now = Date.now();
12
- const ip = req.ip || req.socket.remoteAddress || 'unknown';
38
+ // Deterministic cleanup to keep memory bounded.
39
+ if (now - lastCleanupMs >= CLEANUP_INTERVAL_MS || buckets.size >= MAX_BUCKETS) {
40
+ cleanupExpired(now);
41
+ lastCleanupMs = now;
42
+ }
43
+ // SC-02: Always use socket address for rate limiting. req.ip trusts
44
+ // X-Forwarded-For when trust-proxy is enabled, allowing bypass.
45
+ const ip = req.socket.remoteAddress || 'unknown';
13
46
  const existing = buckets.get(ip);
47
+ // If we're at capacity and this is a new key, evict the oldest bucket.
48
+ if (!existing && buckets.size >= MAX_BUCKETS) {
49
+ evictOldestBucket();
50
+ }
14
51
  if (!existing || existing.resetAtMs <= now) {
15
52
  buckets.set(ip, { count: 1, resetAtMs: now + windowMs });
16
53
  next();
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Request correlation ID middleware.
3
+ *
4
+ * Propagates incoming X-Request-ID header from the Rust server,
5
+ * or generates a new one if absent. Sets it on the response
6
+ * and makes it available via `req.headers['x-request-id']`.
7
+ */
8
+ import { Request, Response, NextFunction } from 'express';
9
+ export declare function createRequestIdMiddleware(): (req: Request, res: Response, next: NextFunction) => void;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /**
3
+ * Request correlation ID middleware.
4
+ *
5
+ * Propagates incoming X-Request-ID header from the Rust server,
6
+ * or generates a new one if absent. Sets it on the response
7
+ * and makes it available via `req.headers['x-request-id']`.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.createRequestIdMiddleware = createRequestIdMiddleware;
11
+ const node_crypto_1 = require("node:crypto");
12
+ // F-25: UUID v4 format validation to prevent request ID spoofing
13
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14
+ function createRequestIdMiddleware() {
15
+ return (req, res, next) => {
16
+ const existing = req.headers['x-request-id'];
17
+ // F-25: Only accept well-formed UUIDs; reject arbitrary strings
18
+ const requestId = (typeof existing === 'string' && UUID_RE.test(existing))
19
+ ? existing
20
+ : (0, node_crypto_1.randomUUID)();
21
+ // Ensure downstream code can read it from the request
22
+ req.headers['x-request-id'] = requestId;
23
+ // Echo back so callers can correlate
24
+ res.setHeader('X-Request-ID', requestId);
25
+ next();
26
+ };
27
+ }
@@ -7,5 +7,5 @@
7
7
  * into the company's preferred currency (USDC/USDT).
8
8
  */
9
9
  import { Router } from 'express';
10
- import { Config } from '../config.js';
11
- export declare function createBatchRouter(config: Config): Router;
10
+ import { JupiterService } from '../services/jupiter.js';
11
+ export declare function createBatchRouter(jupiterService: JupiterService): Router;
@@ -11,9 +11,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.createBatchRouter = createBatchRouter;
12
12
  const express_1 = require("express");
13
13
  const jupiter_js_1 = require("../services/jupiter.js");
14
- function createBatchRouter(config) {
14
+ function createBatchRouter(jupiterService) {
15
15
  const router = (0, express_1.Router)();
16
- const jupiterService = new jupiter_js_1.JupiterService(config);
17
16
  /**
18
17
  * POST /batch/swap
19
18
  *
@@ -30,7 +29,14 @@ function createBatchRouter(config) {
30
29
  error: 'Missing or invalid privateKey',
31
30
  });
32
31
  }
33
- if (!body.amountLamports || typeof body.amountLamports !== 'number' || body.amountLamports <= 0) {
32
+ // SC-02: Use safe integer for max lamports (SOL total supply ~585M = 5.85e17 lamports)
33
+ const MAX_LAMPORTS = 585_000_000_000_000_000;
34
+ if (!body.amountLamports ||
35
+ typeof body.amountLamports !== 'number' ||
36
+ !Number.isFinite(body.amountLamports) ||
37
+ !Number.isInteger(body.amountLamports) ||
38
+ body.amountLamports <= 0 ||
39
+ body.amountLamports > MAX_LAMPORTS) {
34
40
  return res.status(400).json({
35
41
  success: false,
36
42
  error: 'Missing or invalid amountLamports',
@@ -55,16 +61,22 @@ function createBatchRouter(config) {
55
61
  try {
56
62
  keypair = jupiter_js_1.JupiterService.parseKeypair(body.privateKey);
57
63
  }
58
- catch (e) {
59
- console.error('[Batch] Failed to parse private key:', e);
64
+ catch {
65
+ console.error('[Batch] Failed to parse private key');
60
66
  return res.status(400).json({
61
67
  success: false,
62
68
  error: 'Invalid private key format',
63
69
  });
64
70
  }
65
71
  console.log(`[Batch] Executing swap: ${body.amountLamports} lamports SOL -> ${body.outputCurrency}`);
66
- // Execute the swap
67
- const result = await jupiterService.swapFromSol(outputMint, body.amountLamports.toString(), keypair);
72
+ // Execute the swap — zero treasury key material immediately after use
73
+ let result;
74
+ try {
75
+ result = await jupiterService.swapFromSol(outputMint, body.amountLamports.toString(), keypair);
76
+ }
77
+ finally {
78
+ keypair.secretKey.fill(0);
79
+ }
68
80
  if (!result.success) {
69
81
  console.error(`[Batch] Swap failed: ${result.error}`);
70
82
  return res.status(500).json({
@@ -90,7 +102,7 @@ function createBatchRouter(config) {
90
102
  console.error('[Batch] Unexpected error:', error);
91
103
  return res.status(500).json({
92
104
  success: false,
93
- error: error instanceof Error ? error.message : 'Internal server error',
105
+ error: 'Internal server error',
94
106
  });
95
107
  }
96
108
  });