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,27 +1,53 @@
1
1
  const express = require('express');
2
+ const { z } = require('zod');
2
3
  const { manageNotificationPreferences, isDeveloper, sendTestAlert } = require('../helpers/data-fetchers/firestore.js');
3
4
 
4
5
  const router = express.Router();
5
6
 
7
+ // Input validation schemas
8
+ const notificationPreferencesSchema = z.object({
9
+ email: z.boolean().optional(),
10
+ push: z.boolean().optional(),
11
+ sms: z.boolean().optional(),
12
+ alertTypes: z.record(z.boolean()).optional(),
13
+ thresholds: z.record(z.any()).optional()
14
+ });
15
+
16
+ const devOverrideSchema = z.object({
17
+ enabled: z.boolean(),
18
+ impersonateCid: z.union([z.string().regex(/^\d+$/), z.number().int().positive()]).optional(),
19
+ fakeCopiedPIs: z.array(z.union([z.string().regex(/^\d+$/), z.number().int().positive()])).optional()
20
+ });
21
+
6
22
  // GET /settings/notifications
7
- router.get('/notifications', async (req, res) => {
23
+ router.get('/notifications', async (req, res, next) => {
8
24
  try {
9
25
  const { db } = req.dependencies;
10
26
  const data = await manageNotificationPreferences(db, req.targetUserId, 'get');
11
27
  res.json({ success: true, data });
12
28
  } catch (error) {
13
- res.status(500).json({ error: error.message });
29
+ next(error);
14
30
  }
15
31
  });
16
32
 
17
33
  // PUT /settings/notifications
18
- router.put('/notifications', async (req, res) => {
34
+ router.put('/notifications', async (req, res, next) => {
19
35
  try {
36
+ // Validate input
37
+ const validated = notificationPreferencesSchema.parse(req.body);
38
+
20
39
  const { db } = req.dependencies;
21
- const result = await manageNotificationPreferences(db, req.targetUserId, 'update', req.body);
40
+ const result = await manageNotificationPreferences(db, req.targetUserId, 'update', validated);
22
41
  res.json(result);
23
42
  } catch (error) {
24
- res.status(500).json({ error: error.message });
43
+ if (error instanceof z.ZodError) {
44
+ return res.status(400).json({
45
+ success: false,
46
+ error: 'Invalid input',
47
+ details: error.errors
48
+ });
49
+ }
50
+ next(error);
25
51
  }
26
52
  });
27
53
 
@@ -33,27 +59,39 @@ router.get('/dev/override', async (req, res) => {
33
59
 
34
60
  const doc = await db.collection('dev_overrides').doc(req.targetUserId).get();
35
61
  res.json({ success: true, data: doc.exists ? doc.data() : { enabled: false } });
36
- } catch (e) { res.status(500).json({ error: e.message }); }
62
+ } catch (e) { next(e); }
37
63
  });
38
64
 
39
65
  // POST /settings/dev/override
40
- router.post('/dev/override', async (req, res) => {
66
+ router.post('/dev/override', async (req, res, next) => {
41
67
  try {
42
- const { db } = req.dependencies;
43
- const { enabled, impersonateCid, fakeCopiedPIs } = req.body;
68
+ // Validate input
69
+ const validated = devOverrideSchema.parse(req.body);
44
70
 
45
- if (!(await isDeveloper(db, req.targetUserId))) return res.status(403).json({ error: "Unauthorized" });
71
+ const { db } = req.dependencies;
72
+ if (!(await isDeveloper(db, req.targetUserId))) {
73
+ const error = new Error("Unauthorized");
74
+ error.status = 403;
75
+ return next(error);
76
+ }
46
77
 
47
78
  await db.collection('dev_overrides').doc(req.targetUserId).set({
48
- enabled,
49
- impersonateCid,
50
- fakeCopiedPIs: fakeCopiedPIs || [],
79
+ enabled: validated.enabled,
80
+ impersonateCid: validated.impersonateCid ? String(validated.impersonateCid) : undefined,
81
+ fakeCopiedPIs: validated.fakeCopiedPIs ? validated.fakeCopiedPIs.map(cid => String(cid)) : [],
51
82
  updatedAt: new Date()
52
83
  }, { merge: true });
53
84
 
54
85
  res.json({ success: true, message: "Dev override updated" });
55
86
  } catch (error) {
56
- res.status(500).json({ error: error.message });
87
+ if (error instanceof z.ZodError) {
88
+ return res.status(400).json({
89
+ success: false,
90
+ error: 'Invalid input',
91
+ details: error.errors
92
+ });
93
+ }
94
+ next(error);
57
95
  }
58
96
  });
59
97
 
@@ -1,4 +1,5 @@
1
1
  const express = require('express');
2
+ const { z } = require('zod');
2
3
  const { dispatchSyncRequest } = require('../helpers/task_engine_helper.js');
3
4
  const {
4
5
  checkSyncRateLimits,
@@ -10,22 +11,58 @@ const {
10
11
  checkDataStatus,
11
12
  checkPopularInvestorDataStatus
12
13
  } = require('../helpers/data-fetchers/firestore.js');
14
+ const { sanitizeCid } = require('../helpers/security_utils.js');
13
15
 
14
16
  const router = express.Router();
15
17
 
18
+ // Input validation schemas
19
+ const syncRequestSchema = z.object({
20
+ targetId: z.union([
21
+ z.string().regex(/^\d+$/, 'Invalid CID format'),
22
+ z.number().int().positive()
23
+ ]).optional()
24
+ }).transform(data => ({
25
+ ...data,
26
+ targetId: data.targetId ? String(data.targetId) : undefined
27
+ }));
28
+
16
29
  // Helper function for sync request logic (shared between /sync/request and /user/:userId/sync)
17
- const handleSyncRequest = async (req, res) => {
30
+ const handleSyncRequest = async (req, res, next) => {
18
31
  try {
19
32
  const { pubsub, db } = req.dependencies;
20
- // targetId can be passed (for PIs) or default to self
21
- // For /user/:userId/sync, targetId comes from URL param
22
- // For /sync/request, targetId comes from body or req.targetUserId
23
- const targetId = req.body.targetId || req.params.userId || req.targetUserId;
24
33
 
25
- // 1. Rate Limits
34
+ // Validate and sanitize input
35
+ let targetId;
36
+ if (req.params.userId) {
37
+ // From URL param - sanitize directly
38
+ targetId = sanitizeCid(req.params.userId);
39
+ } else if (req.body.targetId) {
40
+ // From body - validate with schema
41
+ const validated = syncRequestSchema.parse({ targetId: req.body.targetId });
42
+ targetId = validated.targetId || sanitizeCid(req.targetUserId);
43
+ } else {
44
+ // Default to authenticated user
45
+ targetId = sanitizeCid(req.targetUserId);
46
+ }
47
+
48
+ // 1. Rate Limits with developer audit logging
26
49
  const isDev = await isDeveloper(db, req.targetUserId);
27
50
  const limit = await checkSyncRateLimits(db, targetId, req.targetUserId, isDev);
28
- if (!limit.allowed) return res.status(429).json({ error: limit.message });
51
+
52
+ // Log developer activity for audit trail
53
+ if (isDev) {
54
+ console.log(`[Sync Rate Limit] Developer ${req.targetUserId} sync request for target ${targetId}`, {
55
+ timestamp: new Date().toISOString(),
56
+ developerCid: req.targetUserId,
57
+ targetCid: targetId,
58
+ ip: req.ip,
59
+ allowed: limit.allowed
60
+ });
61
+ }
62
+
63
+ if (!limit.allowed) {
64
+ return res.status(429).json({ error: limit.message });
65
+ }
29
66
 
30
67
  // 2. Detect User Types (needed for validation)
31
68
  const [isSignedIn, isPI] = await Promise.all([
@@ -156,7 +193,7 @@ const handleSyncRequest = async (req, res) => {
156
193
  message: `Sync dispatched for ${isSignedIn && isPI ? 'both user types' : isSignedIn ? 'signed-in user' : 'popular investor'}`
157
194
  });
158
195
  } catch (error) {
159
- res.status(500).json({ error: error.message });
196
+ next(error);
160
197
  }
161
198
  };
162
199
 
@@ -167,7 +204,9 @@ router.post('/request', handleSyncRequest);
167
204
  router.get('/status/:targetId', async (req, res, next) => {
168
205
  try {
169
206
  const { db } = req.dependencies;
170
- const status = await getSyncStatus(db, req.params.targetId);
207
+ // Sanitize targetId from URL param
208
+ const targetId = sanitizeCid(req.params.targetId);
209
+ const status = await getSyncStatus(db, targetId);
171
210
  res.json({ success: true, data: status });
172
211
  } catch (error) {
173
212
  next(error);
@@ -1,4 +1,5 @@
1
1
  const express = require('express');
2
+ const { z } = require('zod');
2
3
  const {
3
4
  manageUserWatchlist,
4
5
  latestUserCentricSnapshot,
@@ -11,11 +12,29 @@ const {
11
12
  getWatchlistTriggerCounts,
12
13
  subscribeToAllWatchlistPIs
13
14
  } = require('../helpers/data-fetchers/firestore.js');
15
+ const { sanitizeDocId } = require('../helpers/security_utils.js');
14
16
 
15
17
  const router = express.Router();
16
18
 
19
+ // Input validation schemas
20
+ const watchlistManageSchema = z.object({
21
+ instruction: z.enum(['create', 'update', 'delete', 'add_item', 'remove_item', 'update_item']),
22
+ payload: z.object({
23
+ watchlistId: z.string().optional(),
24
+ name: z.string().max(200).optional(),
25
+ description: z.string().max(1000).optional(),
26
+ items: z.array(z.any()).optional(),
27
+ item: z.any().optional()
28
+ })
29
+ });
30
+
31
+ const watchlistSubscribeSchema = z.object({
32
+ alertTypes: z.array(z.string()).optional(),
33
+ thresholds: z.record(z.any()).optional()
34
+ });
35
+
17
36
  // GET /watchlists/all
18
- router.get('/all', async (req, res) => {
37
+ router.get('/all', async (req, res, next) => {
19
38
  try {
20
39
  const { db } = req.dependencies;
21
40
  const watchlists = await latestUserCentricSnapshot(db, req.targetUserId, 'watchlists', 'watchlist', 'SignedInUsers', null);
@@ -36,9 +55,12 @@ router.post('/auto-generate', async (req, res, next) => {
36
55
  });
37
56
 
38
57
  // GET /watchlists/public - Must be before /:id route to avoid route conflict
39
- router.get('/public', async (req, res) => {
58
+ router.get('/public', async (req, res, next) => {
40
59
  try {
41
- const data = await fetchPublicWatchlists(req.dependencies.db, req.query.limit, req.query.offset);
60
+ // Validate and sanitize query parameters
61
+ const limit = req.query.limit ? Math.min(Math.max(parseInt(req.query.limit) || 10, 1), 100) : 10;
62
+ const offset = req.query.offset ? Math.max(parseInt(req.query.offset) || 0, 0) : 0;
63
+ const data = await fetchPublicWatchlists(req.dependencies.db, limit, offset);
42
64
  res.json({ success: true, data });
43
65
  } catch (e) { next(e); }
44
66
  });
@@ -47,9 +69,14 @@ router.get('/public', async (req, res) => {
47
69
  router.get('/:id', async (req, res, next) => {
48
70
  try {
49
71
  const { db } = req.dependencies;
50
- const { id } = req.params;
72
+ // Sanitize watchlist ID
73
+ const id = sanitizeDocId(req.params.id);
51
74
  const doc = await latestUserCentricSnapshot(db, req.targetUserId, 'watchlists', 'watchlist', 'SignedInUsers', id);
52
- if (!doc) return res.status(404).json({ error: "Watchlist not found" });
75
+ if (!doc) {
76
+ const error = new Error("Watchlist not found");
77
+ error.status = 404;
78
+ return next(error);
79
+ }
53
80
  res.json({ success: true, data: doc });
54
81
  } catch (error) {
55
82
  next(error);
@@ -57,24 +84,37 @@ router.get('/:id', async (req, res, next) => {
57
84
  });
58
85
 
59
86
  // GET /watchlists/:id/rankings-check (Rec 10)
60
- router.get('/:id/rankings-check', async (req, res) => {
87
+ router.get('/:id/rankings-check', async (req, res, next) => {
61
88
  try {
62
89
  const { db } = req.dependencies;
63
- const { id } = req.params;
90
+ // Sanitize watchlist ID
91
+ const id = sanitizeDocId(req.params.id);
64
92
 
65
93
  // Fetch watchlist
66
94
  const wl = await latestUserCentricSnapshot(db, req.targetUserId, 'watchlists', 'watchlist', 'SignedInUsers', id);
67
- if (!wl) return res.status(404).json({ error: "Watchlist not found" });
95
+ if (!wl) {
96
+ const error = new Error("Watchlist not found");
97
+ error.status = 404;
98
+ return next(error);
99
+ }
68
100
 
69
- const results = {};
70
- for (const item of (wl.items || [])) {
101
+ // Parallelize PI checks to avoid N+1 queries
102
+ const items = wl.items || [];
103
+ const checkPromises = items.map(async (item) => {
71
104
  try {
72
105
  await fetchPopularInvestorMasterList(db, String(item.cid));
73
- results[item.cid] = true;
106
+ return { cid: item.cid, exists: true };
74
107
  } catch {
75
- results[item.cid] = false;
108
+ return { cid: item.cid, exists: false };
76
109
  }
77
- }
110
+ });
111
+
112
+ const checkResults = await Promise.all(checkPromises);
113
+ const results = {};
114
+ checkResults.forEach(result => {
115
+ results[result.cid] = result.exists;
116
+ });
117
+
78
118
  res.json({ success: true, data: results });
79
119
  } catch (error) {
80
120
  next(error);
@@ -84,11 +124,20 @@ router.get('/:id/rankings-check', async (req, res) => {
84
124
  // POST /watchlists/manage (Unified)
85
125
  router.post('/manage', async (req, res, next) => {
86
126
  try {
127
+ // Validate input
128
+ const validatedData = watchlistManageSchema.parse(req.body);
129
+
87
130
  const { db } = req.dependencies;
88
- const { instruction, payload } = req.body;
89
- const result = await manageUserWatchlist(db, req.targetUserId, instruction, payload);
131
+ const result = await manageUserWatchlist(db, req.targetUserId, validatedData.instruction, validatedData.payload);
90
132
  res.json(result);
91
133
  } catch (error) {
134
+ if (error instanceof z.ZodError) {
135
+ return res.status(400).json({
136
+ success: false,
137
+ error: 'Invalid input',
138
+ details: error.errors
139
+ });
140
+ }
92
141
  next(error);
93
142
  }
94
143
  });
@@ -96,53 +145,71 @@ router.post('/manage', async (req, res, next) => {
96
145
  // Public & Copy Routes (Existing)
97
146
  router.post('/:id/publish', async (req, res, next) => {
98
147
  try {
99
- const result = await publishWatchlistVersion(req.dependencies.db, req.targetUserId, req.params.id);
148
+ // Sanitize watchlist ID
149
+ const id = sanitizeDocId(req.params.id);
150
+ const result = await publishWatchlistVersion(req.dependencies.db, req.targetUserId, id);
100
151
  res.json(result);
101
152
  } catch (e) { next(e); }
102
153
  });
103
154
 
104
- router.post('/:id/copy', async (req, res) => {
155
+ router.post('/:id/copy', async (req, res, next) => {
105
156
  try {
106
- const result = await copyWatchlist(req.dependencies.db, req.targetUserId, req.params.id, req.body.version);
157
+ // Sanitize watchlist ID
158
+ const id = sanitizeDocId(req.params.id);
159
+ const version = req.body.version ? sanitizeDocId(String(req.body.version)) : undefined;
160
+ const result = await copyWatchlist(req.dependencies.db, req.targetUserId, id, version);
107
161
  res.json(result);
108
162
  } catch (e) { next(e); }
109
163
  });
110
164
 
111
165
  router.get('/:id/versions', async (req, res, next) => {
112
166
  try {
113
- const data = await fetchWatchlistVersions(req.dependencies.db, req.params.id);
167
+ // Sanitize watchlist ID
168
+ const id = sanitizeDocId(req.params.id);
169
+ const data = await fetchWatchlistVersions(req.dependencies.db, id);
114
170
  res.json({ success: true, data });
115
171
  } catch (e) { next(e); }
116
172
  });
117
173
 
118
174
  // GET /watchlists/:id/trigger-counts - Get alert trigger counts for watchlist PIs
119
- router.get('/:id/trigger-counts', async (req, res) => {
175
+ router.get('/:id/trigger-counts', async (req, res, next) => {
120
176
  try {
121
177
  const { db } = req.dependencies;
122
- const { id } = req.params;
178
+ // Sanitize watchlist ID
179
+ const id = sanitizeDocId(req.params.id);
123
180
  const result = await getWatchlistTriggerCounts(db, req.targetUserId, id);
124
181
  res.json({ success: true, ...result });
125
182
  } catch (error) {
126
183
  if (error.message === "Watchlist not found") {
127
184
  error.status = 404;
128
185
  }
129
- res.status(500).json({ error: error.message });
186
+ next(error);
130
187
  }
131
188
  });
132
189
 
133
190
  // POST /watchlists/:id/subscribe-all - Subscribe to all PIs in a watchlist
134
191
  router.post('/:id/subscribe-all', async (req, res, next) => {
135
192
  try {
193
+ // Validate input
194
+ const validatedData = watchlistSubscribeSchema.parse(req.body);
195
+ // Sanitize watchlist ID
196
+ const id = sanitizeDocId(req.params.id);
197
+
136
198
  const { db } = req.dependencies;
137
- const { id } = req.params;
138
- const { alertTypes, thresholds } = req.body;
139
- const result = await subscribeToAllWatchlistPIs(db, req.targetUserId, id, alertTypes, thresholds);
199
+ const result = await subscribeToAllWatchlistPIs(db, req.targetUserId, id, validatedData.alertTypes, validatedData.thresholds);
140
200
  res.json(result);
141
201
  } catch (error) {
202
+ if (error instanceof z.ZodError) {
203
+ return res.status(400).json({
204
+ success: false,
205
+ error: 'Invalid input',
206
+ details: error.errors
207
+ });
208
+ }
142
209
  if (error.message === "Watchlist not found") {
143
210
  error.status = 404;
144
211
  }
145
- res.status(500).json({ error: error.message });
212
+ next(error);
146
213
  }
147
214
  });
148
215
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.621",
3
+ "version": "1.0.623",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [