bulltrackers-module 1.0.621 → 1.0.623

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.
@@ -1,6 +1,7 @@
1
1
  // Firestore helper functions for fetching data from collections
2
- const { FieldValue } = require('@google-cloud/firestore');
2
+ const { FieldValue, Timestamp } = require('@google-cloud/firestore');
3
3
  const { dispatchSyncRequest } = require('../task_engine_helper.js');
4
+ const { sanitizeCid, sanitizeDocId } = require('../security_utils.js');
4
5
  const crypto = require('crypto');
5
6
 
6
7
  // 1. Fetch latest stored snapshots of user data from a user-centric collection
@@ -34,11 +35,14 @@ const crypto = require('crypto');
34
35
  */
35
36
  const latestUserCentricSnapshot = async (firestore, userId, collectionName, dataType, userType, documentName, fields = []) => {
36
37
  try {
37
- const baseRef = firestore.collection(userType).doc(userId).collection(collectionName);
38
+ // Sanitize user inputs to prevent injection attacks
39
+ const sanitizedUserId = sanitizeCid(userId);
40
+ const sanitizedDocName = documentName ? sanitizeDocId(documentName) : null;
41
+ const baseRef = firestore.collection(userType).doc(sanitizedUserId).collection(collectionName);
38
42
 
39
- if (documentName) {
43
+ if (sanitizedDocName) {
40
44
  // SCENARIO 1: Single Document
41
- let docRef = baseRef.doc(documentName);
45
+ let docRef = baseRef.doc(sanitizedDocName);
42
46
  // Apply field selection if fields are specified (Firestore projection)
43
47
  if (fields.length > 0) {
44
48
  docRef = docRef.select(...fields);
@@ -85,14 +89,14 @@ const pageCollection = async (firestore, dateStr, computationName, userId, lookb
85
89
  const docRef = firestore.collection('unified_insights').doc(dateKey)
86
90
  .collection('results').doc('popular-investor')
87
91
  .collection('computations').doc(computationName)
88
- .collection('pages').doc(userId);
92
+ .collection('pages').doc(sanitizedUserId);
89
93
  const docSnapshot = await docRef.get();
90
94
  if (docSnapshot.exists) {
91
95
  results.push({ date: dateKey, data: docSnapshot.data() });
92
96
  }
93
97
  }
94
98
  if (results.length === 0) {
95
- throw new Error(`No page data found for User ID ${userId} in computation ${computationName} within the last ${lookbackDays} days`);
99
+ throw new Error(`No page data found for User ID ${sanitizedUserId} in computation ${sanitizedComputationName} within the last ${lookbackDays} days`);
96
100
  }
97
101
  // Sort results by date descending (newest first) so the latest data is always first
98
102
  results.sort((a, b) => {
@@ -370,9 +374,12 @@ const manageUserWatchlist = async (db, userId, instruction, payload = {}) => {
370
374
  const todayStr = new Date().toISOString().split('T')[0]; // "2026-01-09"
371
375
  const batch = db.batch();
372
376
 
377
+ // Sanitize user inputs
378
+ const sanitizedUserId = sanitizeCid(userId);
379
+
373
380
  // 1. Determine Watchlist ID and Reference
374
- const watchlistId = payload.id || `watchlist_${Date.now()}_${Math.random().toString(16).substr(2, 8)}`;
375
- const userDocRef = db.collection('SignedInUsers').doc(userId).collection('watchlists').doc(watchlistId);
381
+ const watchlistId = payload.id ? sanitizeDocId(payload.id) : `watchlist_${Date.now()}_${Math.random().toString(16).substr(2, 8)}`;
382
+ const userDocRef = db.collection('SignedInUsers').doc(sanitizedUserId).collection('watchlists').doc(watchlistId);
376
383
 
377
384
  try {
378
385
  let addedItems = []; // Items newly added (needs Master Log + Current Counter Increment)
@@ -397,7 +404,7 @@ const manageUserWatchlist = async (db, userId, instruction, payload = {}) => {
397
404
  const newDocData = {
398
405
  ...payload,
399
406
  id: watchlistId,
400
- createdBy: userId,
407
+ createdBy: sanitizedUserId,
401
408
  createdAt: FieldValue.serverTimestamp(),
402
409
  updatedAt: FieldValue.serverTimestamp(),
403
410
  copyCount: 0,
@@ -447,7 +454,7 @@ const manageUserWatchlist = async (db, userId, instruction, payload = {}) => {
447
454
  totalUsers: FieldValue.increment(1),
448
455
  privateWatchlistCount: FieldValue.increment(isPrivate ? 1 : 0),
449
456
  publicWatchlistCount: FieldValue.increment(isPrivate ? 0 : 1),
450
- users: FieldValue.arrayUnion(userId)
457
+ users: FieldValue.arrayUnion(sanitizedUserId)
451
458
  }, { merge: true });
452
459
 
453
460
  // B. Update PI Real-time Counter (Increment)
@@ -459,12 +466,12 @@ const manageUserWatchlist = async (db, userId, instruction, payload = {}) => {
459
466
  batch.set(piCounterRef, {
460
467
  lastUpdated: FieldValue.serverTimestamp(),
461
468
  totalUsers: FieldValue.increment(1),
462
- userCids: FieldValue.arrayUnion(userId),
469
+ userCids: FieldValue.arrayUnion(sanitizedUserId),
463
470
  // Nested Map Update for Daily stats
464
471
  [dailyField]: {
465
472
  count: FieldValue.increment(1),
466
473
  timestamp: FieldValue.serverTimestamp(),
467
- userCids: FieldValue.arrayUnion(userId)
474
+ userCids: FieldValue.arrayUnion(sanitizedUserId)
468
475
  }
469
476
  }, { merge: true });
470
477
  });
@@ -618,10 +625,20 @@ const lookupCidByEmail = async (firestore, userEmail, firebaseUid = null) => {
618
625
  const normalizedEmails = emails.map(e => String(e).toLowerCase().trim());
619
626
 
620
627
  if (normalizedEmails.includes(normalizedEmail)) {
621
- // Optional: Validate Firebase UID if provided
622
- if (firebaseUid && verificationData.firebaseUids && Array.isArray(verificationData.firebaseUids)) {
623
- if (!verificationData.firebaseUids.includes(firebaseUid)) {
624
- console.warn(`[lookupCidByEmail] Email ${userEmail} matches CID ${cid} but Firebase UID ${firebaseUid} not in firebaseUids array`);
628
+ // Validate and enforce Firebase UID if provided
629
+ if (firebaseUid) {
630
+ if (verificationData.firebaseUids && Array.isArray(verificationData.firebaseUids)) {
631
+ // New users: strict enforcement
632
+ if (!verificationData.firebaseUids.includes(firebaseUid)) {
633
+ throw new Error(`Firebase UID not authorized for this CID`);
634
+ }
635
+ } else {
636
+ // Legacy users: populate firebaseUids array (migration)
637
+ // Note: A single CID can have multiple emails/UIDs for different accounts
638
+ await verificationRef.update({
639
+ firebaseUids: FieldValue.arrayUnion(firebaseUid)
640
+ });
641
+ console.log(`[Migration] Added Firebase UID ${firebaseUid} to CID ${cid}`);
625
642
  }
626
643
  }
627
644
 
@@ -666,10 +683,20 @@ const lookupCidByEmail = async (firestore, userEmail, firebaseUid = null) => {
666
683
  if (normalizedEmails.includes(normalizedEmail)) {
667
684
  const cid = verificationData.etoroCID || Number(userDoc.id);
668
685
 
669
- // Optional: Validate Firebase UID if provided
670
- if (firebaseUid && verificationData.firebaseUids && Array.isArray(verificationData.firebaseUids)) {
671
- if (!verificationData.firebaseUids.includes(firebaseUid)) {
672
- console.warn(`[lookupCidByEmail] Email ${userEmail} matches CID ${cid} but Firebase UID ${firebaseUid} not in firebaseUids array`);
686
+ // Validate and enforce Firebase UID if provided
687
+ if (firebaseUid) {
688
+ if (verificationData.firebaseUids && Array.isArray(verificationData.firebaseUids)) {
689
+ // New users: strict enforcement
690
+ if (!verificationData.firebaseUids.includes(firebaseUid)) {
691
+ throw new Error(`Firebase UID not authorized for this CID`);
692
+ }
693
+ } else {
694
+ // Legacy users: populate firebaseUids array (migration)
695
+ // Note: A single CID can have multiple emails/UIDs for different accounts
696
+ await verificationRef.update({
697
+ firebaseUids: FieldValue.arrayUnion(firebaseUid)
698
+ });
699
+ console.log(`[Migration] Added Firebase UID ${firebaseUid} to CID ${cid}`);
673
700
  }
674
701
  }
675
702
 
@@ -734,8 +761,7 @@ const requestPopularInvestorAddition = async (firestore, userId, piId, piUsernam
734
761
  piUsername,
735
762
  requestedBy: userId,
736
763
  requestedAt: FieldValue.serverTimestamp(),
737
- ttl: admin.firestore.Timestamp.fromDate(ttlDate)
738
-
764
+ ttl: Timestamp.fromDate(ttlDate)
739
765
  });
740
766
  const globalRequestRef = firestore.collection('PiAdditionRequestLogs').doc(todayStr).collection('requests').doc(piId);
741
767
  await globalRequestRef.set({
@@ -933,14 +959,18 @@ const hasUserCopiedPopularInvestor = async (firestore, userId, popularInvestorId
933
959
  // 3. A GLOBAL REVIEWS COLLECTION LIKE /PiReviews/YYYY-MM-DD/document_name containing a map of the reviews made each day, who made them, for which PI and text content
934
960
  // 4. We need a migration strategy to pull across the existing stored data from the old collection path into the new collection path upon read.
935
961
  const manageReviews = async (db, userId, action, params = {}) => {
962
+ // Sanitize user inputs
963
+ const sanitizedUserId = sanitizeCid(userId);
936
964
  const { piId } = params;
965
+ const sanitizedPiId = sanitizeCid(piId);
966
+
937
967
  const batch = db.batch();
938
968
  const todayStr = new Date().toISOString().split('T')[0];
939
969
 
940
- // Paths
941
- const newReviewPath = db.collection('SignedInUsers').doc(userId).collection('reviews').doc(piId.toString());
942
- const piReviewPath = db.collection('PopularInvestors').doc(piId.toString()).collection('reviews').doc(`${userId}_${piId}`);
943
- const legacyReviewPath = db.collection('pi_reviews').doc(`${userId}_${piId}`);
970
+ // Paths - all user inputs sanitized
971
+ const newReviewPath = db.collection('SignedInUsers').doc(sanitizedUserId).collection('reviews').doc(sanitizedPiId);
972
+ const piReviewPath = db.collection('PopularInvestors').doc(sanitizedPiId).collection('reviews').doc(`${sanitizedUserId}_${sanitizedPiId}`);
973
+ const legacyReviewPath = db.collection('pi_reviews').doc(`${sanitizedUserId}_${sanitizedPiId}`);
944
974
  const globalLogPath = db.collection('PiReviews').doc(todayStr).collection('shards').doc('daily_log');
945
975
 
946
976
  try {
@@ -2048,9 +2078,87 @@ const searchPopularInvestors = async (db, queryStr) => {
2048
2078
  // NEW: SYNC & RATE LIMITS (Rec 7, 9, 15)
2049
2079
  // ==========================================
2050
2080
 
2051
- const checkSyncRateLimits = async (db, targetId, requesterId, isDev) => {
2052
- if (isDev) return { allowed: true };
2081
+ const checkSyncRateLimits = async (db, targetId, requesterId, isDev, maxRequests = 1, windowMs = 6 * 60 * 60 * 1000) => {
2082
+ // Developers get higher limits (100 requests per hour globally, 10 per target) but still limited
2083
+ if (isDev) {
2084
+ const DEV_GLOBAL_LIMIT_MS = 60 * 60 * 1000; // 1 hour
2085
+ const DEV_GLOBAL_MAX_REQUESTS = 100; // Total requests per hour
2086
+ const DEV_PER_TARGET_LIMIT_MS = 60 * 60 * 1000; // 1 hour
2087
+ const DEV_PER_TARGET_MAX_REQUESTS = 10; // Requests per target per hour
2088
+ const now = Date.now();
2089
+
2090
+ // Check global developer limit (total requests across all targets)
2091
+ const globalDevRef = db.collection('developer_rate_limits').doc(String(requesterId)).collection('global').doc('latest');
2092
+ const globalDevSnap = await globalDevRef.get();
2093
+
2094
+ let globalRequestCount = 0;
2095
+ let globalWindowStart = now;
2096
+
2097
+ if (globalDevSnap.exists) {
2098
+ const globalData = globalDevSnap.data();
2099
+ globalRequestCount = globalData.requestCount || 0;
2100
+ globalWindowStart = globalData.windowStart?.toMillis() || now;
2101
+
2102
+ // Reset window if expired
2103
+ if (now - globalWindowStart >= DEV_GLOBAL_LIMIT_MS) {
2104
+ globalRequestCount = 0;
2105
+ globalWindowStart = now;
2106
+ }
2107
+ }
2108
+
2109
+ // Check per-target limit (prevent spamming specific users)
2110
+ const sanitizedTargetId = sanitizeCid(targetId);
2111
+ const perTargetRef = db.collection('user_sync_requests').doc(sanitizedTargetId).collection('developer_limits').doc(String(requesterId));
2112
+ const perTargetSnap = await perTargetRef.get();
2113
+
2114
+ let perTargetCount = 0;
2115
+ let perTargetWindowStart = now;
2116
+
2117
+ if (perTargetSnap.exists) {
2118
+ const perTargetData = perTargetSnap.data();
2119
+ perTargetCount = perTargetData.requestCount || 0;
2120
+ perTargetWindowStart = perTargetData.windowStart?.toMillis() || now;
2121
+
2122
+ // Reset window if expired
2123
+ if (now - perTargetWindowStart >= DEV_PER_TARGET_LIMIT_MS) {
2124
+ perTargetCount = 0;
2125
+ perTargetWindowStart = now;
2126
+ }
2127
+
2128
+ // Check per-target limit
2129
+ if (perTargetCount >= DEV_PER_TARGET_MAX_REQUESTS) {
2130
+ const waitMinutes = Math.ceil((perTargetWindowStart + DEV_PER_TARGET_LIMIT_MS - now) / 60000);
2131
+ return {
2132
+ allowed: false,
2133
+ message: `Developer rate limit exceeded for this target. Try again in ${waitMinutes} minutes.`
2134
+ };
2135
+ }
2136
+ }
2137
+
2138
+ // Check global limit
2139
+ if (globalRequestCount >= DEV_GLOBAL_MAX_REQUESTS) {
2140
+ const waitMinutes = Math.ceil((globalWindowStart + DEV_GLOBAL_LIMIT_MS - now) / 60000);
2141
+ return {
2142
+ allowed: false,
2143
+ message: `Developer global rate limit exceeded. Try again in ${waitMinutes} minutes.`
2144
+ };
2145
+ }
2146
+
2147
+ // Update both counters
2148
+ await globalDevRef.set({
2149
+ requestCount: globalRequestCount + 1,
2150
+ windowStart: Timestamp.fromMillis(globalWindowStart)
2151
+ }, { merge: true });
2152
+
2153
+ await perTargetRef.set({
2154
+ requestCount: perTargetCount + 1,
2155
+ windowStart: Timestamp.fromMillis(perTargetWindowStart)
2156
+ }, { merge: true });
2157
+
2158
+ return { allowed: true };
2159
+ }
2053
2160
 
2161
+ // Regular users: 1 request per 6 hours
2054
2162
  const RATE_LIMIT_MS = 6 * 60 * 60 * 1000; // 6 hours
2055
2163
  const now = Date.now();
2056
2164
 
@@ -2681,33 +2789,42 @@ const getWatchlistTriggerCounts = async (db, userId, watchlistId) => {
2681
2789
  const piCid = String(item.cid);
2682
2790
  let totalCount = 0;
2683
2791
 
2684
- // Check each of the last 7 days
2685
- for (const dateStr of dates) {
2686
- try {
2687
- const dayDoc = await db.collection('PIAlertHistoryData')
2792
+ // Check each of the last 7 days - PARALLELIZED to fix N+1 query problem
2793
+ const dayDocs = await Promise.all(
2794
+ dates.map(dateStr =>
2795
+ db.collection('PIAlertHistoryData')
2688
2796
  .doc(dateStr)
2689
- .get();
2797
+ .get()
2798
+ .catch(e => {
2799
+ console.warn(`Error fetching date ${dateStr} for PI ${piCid}: ${e.message}`);
2800
+ return null; // Return null on error instead of throwing
2801
+ })
2802
+ )
2803
+ );
2804
+
2805
+ // Process all day documents
2806
+ for (const dayDoc of dayDocs) {
2807
+ if (!dayDoc || !dayDoc.exists) continue;
2808
+
2809
+ try {
2810
+ const dayData = dayDoc.data();
2811
+ const piData = dayData[piCid];
2690
2812
 
2691
- if (dayDoc.exists) {
2692
- const dayData = dayDoc.data();
2693
- const piData = dayData[piCid];
2694
-
2695
- if (piData) {
2696
- // Check all alert types for this PI
2697
- for (const [alertType, alertData] of Object.entries(piData)) {
2698
- if (alertType === 'lastUpdated' || typeof alertData !== 'object') continue;
2699
-
2700
- // Check if triggeredFor array contains this user
2701
- const triggeredFor = alertData.triggeredFor || [];
2702
- if (Array.isArray(triggeredFor) && triggeredFor.includes(userCidStr)) {
2703
- totalCount += alertData.count || 1; // Count each trigger
2704
- }
2813
+ if (piData) {
2814
+ // Check all alert types for this PI
2815
+ for (const [alertType, alertData] of Object.entries(piData)) {
2816
+ if (alertType === 'lastUpdated' || typeof alertData !== 'object') continue;
2817
+
2818
+ // Check if triggeredFor array contains this user
2819
+ const triggeredFor = alertData.triggeredFor || [];
2820
+ if (Array.isArray(triggeredFor) && triggeredFor.includes(userCidStr)) {
2821
+ totalCount += alertData.count || 1; // Count each trigger
2705
2822
  }
2706
2823
  }
2707
2824
  }
2708
2825
  } catch (e) {
2709
- // Skip this day if error (document might not exist)
2710
- console.warn(`Error checking date ${dateStr} for PI ${piCid}: ${e.message}`);
2826
+ // Skip this day if error processing
2827
+ console.warn(`Error processing day data for PI ${piCid}: ${e.message}`);
2711
2828
  }
2712
2829
  }
2713
2830
 
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Security utility functions for input validation and sanitization
3
+ */
4
+
5
+ /**
6
+ * Sanitizes a CID (Customer ID) to prevent injection attacks
7
+ * @param {string|number} cid - The CID to sanitize
8
+ * @returns {string} - Sanitized CID
9
+ * @throws {Error} - If CID is invalid
10
+ */
11
+ function sanitizeCid(cid) {
12
+ if (cid === null || cid === undefined) {
13
+ throw new Error('CID is required');
14
+ }
15
+
16
+ // Convert to string and remove all non-numeric characters
17
+ const sanitized = String(cid).replace(/[^0-9]/g, '');
18
+
19
+ // Validate length and format
20
+ if (!sanitized || sanitized.length === 0) {
21
+ throw new Error('Invalid CID format: must contain at least one digit');
22
+ }
23
+
24
+ if (sanitized.length > 20) {
25
+ throw new Error('Invalid CID format: too long (max 20 digits)');
26
+ }
27
+
28
+ return sanitized;
29
+ }
30
+
31
+ /**
32
+ * Sanitizes a document ID to prevent path traversal attacks
33
+ * @param {string} docId - The document ID to sanitize
34
+ * @returns {string} - Sanitized document ID
35
+ * @throws {Error} - If document ID is invalid
36
+ */
37
+ function sanitizeDocId(docId) {
38
+ if (!docId || typeof docId !== 'string') {
39
+ throw new Error('Document ID is required and must be a string');
40
+ }
41
+
42
+ // Remove path traversal attempts and other dangerous characters
43
+ const sanitized = docId
44
+ .replace(/\.\./g, '') // Remove .. (path traversal)
45
+ .replace(/\//g, '') // Remove / (path separator)
46
+ .replace(/\\/g, '') // Remove \ (path separator)
47
+ .trim();
48
+
49
+ if (!sanitized || sanitized.length === 0) {
50
+ throw new Error('Invalid document ID: empty after sanitization');
51
+ }
52
+
53
+ if (sanitized.length > 200) {
54
+ throw new Error('Invalid document ID: too long (max 200 characters)');
55
+ }
56
+
57
+ return sanitized;
58
+ }
59
+
60
+ /**
61
+ * Validates and sanitizes a username
62
+ * @param {string} username - The username to validate
63
+ * @returns {string} - Sanitized username
64
+ * @throws {Error} - If username is invalid
65
+ */
66
+ function sanitizeUsername(username) {
67
+ if (!username || typeof username !== 'string') {
68
+ throw new Error('Username is required and must be a string');
69
+ }
70
+
71
+ const sanitized = username.trim();
72
+
73
+ if (sanitized.length === 0) {
74
+ throw new Error('Username cannot be empty');
75
+ }
76
+
77
+ if (sanitized.length > 100) {
78
+ throw new Error('Username too long (max 100 characters)');
79
+ }
80
+
81
+ // Allow alphanumeric, underscore, hyphen, and dot
82
+ if (!/^[a-zA-Z0-9._-]+$/.test(sanitized)) {
83
+ throw new Error('Username contains invalid characters');
84
+ }
85
+
86
+ return sanitized;
87
+ }
88
+
89
+ /**
90
+ * Validates batch operation size
91
+ * @param {number} size - The size of the batch operation
92
+ * @param {number} maxSize - Maximum allowed size (default: 1000)
93
+ * @throws {Error} - If batch size exceeds limit
94
+ */
95
+ function validateBatchSize(size, maxSize = 1000) {
96
+ if (typeof size !== 'number' || size < 0) {
97
+ throw new Error('Batch size must be a non-negative number');
98
+ }
99
+
100
+ if (size > maxSize) {
101
+ throw new Error(`Batch operation too large. Maximum ${maxSize} operations allowed, got ${size}`);
102
+ }
103
+ }
104
+
105
+ module.exports = {
106
+ sanitizeCid,
107
+ sanitizeDocId,
108
+ sanitizeUsername,
109
+ validateBatchSize
110
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Utility functions for request timeout protection
3
+ */
4
+
5
+ /**
6
+ * Wraps a promise with a timeout
7
+ * @param {Promise} promise - The promise to wrap
8
+ * @param {number} ms - Timeout in milliseconds (default: 10000)
9
+ * @param {string} errorMessage - Custom error message (optional)
10
+ * @returns {Promise} - Promise that rejects on timeout
11
+ */
12
+ function withTimeout(promise, ms = 10000, errorMessage = 'Request timeout') {
13
+ return Promise.race([
14
+ promise,
15
+ new Promise((_, reject) =>
16
+ setTimeout(() => reject(new Error(`${errorMessage} (${ms}ms)`)), ms)
17
+ )
18
+ ]);
19
+ }
20
+
21
+ /**
22
+ * Creates a timeout wrapper with a specific duration
23
+ * @param {number} ms - Timeout in milliseconds
24
+ * @returns {Function} - Function that wraps promises with the specified timeout
25
+ */
26
+ function createTimeoutWrapper(ms) {
27
+ return (promise, errorMessage) => withTimeout(promise, ms, errorMessage);
28
+ }
29
+
30
+ // Pre-configured timeout wrappers for common use cases
31
+ const timeouts = {
32
+ short: createTimeoutWrapper(5000), // 5 seconds
33
+ medium: createTimeoutWrapper(15000), // 15 seconds
34
+ long: createTimeoutWrapper(30000), // 30 seconds
35
+ veryLong: createTimeoutWrapper(60000) // 60 seconds
36
+ };
37
+
38
+ module.exports = {
39
+ withTimeout,
40
+ createTimeoutWrapper,
41
+ timeouts
42
+ };
@@ -6,6 +6,7 @@
6
6
  const express = require('express');
7
7
  const cors = require('cors');
8
8
  const rateLimit = require('express-rate-limit');
9
+ const { v4: uuidv4 } = require('uuid');
9
10
  const createRouter = require('./routes/index.js');
10
11
  const errorHandler = require('./middleware/error_handler.js');
11
12
 
@@ -27,6 +28,10 @@ function createApiV2App(config, dependencies) {
27
28
  const app = express();
28
29
  const { logger } = dependencies;
29
30
 
31
+ // Trust proxy - Required when behind a load balancer/proxy (e.g., Google Cloud Functions)
32
+ // This allows express-rate-limit to correctly identify client IPs from X-Forwarded-For headers
33
+ app.set('trust proxy', true);
34
+
30
35
  // CORS Configuration - Restrict to specific origins
31
36
  app.use(cors({
32
37
  origin: function (origin, callback) {
@@ -66,6 +71,13 @@ function createApiV2App(config, dependencies) {
66
71
  app.use('/verification', authLimiter);
67
72
  app.use('/', apiLimiter);
68
73
 
74
+ // Request ID tracking middleware (for debugging and error tracking)
75
+ app.use((req, res, next) => {
76
+ req.requestId = uuidv4();
77
+ res.setHeader('X-Request-ID', req.requestId);
78
+ next();
79
+ });
80
+
69
81
  app.use(express.json());
70
82
 
71
83
  // Health Check
@@ -6,6 +6,7 @@
6
6
  module.exports = (err, req, res, next) => {
7
7
  // Log full error internally for debugging
8
8
  console.error('[Error Handler]', {
9
+ requestId: req.requestId,
9
10
  error: err.message,
10
11
  stack: err.stack,
11
12
  path: req.path,
@@ -113,7 +113,8 @@ const resolveUserIdentity = async (req, res, next) => {
113
113
  // SECURITY: For private routes, require Firebase Auth to prevent IDOR attacks
114
114
  if (!isPublic && !authenticatedUserCid && !hasFirebaseAuth) {
115
115
  // Private route without authentication - reject immediately
116
- console.warn(`[Identity] Security violation: Private route ${req.path} accessed without Firebase Auth`);
116
+ // This is expected behavior - the security system is working correctly
117
+ console.log(`[Identity] Rejected unauthorized access to private route ${req.path} (no Firebase Auth token provided)`);
117
118
  return res.status(401).json({
118
119
  error: "Authentication required. Please provide a valid Firebase ID token in the Authorization header."
119
120
  });