@heavybit/pendoadmin-shared-lib 1.1.6 → 1.1.7

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,31 +1,143 @@
1
1
  import winston from 'winston';
2
+ import DailyRotateFile from 'winston-daily-rotate-file';
3
+ import { mkdirSync } from 'fs';
4
+ import { resolve } from 'path';
2
5
 
3
- const { combine, timestamp, json, errors, colorize, simple } = winston.format;
6
+ const { combine, timestamp, json, errors, colorize, printf, splat } = winston.format;
7
+
8
+ /**
9
+ * Custom dev console format that renders meta cleanly and warns when callers
10
+ * pass a Promise as the message (a common forgotten-await bug).
11
+ *
12
+ * info: SMS sent { batchId: 'xyz', recipients: 1 }
13
+ * error: DB failed Error: connection refused
14
+ * at ...stack...
15
+ */
16
+ const devConsoleFormat = printf((info) => {
17
+ const { level, message, timestamp: ts, service, stack, ...rest } = info as Record<string, unknown> & {
18
+ level: string;
19
+ message: unknown;
20
+ timestamp?: string;
21
+ service?: string;
22
+ stack?: string;
23
+ };
24
+
25
+ // Detect forgotten-await: someone passed a Promise as the message.
26
+ let renderedMessage: string;
27
+ if (message && typeof (message as { then?: unknown }).then === 'function') {
28
+ renderedMessage = '[Promise — forgot to await?]';
29
+ } else if (message instanceof Error) {
30
+ renderedMessage = message.message;
31
+ } else if (typeof message === 'object' && message !== null) {
32
+ renderedMessage = JSON.stringify(message);
33
+ } else {
34
+ renderedMessage = String(message ?? '');
35
+ }
36
+
37
+ // Strip winston-internal symbols/keys before serialising meta
38
+ const meta: Record<string, unknown> = {};
39
+ for (const [k, v] of Object.entries(rest)) {
40
+ if (k === 'splat' || k.startsWith('Symbol(')) continue;
41
+ meta[k] = v;
42
+ }
43
+
44
+ const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : '';
45
+ const tag = service ? `[${service}] ` : '';
46
+ const stackStr = stack ? `\n${stack}` : '';
47
+ return `${ts ?? ''} ${level}: ${tag}${renderedMessage}${metaStr}${stackStr}`;
48
+ });
49
+
50
+ /**
51
+ * LOG_TARGET controls where logs are shipped:
52
+ * console — stdout only (default for dev)
53
+ * file — rotating files only
54
+ * both — stdout + rotating files (default for staging/production)
55
+ *
56
+ * LOG_DIR — directory for log files (default: /app/logs)
57
+ * LOG_MAX_SIZE — max size per log file before rotation (default: 20m)
58
+ * LOG_MAX_FILES — retention period or count, e.g. 14d or 30 (default: 14d)
59
+ */
60
+ function resolveLogTarget(): 'console' | 'file' | 'both' {
61
+ const raw = (process.env.LOG_TARGET || 'console').toLowerCase().trim();
62
+ if (raw === 'file' || raw === 'both') return raw;
63
+ return 'console';
64
+ }
65
+
66
+ function buildFileTransports(service: string, level: string): DailyRotateFile[] {
67
+ const logDir = resolve(process.env.LOG_DIR || '/app/logs');
68
+ const maxSize = process.env.LOG_MAX_SIZE || '20m';
69
+ const maxFiles = process.env.LOG_MAX_FILES || '14d';
70
+
71
+ try {
72
+ mkdirSync(logDir, { recursive: true });
73
+ } catch {
74
+ // Ignore — directory may already exist or be read-only
75
+ }
76
+
77
+ const shared = {
78
+ dirname: logDir,
79
+ datePattern: 'YYYY-MM-DD',
80
+ zippedArchive: true,
81
+ maxSize,
82
+ maxFiles,
83
+ format: combine(errors({ stack: true }), splat(), timestamp(), json()),
84
+ };
85
+
86
+ return [
87
+ new DailyRotateFile({
88
+ ...shared,
89
+ filename: `${service}-%DATE%-combined.log`,
90
+ level,
91
+ }),
92
+ new DailyRotateFile({
93
+ ...shared,
94
+ filename: `${service}-%DATE%-error.log`,
95
+ level: 'error',
96
+ }),
97
+ ];
98
+ }
4
99
 
5
100
  export function createLogger(service: string, level?: string): winston.Logger {
6
101
  const isProduction = process.env.NODE_ENV === 'production';
102
+ const resolvedLevel = level || process.env.LOG_LEVEL || (isProduction ? 'info' : 'debug');
103
+ const target = resolveLogTarget();
7
104
 
8
- return winston.createLogger({
9
- level: level || process.env.LOG_LEVEL || (isProduction ? 'info' : 'debug'),
10
- defaultMeta: { service },
105
+ const consoleTransport = new winston.transports.Console({
11
106
  format: combine(
12
107
  errors({ stack: true }),
108
+ splat(),
13
109
  timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
14
- isProduction ? json() : combine(colorize(), simple())
110
+ isProduction ? json() : combine(colorize({ level: true }), devConsoleFormat),
15
111
  ),
16
- transports: [
17
- new winston.transports.Console(),
18
- ],
112
+ });
113
+
114
+ const transports: winston.transport[] = [];
115
+ if (target === 'console' || target === 'both') transports.push(consoleTransport);
116
+ if (target === 'file' || target === 'both') transports.push(...buildFileTransports(service, resolvedLevel));
117
+
118
+ return winston.createLogger({
119
+ level: resolvedLevel,
120
+ defaultMeta: { service },
121
+ format: combine(errors({ stack: true }), splat(), timestamp()),
122
+ transports,
19
123
  });
20
124
  }
21
125
 
22
126
  export function createAuditLogger(service: string): winston.Logger {
127
+ const target = resolveLogTarget();
128
+
129
+ const consoleTransport = new winston.transports.Console({
130
+ format: combine(splat(), timestamp(), json()),
131
+ });
132
+
133
+ const transports: winston.transport[] = [];
134
+ if (target === 'console' || target === 'both') transports.push(consoleTransport);
135
+ if (target === 'file' || target === 'both') transports.push(...buildFileTransports(`${service}-audit`, 'info'));
136
+
23
137
  return winston.createLogger({
24
138
  level: 'info',
25
139
  defaultMeta: { service, type: 'audit' },
26
- format: combine(timestamp(), json()),
27
- transports: [
28
- new winston.transports.Console(),
29
- ],
140
+ format: combine(splat(), timestamp(), json()),
141
+ transports,
30
142
  });
31
143
  }
@@ -53,6 +53,8 @@ export interface IJwtPayload {
53
53
  roles: string[];
54
54
  permissions: string[];
55
55
  isSuperAdmin: boolean;
56
+ /** Whether the user has completed post-login OTP verification for this session. */
57
+ otpVerified?: boolean;
56
58
  iat?: number;
57
59
  exp?: number;
58
60
  iss?: string;