@peopl-health/nexus 2.2.9 → 2.3.0

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.
@@ -0,0 +1,118 @@
1
+ const { NodeSDK } = require('@opentelemetry/sdk-node');
2
+ const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
3
+ const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus');
4
+ const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
5
+ const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-node');
6
+
7
+ class TelemetryManager {
8
+ constructor() {
9
+ this.sdk = null;
10
+ this.isInitialized = false;
11
+ }
12
+
13
+ async init(config = {}) {
14
+ if (this.isInitialized) {
15
+ console.log('✅ OpenTelemetry already initialized');
16
+ return;
17
+ }
18
+
19
+ const {
20
+ serviceName = 'nexus-assistant',
21
+ serviceVersion = '1.0.0',
22
+ jaegerEndpoint = 'http://localhost:14268/api/traces',
23
+ prometheusEndpoint = '/metrics',
24
+ prometheusPort = 9090
25
+ } = config;
26
+
27
+ try {
28
+ process.env.OTEL_SERVICE_NAME = serviceName;
29
+ process.env.OTEL_SERVICE_VERSION = serviceVersion;
30
+
31
+ const prometheusExporter = new PrometheusExporter({
32
+ endpoint: prometheusEndpoint,
33
+ port: prometheusPort,
34
+ });
35
+
36
+ const jaegerExporter = new JaegerExporter({
37
+ endpoint: jaegerEndpoint,
38
+ tags: [],
39
+ timeout: 10000,
40
+ });
41
+
42
+ const batchSpanProcessor = new BatchSpanProcessor(jaegerExporter, {
43
+ maxExportBatchSize: process.env.NODE_ENV === 'production' ? 50 : 10,
44
+ maxQueueSize: process.env.NODE_ENV === 'production' ? 2048 : 100,
45
+ exportTimeoutMillis: process.env.NODE_ENV === 'production' ? 5000 : 2000,
46
+ scheduledDelayMillis: process.env.NODE_ENV === 'production' ? 2000 : 500,
47
+ });
48
+
49
+ this.sdk = new NodeSDK({
50
+ instrumentations: [getNodeAutoInstrumentations({
51
+ '@opentelemetry/instrumentation-fs': {
52
+ enabled: false,
53
+ },
54
+ '@opentelemetry/instrumentation-dns': {
55
+ enabled: process.env.NODE_ENV !== 'production',
56
+ },
57
+ '@opentelemetry/instrumentation-net': {
58
+ enabled: process.env.NODE_ENV !== 'production',
59
+ },
60
+ '@opentelemetry/instrumentation-http': {
61
+ enabled: true,
62
+ requestHook: (span, request) => {
63
+ span.setAttributes({
64
+ 'http.request.content_length': request.headers['content-length'] || 0,
65
+ 'http.route': request.url,
66
+ });
67
+ }
68
+ },
69
+ '@opentelemetry/instrumentation-express': {
70
+ enabled: true,
71
+ },
72
+ '@opentelemetry/instrumentation-mongodb': {
73
+ enabled: true,
74
+ },
75
+ '@opentelemetry/instrumentation-mongoose': {
76
+ enabled: true,
77
+ }
78
+ })],
79
+ spanProcessor: batchSpanProcessor,
80
+ metricReader: prometheusExporter,
81
+ });
82
+
83
+ await this.sdk.start();
84
+ this.isInitialized = true;
85
+
86
+ console.log(`🚀 OpenTelemetry initialized for "${serviceName}"`);
87
+ console.log(`📊 Metrics available at: http://localhost:${prometheusPort}${prometheusEndpoint}`);
88
+ console.log(`🔍 Traces sent to: ${jaegerEndpoint}`);
89
+
90
+ } catch (error) {
91
+ console.error('❌ Failed to initialize OpenTelemetry:', error.message);
92
+ this.isInitialized = false; // Ensure state is consistent on failure
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ async shutdown() {
98
+ if (this.sdk && this.isInitialized) {
99
+ try {
100
+ await this.sdk.shutdown();
101
+ this.isInitialized = false;
102
+ console.log('🛑 OpenTelemetry shutdown completed');
103
+ } catch (error) {
104
+ console.error('❌ Error during OpenTelemetry shutdown:', error.message);
105
+ this.isInitialized = false; // Reset state even on error
106
+ throw error;
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ const telemetryManager = new TelemetryManager();
113
+
114
+ module.exports = {
115
+ telemetryManager,
116
+ initTelemetry: async (config) => await telemetryManager.init(config),
117
+ shutdownTelemetry: async () => await telemetryManager.shutdown()
118
+ };
@@ -205,7 +205,6 @@ class OpenAIResponsesProvider {
205
205
  content: this._normalizeContent(role, content),
206
206
  type: 'message',
207
207
  });
208
- console.log('payload', payload);
209
208
 
210
209
  if (payload.content) {
211
210
  return this._retryWithRateLimit(async () => {
@@ -307,11 +306,9 @@ class OpenAIResponsesProvider {
307
306
  tools: transformedTools,
308
307
  });
309
308
 
310
- console.log('payload', payload);
311
309
  const response = await this._retryWithRateLimit(() =>
312
310
  this.client.responses.create(payload)
313
311
  );
314
- console.log('response', response);
315
312
 
316
313
  if (response?.status !== 'completed') {
317
314
  await this.cleanupOrphanedFunctionCalls(id, true);
@@ -5,11 +5,13 @@ const runtimeConfig = require('../config/runtimeConfig');
5
5
  const { BaseAssistant } = require('../assistants/BaseAssistant');
6
6
  const { createProvider } = require('../providers/createProvider');
7
7
 
8
- const { Message, formatTimestamp } = require('../models/messageModel.js');
9
8
  const { Thread } = require('../models/threadModel.js');
10
9
 
11
10
  const { getCurRow } = require('../helpers/assistantHelper.js');
12
- const { processIndividualMessage, getLastMessages } = require('../helpers/assistantHelper.js');
11
+ const { runAssistantAndWait, runAssistantWithRetries } = require('../helpers/assistantHelper.js');
12
+ const { getThread, getThreadInfo } = require('../helpers/threadHelper.js');
13
+ const { withTracing } = require('../utils/tracingDecorator.js');
14
+ const { processIndividualMessage, getLastMessages } = require('../helpers/messageHelper.js');
13
15
  const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
14
16
  const { delay } = require('../helpers/whatsappHelper.js');
15
17
 
@@ -27,64 +29,6 @@ const configureAssistants = (config) => {
27
29
  assistantConfig = config;
28
30
  };
29
31
 
30
- const runAssistantAndWait = async ({
31
- thread,
32
- assistant,
33
- runConfig = {}
34
- }) => {
35
- if (!thread || !thread.getConversationId()) {
36
- throw new Error('runAssistantAndWait requires a thread with a valid thread_id or conversation_id');
37
- }
38
-
39
- if (!assistant) {
40
- throw new Error('runAssistantAndWait requires an assistant instance');
41
- }
42
-
43
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
44
- const { polling, tools: configTools, ...conversationConfig } = runConfig || {};
45
- const variant = provider.getVariant ? provider.getVariant() : (process.env.VARIANT || 'assistants');
46
-
47
- // Get tool schemas from assistant if available, fallback to config tools
48
- const tools = assistant.getToolSchemas ? assistant.getToolSchemas() : (configTools || []);
49
-
50
- // Only pass assistant for Responses API (needed to handle pending function calls)
51
- const runConfigWithAssistant = variant === 'responses'
52
- ? { ...conversationConfig, assistant }
53
- : conversationConfig;
54
-
55
- let run = await provider.runConversation({
56
- threadId: thread.getConversationId(),
57
- assistantId: thread.getAssistantId(),
58
- tools: tools.length > 0 ? tools : undefined,
59
- ...runConfigWithAssistant,
60
- });
61
-
62
- const filter = thread.code ? { code: thread.code, active: true } : null;
63
- if (filter) {
64
- await Thread.updateOne(filter, { $set: { run_id: run.id } });
65
- }
66
-
67
- const maxRetries = polling?.maxRetries ?? DEFAULT_MAX_RETRIES;
68
- let completed = false;
69
-
70
- try {
71
- console.log('RUN ID', run.id, 'THREAD ID', thread.getConversationId(), 'ASSISTANT ID', thread.getAssistantId());
72
- ({run, completed} = await provider.checkRunStatus(assistant, thread.getConversationId(), run.id, 0, maxRetries));
73
- } finally {
74
- if (filter) {
75
- await Thread.updateOne(filter, { $set: { run_id: null } });
76
- }
77
- }
78
-
79
- if (!completed) {
80
- return { run: run, completed: false, output: '' };
81
- }
82
-
83
- const output = await provider.getRunText({ threadId: thread.getConversationId(), runId: run.id, fallback: '' });
84
-
85
- return { completed: true, output };
86
- };
87
-
88
32
  const registerAssistant = (assistantId, definition) => {
89
33
  if (!assistantId || typeof assistantId !== 'string') {
90
34
  throw new Error('registerAssistant requires a string assistantId');
@@ -189,7 +133,6 @@ const getAssistantById = (assistant_id, thread) => {
189
133
 
190
134
 
191
135
  const createAssistant = async (code, assistant_id, messages=[], force=false) => {
192
- // If thread already exists, update it
193
136
  const findThread = await Thread.findOne({ code: code });
194
137
  console.log('[createAssistant] findThread', findThread);
195
138
  if (findThread && findThread.getConversationId()) {
@@ -279,162 +222,123 @@ const addMsgAssistant = async (code, inMessages, role = 'user', reply = false) =
279
222
  }
280
223
  };
281
224
 
282
- const addInsAssistant = async (code, instruction, role = 'user') => {
283
- try {
284
- const thread = await Thread.findOne({ code: code });
285
- console.log(thread);
286
- if (thread === null) return null;
287
-
288
- let output, completed;
289
- let retries = 0;
290
- const maxRetries = DEFAULT_MAX_RETRIES;
291
- const assistant = getAssistantById(thread.getAssistantId(), thread);
292
- do {
293
- ({ output, completed } = await runAssistantAndWait({
294
- thread,
295
- assistant,
296
- runConfig: {
297
- additionalInstructions: instruction,
298
- additionalMessages: [
299
- { role: role, content: instruction }
300
- ]
301
- }
302
- }));
303
- console.log(`Attempt ${retries + 1}: completed=${completed}, output=${output || '(empty)'}`);
304
-
305
- if (completed && output) break;
306
- if (retries < maxRetries) await new Promise(resolve => setTimeout(resolve, 2000));
307
- retries++;
308
- } while (retries <= maxRetries && (!completed || !output));
309
- console.log('RUN RESPONSE', output);
310
-
311
- return output;
312
- } catch (error) {
313
- console.log(error);
314
- return null;
315
- }
225
+ const addInstructionCore = async (code, instruction, role = 'user') => {
226
+ const thread = await withTracing(getThread, 'get_thread_operation',
227
+ (threadCode) => ({
228
+ 'thread.code': threadCode,
229
+ 'operation.type': 'thread_retrieval'
230
+ })
231
+ )(code);
232
+ if (thread === null) return null;
233
+
234
+ const assistant = getAssistantById(thread.getAssistantId(), thread);
235
+ const { output, completed, retries } = await withTracing(
236
+ runAssistantWithRetries,
237
+ 'run_assistant_with_retries',
238
+ (thread, assistant, runConfig, patientReply) => ({
239
+ 'assistant.id': thread.getAssistantId(),
240
+ 'assistant.max_retries': DEFAULT_MAX_RETRIES,
241
+ 'assistant.has_patient_reply': !!patientReply
242
+ })
243
+ )(
244
+ thread,
245
+ assistant,
246
+ {
247
+ additionalInstructions: instruction,
248
+ additionalMessages: [
249
+ { role: role, content: instruction }
250
+ ]
251
+ },
252
+ null // no patientReply for instructions
253
+ );
254
+
255
+ console.log('RUN RESPONSE', output);
256
+ return output;
316
257
  };
317
258
 
318
- const getThread = async (code, message = null) => {
319
- try {
320
- let thread = await Thread.findOne({ code: code });
321
- console.log('GET THREAD');
322
- console.log(thread);
323
-
324
- if (thread === null) {
325
- if (message != null) {
326
- const timestamp = formatTimestamp(message.messageTimestamp);
327
- await Message.updateOne({ message_id: message.key.id, timestamp: timestamp }, { $set: { processed: true } });
328
-
329
- }
330
- return null;
331
- }
332
-
333
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
334
- while (thread && thread.run_id) {
335
- console.log(`Wait for ${thread.run_id} to be executed`);
336
- const activeProvider = provider || llmConfig.requireOpenAIProvider();
337
- const run = await activeProvider.getRun({ threadId: thread.getConversationId(), runId: thread.run_id });
338
- if (run.status === 'cancelled' || run.status === 'expired' || run.status === 'completed') {
339
- await Thread.updateOne({ code: code }, { $set: { run_id: null } });
340
- }
341
- thread = await Thread.findOne({ code: code });
342
- await delay(5000);
343
- }
344
-
345
- return thread;
346
- } catch (error) {
347
- console.error('Error in getThread:', error.message || error);
259
+ const addInsAssistant = withTracing(
260
+ addInstructionCore,
261
+ 'add_instruction_assistant',
262
+ (code, instruction, role) => ({
263
+ 'instruction.thread_code': code,
264
+ 'instruction.content_length': instruction?.length || 0,
265
+ 'instruction.role': role,
266
+ 'operation.type': 'add_instruction'
267
+ })
268
+ );
269
+
270
+ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOptions = {}) => {
271
+ const thread = thread_ || await withTracing(getThread, 'get_thread_operation',
272
+ (threadCode) => ({
273
+ 'thread.code': threadCode,
274
+ 'operation.type': 'thread_retrieval',
275
+ 'thread.provided': !!thread_
276
+ })
277
+ )(code);
278
+ if (!thread) return null;
279
+
280
+ const patientReply = await getLastMessages(code);
281
+ if (!patientReply) {
282
+ console.log('[replyAssistantCore] No relevant data found for this assistant.');
348
283
  return null;
349
284
  }
350
- };
351
285
 
352
- const getThreadInfo = async (code) => {
353
- try {
354
- let thread = await Thread.findOne({ code: code });
355
- return thread;
356
- } catch (error) {
357
- console.log(error);
358
- return null;
286
+ const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
287
+
288
+ let patientMsg = false;
289
+ let urls = [];
290
+ for (let i = 0; i < patientReply.length; i++) {
291
+ const reply = patientReply[i];
292
+ const { isPatient, url } = await processIndividualMessage(code, reply, provider, thread);
293
+ console.log(`[replyAssistantCore] Processing message ${i + 1}/${patientReply.length}: isPatient=${isPatient}, hasUrl=${!!url}`);
294
+ patientMsg = patientMsg || isPatient;
295
+ if (url) urls.push({ 'url': url });
359
296
  }
360
- };
361
-
362
- const replyAssistant = async function (code, message_ = null, thread_ = null, runOptions = {}) {
363
- try {
364
- let thread = thread_ || await getThread(code);
365
- console.log('THREAD STOPPED', code, thread?.active);
366
- if (!thread) return null;
367
-
368
- const patientReply = await getLastMessages(code);
369
- if (!patientReply) {
370
- console.log('No relevant data found for this assistant.');
371
- return null;
372
- }
373
-
374
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
375
- let activeRuns = await provider.listRuns({ threadId: thread.getConversationId(), activeOnly: true });
376
- let activeRunsCount = activeRuns?.data?.length || 0;
377
- console.log('ACTIVE RUNS:', activeRunsCount, activeRuns?.data?.map(run => ({ id: run.id, status: run.status })));
378
- while (activeRunsCount > 0) {
379
- console.log(`WAITING FOR ${activeRunsCount} ACTIVE RUNS TO COMPLETE - ${thread.getConversationId()}`);
380
- activeRuns = await provider.listRuns({ threadId: thread.getConversationId(), activeOnly: true });
381
- activeRunsCount = activeRuns?.data?.length || 0;
382
- await delay(5000);
383
- }
384
-
385
- let patientMsg = false;
386
- let urls = [];
387
- for (const reply of patientReply) {
388
- const { isPatient, url } = await processIndividualMessage(code, reply, provider, thread);
389
- console.log(`isPatient ${isPatient} ${url}`);
390
- patientMsg = patientMsg || isPatient;
391
- if (url) urls.push({ 'url': url });
392
- }
393
-
394
- if (urls.length > 0) {
395
- console.log('urls', urls);
396
- const { pdfBuffer, processedFiles } = await combineImagesToPDF({ code });
397
- console.log('AFTER COMBINED IN BUFFER', processedFiles);
297
+
298
+ if (urls.length > 0) {
299
+ console.log(`[replyAssistantCore] Processing ${urls.length} URLs for PDF combination`);
300
+ const { pdfBuffer, processedFiles } = await combineImagesToPDF({ code });
301
+ console.log(`[replyAssistantCore] PDF combination complete: ${processedFiles?.length || 0} files processed`);
302
+
303
+ if (pdfBuffer) {
398
304
  const key = `${code}-${Date.now()}-combined.pdf`;
399
305
  const bucket = runtimeConfig.get('AWS_S3_BUCKET_NAME');
400
- if (bucket && pdfBuffer) {
306
+ if (bucket) {
401
307
  await AWS.uploadBufferToS3(pdfBuffer, bucket, key, 'application/pdf');
402
308
  }
403
- if (processedFiles && processedFiles.length) {
404
- cleanupFiles(processedFiles);
405
- }
406
309
  }
310
+
311
+ if (processedFiles && processedFiles.length) {
312
+ cleanupFiles(processedFiles);
313
+ }
314
+ }
407
315
 
408
- if (!patientMsg || thread.stopped) return null;
316
+ if (!patientMsg || thread.stopped) return null;
409
317
 
410
- const assistant = getAssistantById(thread.getAssistantId(), thread);
411
- assistant.setReplies(patientReply);
318
+ const assistant = getAssistantById(thread.getAssistantId(), thread);
319
+ const { run, output, completed, retries } = await withTracing(
320
+ runAssistantWithRetries,
321
+ 'run_assistant_with_retries',
322
+ (thread, assistant, runConfig, patientReply) => ({
323
+ 'assistant.id': thread.getAssistantId(),
324
+ 'assistant.max_retries': DEFAULT_MAX_RETRIES,
325
+ 'assistant.has_patient_reply': !!patientReply
326
+ })
327
+ )(thread, assistant, runOptions, patientReply);
412
328
 
413
- let run, output, completed;
414
- let retries = 0;
415
- const maxRetries = DEFAULT_MAX_RETRIES;
416
- do {
417
- ({ run, output, completed } = await runAssistantAndWait({
418
- thread,
419
- assistant,
420
- runConfig: runOptions
421
- }));
422
- console.log(`Attempt ${retries + 1}: completed=${completed}, output=${output || '(empty)'}`);
423
-
424
- if (completed && output) break;
425
- if (retries < maxRetries) await new Promise(resolve => setTimeout(resolve, 2000));
426
- retries++;
427
- } while (retries <= maxRetries && (!completed || !output));
428
-
429
- console.log('RUN LAST ERROR:', run?.last_error);
430
- console.log('RUN STATUS', completed);
431
- console.log(output);
329
+ return output;
330
+ };
432
331
 
433
- return output;
434
- } catch (err) {
435
- console.log(`Error inside reply assistant ${err} ${code}`);
436
- }
437
- };
332
+ const replyAssistant = withTracing(
333
+ replyAssistantCore,
334
+ 'assistant_reply',
335
+ (code, message_, thread_, runOptions) => ({
336
+ 'assistant.thread_code': code,
337
+ 'assistant.has_message': !!message_,
338
+ 'assistant.has_custom_thread': !!thread_,
339
+ 'assistant.run_options': JSON.stringify(runOptions)
340
+ })
341
+ );
438
342
 
439
343
  const switchAssistant = async (code, assistant_id) => {
440
344
  try {
@@ -43,9 +43,7 @@ class MongoStorage {
43
43
  async saveMessage(messageData) {
44
44
  try {
45
45
  const enrichedMessage = await this._enrichTwilioMedia(messageData);
46
- console.log('[MongoStorage] Enriched message', enrichedMessage);
47
46
  const values = this.buildMessageValues(enrichedMessage);
48
- console.log('[MongoStorage] Message values', values);
49
47
  const { insertMessage } = require('../models/messageModel');
50
48
  await insertMessage(values);
51
49
  console.log('[MongoStorage] Message stored');
@@ -1,4 +1,5 @@
1
1
  const pino = require('pino');
2
+ const { trace, context } = require('@opentelemetry/api');
2
3
 
3
4
  const createLogger = (config = {}) => {
4
5
  const {
@@ -10,13 +11,99 @@ const createLogger = (config = {}) => {
10
11
  transport: process.env.NODE_ENV !== 'production' ? {
11
12
  target: 'pino-pretty',
12
13
  options: {
13
- colorize: true
14
+ colorize: true,
15
+ ignore: 'trace_id,span_id,trace_flags'
14
16
  }
15
- } : undefined
17
+ } : undefined,
18
+ // Add OpenTelemetry trace context automatically
19
+ formatters: {
20
+ log: (object) => {
21
+ const span = trace.getActiveSpan();
22
+ if (span) {
23
+ const spanContext = span.spanContext();
24
+ return {
25
+ ...object,
26
+ trace_id: spanContext.traceId,
27
+ span_id: spanContext.spanId,
28
+ trace_flags: spanContext.traceFlags.toString(16).padStart(2, '0')
29
+ };
30
+ }
31
+ return object;
32
+ }
33
+ }
16
34
  });
17
35
  };
18
36
 
19
- // Default logger instance
37
+ // Enhanced logger with observability methods
38
+ const createObservabilityLogger = (serviceName = 'nexus-assistant', config = {}) => {
39
+ const baseLogger = createLogger(config);
40
+
41
+ return {
42
+ ...baseLogger,
43
+
44
+ // Trace-aware logging methods
45
+ traceInfo: (message, extra = {}) => {
46
+ const span = trace.getActiveSpan();
47
+ baseLogger.info({
48
+ ...extra,
49
+ operation: span?.name || 'unknown',
50
+ service: serviceName
51
+ }, message);
52
+ },
53
+
54
+ traceError: (error, message, extra = {}) => {
55
+ const span = trace.getActiveSpan();
56
+ if (span) {
57
+ span.recordException(error);
58
+ span.setStatus({ code: 2, message: error.message });
59
+ }
60
+ baseLogger.error({
61
+ ...extra,
62
+ err: error,
63
+ operation: span?.name || 'unknown',
64
+ service: serviceName
65
+ }, message);
66
+ },
67
+
68
+ traceWarn: (message, extra = {}) => {
69
+ const span = trace.getActiveSpan();
70
+ baseLogger.warn({
71
+ ...extra,
72
+ operation: span?.name || 'unknown',
73
+ service: serviceName
74
+ }, message);
75
+ },
76
+
77
+ // Performance logging
78
+ performance: (operation, duration, extra = {}) => {
79
+ baseLogger.info({
80
+ ...extra,
81
+ operation,
82
+ duration_ms: duration,
83
+ service: serviceName,
84
+ performance: true
85
+ }, `Operation ${operation} completed in ${duration}ms`);
86
+ },
87
+
88
+ // Assistant-specific logging
89
+ assistantOperation: (operation, threadCode, extra = {}) => {
90
+ baseLogger.info({
91
+ ...extra,
92
+ assistant_operation: operation,
93
+ thread_code: threadCode,
94
+ service: serviceName
95
+ }, `Assistant operation: ${operation} for thread ${threadCode}`);
96
+ }
97
+ };
98
+ };
99
+
100
+ // Default logger instances
20
101
  const logger = createLogger();
102
+ const observabilityLogger = createObservabilityLogger();
21
103
 
22
- module.exports = { logger, createLogger };
104
+ module.exports = {
105
+ logger,
106
+ createLogger,
107
+ observabilityLogger,
108
+ createObservabilityLogger
109
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Sanitize filename to prevent path traversal and command injection
3
+ * @param {string} filename - The filename to sanitize
4
+ * @param {number} maxLength - Maximum length (default: 50)
5
+ * @returns {string} Sanitized filename
6
+ */
7
+ function sanitizeFilename(filename, maxLength = 50) {
8
+ if (typeof filename !== 'string') return 'unknown';
9
+
10
+ return filename
11
+ .replace(/[^\w.-]/g, '_')
12
+ .replace(/^\.+/, '')
13
+ .replace(/\.+$/, '')
14
+ .slice(0, maxLength);
15
+ }
16
+
17
+ /**
18
+ * Sanitize filename for media uploads (more restrictive)
19
+ * @param {string} filename - The filename to sanitize
20
+ * @param {number} maxLength - Maximum length (default: 50)
21
+ * @returns {string} Sanitized filename for media
22
+ */
23
+ function sanitizeMediaFilename(filename, maxLength = 50) {
24
+ if (typeof filename !== 'string') return '';
25
+
26
+ return filename
27
+ .replace(/[^a-zA-Z0-9_-]/g, '')
28
+ .slice(0, maxLength);
29
+ }
30
+
31
+ /**
32
+ * Sanitize log metadata to remove sensitive information
33
+ * @param {Object} metadata - Metadata object to sanitize
34
+ * @returns {Object} Sanitized metadata
35
+ */
36
+ function sanitizeLogMetadata(metadata) {
37
+ const safe = { ...metadata };
38
+
39
+ delete safe.text;
40
+ delete safe.content;
41
+ delete safe.body;
42
+ delete safe.phone;
43
+ delete safe.numero;
44
+
45
+ if (safe.message_id) {
46
+ safe.message_id = safe.message_id.length > 8
47
+ ? `${safe.message_id.slice(0, 4)}***${safe.message_id.slice(-4)}`
48
+ : '***';
49
+ }
50
+
51
+ if (safe.code && safe.code.length > 8) {
52
+ safe.code = `${safe.code.substring(0, 3)}***${safe.code.slice(-4)}`;
53
+ }
54
+
55
+ return safe;
56
+ }
57
+
58
+ module.exports = {
59
+ sanitizeFilename,
60
+ sanitizeMediaFilename,
61
+ sanitizeLogMetadata
62
+ };