@ceon-oy/monitor-sdk 1.0.1

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/dist/index.js ADDED
@@ -0,0 +1,605 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ MonitorClient: () => MonitorClient
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/MonitorClient.ts
38
+ var MonitorClient = class {
39
+ constructor(config) {
40
+ this.queue = [];
41
+ this.flushTimer = null;
42
+ this.isClosed = false;
43
+ this.retryCount = /* @__PURE__ */ new Map();
44
+ this.isFlushInProgress = false;
45
+ if (!config.apiKey || config.apiKey.trim().length === 0) {
46
+ throw new Error("[MonitorClient] API key is required");
47
+ }
48
+ if (!/^cm_[a-zA-Z0-9_-]+$/.test(config.apiKey)) {
49
+ throw new Error("[MonitorClient] Invalid API key format. Expected format: cm_xxx");
50
+ }
51
+ if (!config.endpoint || config.endpoint.trim().length === 0) {
52
+ throw new Error("[MonitorClient] Endpoint URL is required");
53
+ }
54
+ try {
55
+ const url = new URL(config.endpoint);
56
+ if (!["https:", "http:"].includes(url.protocol)) {
57
+ throw new Error("[MonitorClient] Endpoint must use HTTP or HTTPS protocol");
58
+ }
59
+ if (url.protocol === "http:" && config.environment !== "development" && config.environment !== "test") {
60
+ console.warn("[MonitorClient] Warning: Using HTTP in non-development environment is not recommended");
61
+ }
62
+ } catch (err) {
63
+ if (err instanceof Error && err.message.includes("[MonitorClient]")) {
64
+ throw err;
65
+ }
66
+ throw new Error("[MonitorClient] Invalid endpoint URL");
67
+ }
68
+ this.apiKey = config.apiKey;
69
+ this.endpoint = config.endpoint.replace(/\/$/, "");
70
+ this.environment = config.environment || "production";
71
+ this.batchSize = Math.max(1, config.batchSize || 10);
72
+ this.flushIntervalMs = Math.max(1e3, config.flushIntervalMs || 5e3);
73
+ this.trackDependencies = config.trackDependencies || false;
74
+ this.packageJsonPath = config.packageJsonPath;
75
+ this.dependencySources = config.dependencySources;
76
+ this.maxQueueSize = config.maxQueueSize || 1e3;
77
+ this.maxRetries = config.maxRetries || 3;
78
+ this.requestTimeoutMs = config.requestTimeoutMs || 1e4;
79
+ const defaultExcludePatterns = [
80
+ "@types/*",
81
+ "eslint*",
82
+ "prettier*",
83
+ "*-loader",
84
+ "*-plugin",
85
+ "@eslint/*",
86
+ "@typescript-eslint/*"
87
+ ];
88
+ this.excludePatterns = config.excludePatterns || defaultExcludePatterns;
89
+ this.startFlushTimer();
90
+ if (this.trackDependencies) {
91
+ this.syncDependencies().catch((err) => {
92
+ console.error("[MonitorClient] Failed to sync dependencies:", err instanceof Error ? err.message : String(err));
93
+ });
94
+ }
95
+ }
96
+ async captureError(error, context) {
97
+ if (this.isClosed) return;
98
+ const payload = {
99
+ severity: context?.severity || "ERROR",
100
+ message: error.message,
101
+ stack: error.stack,
102
+ environment: this.environment,
103
+ route: context?.route,
104
+ method: context?.method,
105
+ statusCode: context?.statusCode,
106
+ userAgent: context?.userAgent,
107
+ ip: context?.ip,
108
+ requestId: context?.requestId,
109
+ metadata: context?.metadata
110
+ };
111
+ this.enqueue(payload);
112
+ }
113
+ async captureMessage(message, severity = "INFO", context) {
114
+ if (this.isClosed) return;
115
+ const payload = {
116
+ severity,
117
+ message,
118
+ environment: this.environment,
119
+ route: context?.route,
120
+ method: context?.method,
121
+ statusCode: context?.statusCode,
122
+ userAgent: context?.userAgent,
123
+ ip: context?.ip,
124
+ requestId: context?.requestId,
125
+ metadata: context?.metadata
126
+ };
127
+ this.enqueue(payload);
128
+ }
129
+ async flush() {
130
+ if (this.isFlushInProgress || this.queue.length === 0) return;
131
+ this.isFlushInProgress = true;
132
+ const errors = [...this.queue];
133
+ this.queue = [];
134
+ try {
135
+ if (errors.length === 1) {
136
+ await this.sendSingle(errors[0]);
137
+ } else {
138
+ await this.sendBatch(errors);
139
+ }
140
+ for (const error of errors) {
141
+ this.retryCount.delete(this.getErrorKey(error));
142
+ }
143
+ } catch (err) {
144
+ console.error("[MonitorClient] Failed to send errors:", err instanceof Error ? err.message : String(err));
145
+ for (const error of errors) {
146
+ const key = this.getErrorKey(error);
147
+ const retries = this.retryCount.get(key) || 0;
148
+ if (retries < this.maxRetries) {
149
+ this.retryCount.set(key, retries + 1);
150
+ if (this.queue.length < this.maxQueueSize) {
151
+ this.queue.push(error);
152
+ } else {
153
+ console.warn("[MonitorClient] Queue full, dropping error");
154
+ this.retryCount.delete(key);
155
+ }
156
+ } else {
157
+ console.warn("[MonitorClient] Max retries exceeded, dropping error");
158
+ this.retryCount.delete(key);
159
+ }
160
+ }
161
+ } finally {
162
+ this.isFlushInProgress = false;
163
+ }
164
+ }
165
+ getErrorKey(error) {
166
+ return `${error.message}-${error.stack?.substring(0, 100) || ""}-${error.route || ""}`;
167
+ }
168
+ async close() {
169
+ this.isClosed = true;
170
+ this.stopFlushTimer();
171
+ await this.flush();
172
+ }
173
+ enqueue(payload) {
174
+ if (this.queue.length >= this.maxQueueSize) {
175
+ console.warn("[MonitorClient] Queue full, dropping oldest error");
176
+ this.queue.shift();
177
+ }
178
+ this.queue.push(payload);
179
+ if (this.queue.length >= this.batchSize) {
180
+ this.flush();
181
+ }
182
+ }
183
+ /**
184
+ * Fetch with timeout to prevent hanging requests
185
+ */
186
+ async fetchWithTimeout(url, options, timeoutMs = this.requestTimeoutMs) {
187
+ const controller = new AbortController();
188
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
189
+ try {
190
+ const response = await fetch(url, {
191
+ ...options,
192
+ signal: controller.signal
193
+ });
194
+ return response;
195
+ } catch (err) {
196
+ if (err instanceof Error && err.name === "AbortError") {
197
+ throw new Error(`Request timeout after ${timeoutMs}ms`);
198
+ }
199
+ throw err;
200
+ } finally {
201
+ clearTimeout(timeoutId);
202
+ }
203
+ }
204
+ async sendSingle(error) {
205
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/errors`, {
206
+ method: "POST",
207
+ headers: {
208
+ "Content-Type": "application/json",
209
+ Authorization: `Bearer ${this.apiKey}`
210
+ },
211
+ body: JSON.stringify(error)
212
+ });
213
+ if (!response.ok) {
214
+ const errorText = await response.text().catch(() => "");
215
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
216
+ }
217
+ }
218
+ async sendBatch(errors) {
219
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/errors/batch`, {
220
+ method: "POST",
221
+ headers: {
222
+ "Content-Type": "application/json",
223
+ Authorization: `Bearer ${this.apiKey}`
224
+ },
225
+ body: JSON.stringify({ errors })
226
+ });
227
+ if (!response.ok) {
228
+ const errorText = await response.text().catch(() => "");
229
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
230
+ }
231
+ }
232
+ startFlushTimer() {
233
+ this.flushTimer = setInterval(() => {
234
+ this.flush();
235
+ }, this.flushIntervalMs);
236
+ }
237
+ stopFlushTimer() {
238
+ if (this.flushTimer) {
239
+ clearInterval(this.flushTimer);
240
+ this.flushTimer = null;
241
+ }
242
+ }
243
+ async syncDependencies() {
244
+ try {
245
+ if (this.dependencySources && this.dependencySources.length > 0) {
246
+ for (const source of this.dependencySources) {
247
+ const technologies = await this.readPackageJsonFromPath(source.path);
248
+ if (technologies.length === 0) continue;
249
+ await this.sendTechnologiesWithEnvironment(technologies, source.environment);
250
+ }
251
+ } else {
252
+ const technologies = await this.readPackageJson();
253
+ if (technologies.length === 0) return;
254
+ await this.sendTechnologies(technologies);
255
+ }
256
+ } catch (err) {
257
+ console.error("[MonitorClient] Failed to sync dependencies:", err);
258
+ }
259
+ }
260
+ async syncTechnologies(technologies) {
261
+ await this.sendTechnologies(technologies);
262
+ }
263
+ async readPackageJson() {
264
+ const packagePath = this.packageJsonPath || "package.json";
265
+ return this.readPackageJsonFromPath(packagePath);
266
+ }
267
+ async readPackageJsonFromPath(packagePath) {
268
+ if (typeof process === "undefined" || typeof process.cwd !== "function") {
269
+ return [];
270
+ }
271
+ try {
272
+ const fsModule = await import("fs");
273
+ const pathModule = await import("path");
274
+ const fs = fsModule.default || fsModule;
275
+ const path = pathModule.default || pathModule;
276
+ const baseDir = process.cwd();
277
+ const resolvedPath = path.isAbsolute(packagePath) ? packagePath : path.join(baseDir, packagePath);
278
+ const normalizedPath = path.normalize(resolvedPath);
279
+ const normalizedBase = path.normalize(baseDir);
280
+ if (!path.isAbsolute(packagePath)) {
281
+ if (!normalizedPath.startsWith(normalizedBase)) {
282
+ console.warn("[MonitorClient] Path traversal attempt blocked:", packagePath);
283
+ return [];
284
+ }
285
+ }
286
+ if (packagePath.includes("\0") || /\.\.[\\/]/.test(packagePath)) {
287
+ console.warn("[MonitorClient] Suspicious path blocked:", packagePath);
288
+ return [];
289
+ }
290
+ if (!normalizedPath.endsWith("package.json")) {
291
+ console.warn("[MonitorClient] Path must point to package.json");
292
+ return [];
293
+ }
294
+ if (!fs.existsSync(normalizedPath)) {
295
+ return [];
296
+ }
297
+ const packageJson = JSON.parse(fs.readFileSync(normalizedPath, "utf-8"));
298
+ const technologies = [];
299
+ const deps = {
300
+ ...packageJson.dependencies,
301
+ ...packageJson.devDependencies
302
+ };
303
+ for (const [name, version] of Object.entries(deps)) {
304
+ if (typeof version === "string") {
305
+ if (this.shouldExclude(name)) {
306
+ continue;
307
+ }
308
+ technologies.push({
309
+ name,
310
+ version: version.replace(/^[\^~]/, "")
311
+ });
312
+ }
313
+ }
314
+ return technologies;
315
+ } catch {
316
+ return [];
317
+ }
318
+ }
319
+ shouldExclude(packageName) {
320
+ for (const pattern of this.excludePatterns) {
321
+ const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
322
+ if (new RegExp(`^${regexPattern}$`).test(packageName)) {
323
+ return true;
324
+ }
325
+ }
326
+ return false;
327
+ }
328
+ async sendTechnologies(technologies) {
329
+ await this.sendTechnologiesWithEnvironment(technologies, this.environment);
330
+ }
331
+ async sendTechnologiesWithEnvironment(technologies, environment) {
332
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/technologies/sync`, {
333
+ method: "POST",
334
+ headers: {
335
+ "Content-Type": "application/json",
336
+ Authorization: `Bearer ${this.apiKey}`
337
+ },
338
+ body: JSON.stringify({
339
+ environment,
340
+ technologies
341
+ })
342
+ });
343
+ if (!response.ok) {
344
+ const errorText = await response.text().catch(() => "");
345
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
346
+ }
347
+ }
348
+ /**
349
+ * Capture a security event (auth failures, rate limits, suspicious activity, etc.)
350
+ * Returns brute force warning if pattern is detected
351
+ */
352
+ async captureSecurityEvent(input) {
353
+ if (this.isClosed) return {};
354
+ const payload = {
355
+ ...input,
356
+ environment: this.environment
357
+ };
358
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/security`, {
359
+ method: "POST",
360
+ headers: {
361
+ "Content-Type": "application/json",
362
+ Authorization: `Bearer ${this.apiKey}`
363
+ },
364
+ body: JSON.stringify(payload)
365
+ });
366
+ if (!response.ok) {
367
+ const errorText = await response.text().catch(() => "");
368
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
369
+ }
370
+ const result = await response.json();
371
+ return { warning: result.warning };
372
+ }
373
+ /**
374
+ * Capture a login failure event (convenience method)
375
+ */
376
+ async captureLoginFailure(options) {
377
+ return this.captureSecurityEvent({
378
+ eventType: `login_failed_${options.authMethod || "other"}`,
379
+ category: "AUTHENTICATION",
380
+ severity: "MEDIUM",
381
+ ip: options.ip,
382
+ identifier: options.identifier,
383
+ endpoint: options.endpoint,
384
+ userAgent: options.userAgent,
385
+ metadata: { reason: options.reason, authMethod: options.authMethod }
386
+ });
387
+ }
388
+ /**
389
+ * Capture a successful login event
390
+ */
391
+ async captureLoginSuccess(options) {
392
+ await this.captureSecurityEvent({
393
+ eventType: `login_success_${options.authMethod || "other"}`,
394
+ category: "AUTHENTICATION",
395
+ severity: "LOW",
396
+ ip: options.ip,
397
+ identifier: options.identifier,
398
+ endpoint: options.endpoint,
399
+ userAgent: options.userAgent,
400
+ metadata: { authMethod: options.authMethod }
401
+ });
402
+ }
403
+ /**
404
+ * Capture a rate limit event
405
+ */
406
+ async captureRateLimit(options) {
407
+ await this.captureSecurityEvent({
408
+ eventType: "rate_limit_exceeded",
409
+ category: "RATE_LIMIT",
410
+ severity: "MEDIUM",
411
+ ip: options.ip,
412
+ identifier: options.identifier,
413
+ endpoint: options.endpoint,
414
+ userAgent: options.userAgent,
415
+ metadata: { limit: options.limit, window: options.window }
416
+ });
417
+ }
418
+ /**
419
+ * Capture an authorization failure (user tried to access unauthorized resource)
420
+ */
421
+ async captureAuthorizationFailure(options) {
422
+ await this.captureSecurityEvent({
423
+ eventType: "authorization_denied",
424
+ category: "AUTHORIZATION",
425
+ severity: "MEDIUM",
426
+ ip: options.ip,
427
+ identifier: options.identifier,
428
+ endpoint: options.endpoint,
429
+ userAgent: options.userAgent,
430
+ metadata: { resource: options.resource, action: options.action }
431
+ });
432
+ }
433
+ /**
434
+ * Check if an IP or identifier has triggered brute force detection
435
+ */
436
+ async checkBruteForce(options) {
437
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/security/detect/brute-force`, {
438
+ method: "POST",
439
+ headers: {
440
+ "Content-Type": "application/json",
441
+ Authorization: `Bearer ${this.apiKey}`
442
+ },
443
+ body: JSON.stringify(options)
444
+ });
445
+ if (!response.ok) {
446
+ const errorText = await response.text().catch(() => "");
447
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
448
+ }
449
+ const result = await response.json();
450
+ return result.data;
451
+ }
452
+ /**
453
+ * Run npm audit and send results to the monitoring server.
454
+ * This scans the project for known vulnerabilities in dependencies.
455
+ *
456
+ * @param options.projectPath - Path to the project directory (defaults to cwd)
457
+ * @param options.environment - Environment label (defaults to client environment)
458
+ * @returns Audit summary with vulnerability counts
459
+ */
460
+ async auditDependencies(options = {}) {
461
+ if (typeof require === "undefined") {
462
+ console.warn("[MonitorClient] auditDependencies only works in Node.js environment");
463
+ return null;
464
+ }
465
+ const startTime = Date.now();
466
+ const environment = options.environment || this.environment;
467
+ try {
468
+ const { execSync } = require("child_process");
469
+ const path = require("path");
470
+ const fs = require("fs");
471
+ let projectPath = options.projectPath || process.cwd();
472
+ if (projectPath.includes("\0") || /[;&|`$(){}[\]<>]/.test(projectPath)) {
473
+ console.error("[MonitorClient] Invalid projectPath: contains forbidden characters");
474
+ return null;
475
+ }
476
+ projectPath = path.resolve(projectPath);
477
+ try {
478
+ const stats = fs.statSync(projectPath);
479
+ if (!stats.isDirectory()) {
480
+ console.error("[MonitorClient] projectPath is not a directory");
481
+ return null;
482
+ }
483
+ } catch {
484
+ console.error("[MonitorClient] projectPath does not exist");
485
+ return null;
486
+ }
487
+ const packageJsonPath = path.join(projectPath, "package.json");
488
+ if (!fs.existsSync(packageJsonPath)) {
489
+ console.error("[MonitorClient] No package.json found in projectPath");
490
+ return null;
491
+ }
492
+ let auditOutput;
493
+ try {
494
+ auditOutput = execSync("npm audit --json", {
495
+ cwd: projectPath,
496
+ encoding: "utf-8",
497
+ stdio: ["pipe", "pipe", "pipe"],
498
+ maxBuffer: 10 * 1024 * 1024,
499
+ // 10MB buffer for large outputs
500
+ timeout: 6e4
501
+ // 60 second timeout
502
+ });
503
+ } catch (err) {
504
+ const execError = err;
505
+ if (execError.killed) {
506
+ console.error("[MonitorClient] npm audit timed out");
507
+ return null;
508
+ }
509
+ if (execError.stdout) {
510
+ auditOutput = execError.stdout;
511
+ } else {
512
+ throw err;
513
+ }
514
+ }
515
+ const auditData = JSON.parse(auditOutput);
516
+ const vulnerabilities = this.parseNpmAuditOutput(auditData);
517
+ const totalDeps = auditData.metadata?.dependencies?.total || 0;
518
+ const scanDurationMs = Date.now() - startTime;
519
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/vulnerabilities/audit`, {
520
+ method: "POST",
521
+ headers: {
522
+ "Content-Type": "application/json",
523
+ Authorization: `Bearer ${this.apiKey}`
524
+ },
525
+ body: JSON.stringify({
526
+ environment,
527
+ totalDeps,
528
+ vulnerabilities,
529
+ scanDurationMs
530
+ })
531
+ });
532
+ if (!response.ok) {
533
+ const errorText = await response.text().catch(() => "");
534
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
535
+ }
536
+ const result = await response.json();
537
+ return result.data;
538
+ } catch (err) {
539
+ console.error("[MonitorClient] Failed to audit dependencies:", err instanceof Error ? err.message : String(err));
540
+ return null;
541
+ }
542
+ }
543
+ /**
544
+ * Parse npm audit JSON output into vulnerability items
545
+ */
546
+ parseNpmAuditOutput(auditData) {
547
+ const vulnerabilities = [];
548
+ if (!auditData.vulnerabilities) {
549
+ return vulnerabilities;
550
+ }
551
+ for (const [packageName, vuln] of Object.entries(auditData.vulnerabilities)) {
552
+ const viaDetails = vuln.via.find(
553
+ (v) => typeof v === "object" && "title" in v
554
+ );
555
+ if (viaDetails) {
556
+ vulnerabilities.push({
557
+ packageName,
558
+ severity: vuln.severity.toLowerCase(),
559
+ title: viaDetails.title,
560
+ url: viaDetails.url,
561
+ vulnerableRange: viaDetails.range,
562
+ installedVersion: vuln.nodes?.[0]?.split("@").pop(),
563
+ patchedVersions: this.getFixVersion(vuln.fixAvailable),
564
+ path: vuln.nodes?.join(" > "),
565
+ recommendation: this.getRecommendation(vuln.fixAvailable),
566
+ cwe: viaDetails.cwe,
567
+ cvss: viaDetails.cvss?.score,
568
+ isFixable: Boolean(vuln.fixAvailable),
569
+ isDirect: vuln.isDirect
570
+ });
571
+ } else {
572
+ const viaNames = vuln.via.filter((v) => typeof v === "string");
573
+ vulnerabilities.push({
574
+ packageName,
575
+ severity: vuln.severity.toLowerCase(),
576
+ title: `Depends on vulnerable ${viaNames.join(", ")}`,
577
+ vulnerableRange: vuln.range,
578
+ path: vuln.nodes?.join(" > "),
579
+ isFixable: Boolean(vuln.fixAvailable),
580
+ isDirect: vuln.isDirect
581
+ });
582
+ }
583
+ }
584
+ return vulnerabilities;
585
+ }
586
+ getFixVersion(fixAvailable) {
587
+ if (typeof fixAvailable === "object" && fixAvailable !== null) {
588
+ return fixAvailable.version;
589
+ }
590
+ return void 0;
591
+ }
592
+ getRecommendation(fixAvailable) {
593
+ if (typeof fixAvailable === "object" && fixAvailable !== null) {
594
+ const majorWarning = fixAvailable.isSemVerMajor ? " (breaking change)" : "";
595
+ return `Update ${fixAvailable.name} to ${fixAvailable.version}${majorWarning}`;
596
+ } else if (fixAvailable === true) {
597
+ return "Run npm audit fix";
598
+ }
599
+ return "No fix available";
600
+ }
601
+ };
602
+ // Annotate the CommonJS export names for ESM import in node:
603
+ 0 && (module.exports = {
604
+ MonitorClient
605
+ });