gateops-core 0.1.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1176 @@
1
+ import { v4 } from 'uuid';
2
+ import Database from 'better-sqlite3';
3
+ import { drizzle } from 'drizzle-orm/better-sqlite3';
4
+ import { eq, like, gte, lte, and, desc, sql } from 'drizzle-orm';
5
+ import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
6
+ import * as fs2 from 'fs';
7
+ import * as path from 'path';
8
+ import axios2 from 'axios';
9
+ import { Router } from 'express';
10
+
11
+ var __defProp = Object.defineProperty;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __commonJS = (cb, mod) => function __require() {
14
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
15
+ };
16
+ var __export = (target, all) => {
17
+ for (var name in all)
18
+ __defProp(target, name, { get: all[name], enumerable: true });
19
+ };
20
+
21
+ // package.json
22
+ var require_package = __commonJS({
23
+ "package.json"(exports$1, module) {
24
+ module.exports = {
25
+ name: "gateops-core",
26
+ version: "0.1.0",
27
+ description: "Lightweight API observability SDK for Express.js - Zero-config, privacy-first traffic monitoring",
28
+ main: "dist/index.js",
29
+ module: "dist/index.mjs",
30
+ types: "dist/index.d.ts",
31
+ bin: {
32
+ "gateops-init": "dist/bin/gateops-init.js"
33
+ },
34
+ files: [
35
+ "dist"
36
+ ],
37
+ scripts: {
38
+ build: "tsup",
39
+ dev: "tsup --watch",
40
+ test: "vitest",
41
+ "test:run": "vitest run",
42
+ lint: "eslint src --ext .ts",
43
+ format: "prettier --write src",
44
+ prepublishOnly: "npm run build"
45
+ },
46
+ keywords: [
47
+ "api",
48
+ "observability",
49
+ "monitoring",
50
+ "express",
51
+ "middleware",
52
+ "logging",
53
+ "debugging",
54
+ "documentation",
55
+ "swagger",
56
+ "openapi"
57
+ ],
58
+ author: "GateOps",
59
+ license: "MIT",
60
+ repository: {
61
+ type: "git",
62
+ url: "https://github.com/gateops/gateops-core"
63
+ },
64
+ engines: {
65
+ node: ">=18.0.0"
66
+ },
67
+ peerDependencies: {
68
+ express: "^4.18.0 || ^5.0.0"
69
+ },
70
+ dependencies: {
71
+ axios: "^1.6.0",
72
+ "better-sqlite3": "^11.0.0",
73
+ chalk: "^4.1.2",
74
+ commander: "^12.0.0",
75
+ "drizzle-orm": "^0.29.0",
76
+ inquirer: "^8.2.6",
77
+ uuid: "^9.0.0",
78
+ zod: "^3.22.0"
79
+ },
80
+ devDependencies: {
81
+ "@types/better-sqlite3": "^7.6.8",
82
+ "@types/express": "^4.17.21",
83
+ "@types/inquirer": "^8.2.10",
84
+ "@types/node": "^20.10.0",
85
+ "@types/uuid": "^9.0.7",
86
+ "drizzle-kit": "^0.20.0",
87
+ eslint: "^8.56.0",
88
+ express: "^4.18.2",
89
+ prettier: "^3.2.0",
90
+ tsup: "^8.0.0",
91
+ typescript: "^5.3.0",
92
+ vitest: "^1.2.0"
93
+ }
94
+ };
95
+ }
96
+ });
97
+
98
+ // src/config.ts
99
+ var PANEL_BASE_URL = process.env.GATEOPS_PANEL_URL || "https://gateops.sleeksoulsmedia.com";
100
+ var PANEL_ENDPOINTS = {
101
+ // Verify API key during initialization
102
+ VERIFY: "/api/sdk/verify",
103
+ // Send batched logs to Panel
104
+ LOGS: "/api/sdk/logs",
105
+ // Fetch remote configuration (optional)
106
+ CONFIG: "/api/sdk/config",
107
+ // Send discovered endpoints/routes
108
+ ENDPOINTS: "/api/sdk/endpoints",
109
+ // Heartbeat/health check
110
+ HEARTBEAT: "/api/sdk/heartbeat"
111
+ };
112
+ var SDK_DEFAULTS = {
113
+ // Buffer settings
114
+ BUFFER_MAX_SIZE: 50,
115
+ // Flush when buffer reaches this size
116
+ BUFFER_FLUSH_INTERVAL: 1e4,
117
+ // Flush every 10 seconds (ms)
118
+ // Payload limits
119
+ MAX_BODY_SIZE: 10240,
120
+ // 10KB max for req/res bodies
121
+ // Log retention
122
+ LOG_TTL_DAYS: 15,
123
+ // Auto-cleanup logs older than 15 days
124
+ // Retry settings
125
+ MAX_RETRY_ATTEMPTS: 3,
126
+ RETRY_DELAY_BASE: 1e3,
127
+ // Base delay for exponential backoff (ms)
128
+ // Database
129
+ DEFAULT_DB_PATH: ".gateops/data.sqlite",
130
+ BUFFER_FILE_PATH: ".gateops/buffer.json",
131
+ // Route prefix for exposed endpoints
132
+ ROUTE_PREFIX: "/.well-known/gateops"
133
+ };
134
+ var SENSITIVE_PATTERNS = {
135
+ // Field names to redact (case-insensitive)
136
+ FIELD_NAMES: [
137
+ "password",
138
+ "passwd",
139
+ "secret",
140
+ "token",
141
+ "apikey",
142
+ "api_key",
143
+ "api-key",
144
+ "authorization",
145
+ "auth",
146
+ "bearer",
147
+ "credential",
148
+ "private",
149
+ "ssn",
150
+ "credit_card",
151
+ "creditcard",
152
+ "card_number",
153
+ "cvv",
154
+ "pin"
155
+ ],
156
+ // Regex patterns for value-based detection
157
+ VALUE_PATTERNS: {
158
+ // Credit card (basic pattern)
159
+ CREDIT_CARD: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
160
+ // SSN pattern
161
+ SSN: /\b\d{3}[-]?\d{2}[-]?\d{4}\b/g
162
+ },
163
+ // Replacement text
164
+ REDACTED: "[REDACTED]",
165
+ MASKED_CARD: "****-****-****-****"
166
+ };
167
+ function getPanelUrl(endpoint) {
168
+ return `${PANEL_BASE_URL}${PANEL_ENDPOINTS[endpoint]}`;
169
+ }
170
+
171
+ // src/db/schema.ts
172
+ var schema_exports = {};
173
+ __export(schema_exports, {
174
+ config: () => config,
175
+ endpoints: () => endpoints,
176
+ trafficLogs: () => trafficLogs
177
+ });
178
+ var trafficLogs = sqliteTable("gateops_traffic_logs", {
179
+ id: integer("id").primaryKey({ autoIncrement: true }),
180
+ // Unique identifier for request tracing
181
+ trace_id: text("trace_id").notNull(),
182
+ // Request info
183
+ endpoint: text("endpoint").notNull(),
184
+ method: text("method").notNull(),
185
+ status: integer("status").notNull(),
186
+ duration_ms: integer("duration_ms").notNull(),
187
+ // Headers (JSON strings)
188
+ req_headers: text("req_headers"),
189
+ res_headers: text("res_headers"),
190
+ // Bodies (JSON strings, sanitized, max 10KB each)
191
+ req_body: text("req_body"),
192
+ res_body: text("res_body"),
193
+ // Additional context
194
+ query_params: text("query_params"),
195
+ ip_address: text("ip_address"),
196
+ user_agent: text("user_agent"),
197
+ // Timestamp (stored as Unix timestamp)
198
+ created_at: integer("created_at", { mode: "timestamp" }).notNull()
199
+ });
200
+ var endpoints = sqliteTable("gateops_endpoints", {
201
+ id: integer("id").primaryKey({ autoIncrement: true }),
202
+ // Route identification (unique constraint on path + method)
203
+ path: text("path").notNull(),
204
+ method: text("method").notNull(),
205
+ // Inferred request/response schema (JSON string)
206
+ // Deep inference captures nested structures
207
+ detected_schema: text("detected_schema"),
208
+ // List of middleware names (JSON array)
209
+ middleware_names: text("middleware_names"),
210
+ // Last time this endpoint was hit
211
+ last_seen: integer("last_seen", { mode: "timestamp" }).notNull()
212
+ });
213
+ var config = sqliteTable("gateops_config", {
214
+ // Unique key name
215
+ key: text("key").primaryKey(),
216
+ // Value as JSON string
217
+ value: text("value").notNull(),
218
+ // When this config was last updated
219
+ updated_at: integer("updated_at", { mode: "timestamp" })
220
+ });
221
+ var db = null;
222
+ var sqliteDb = null;
223
+ function getDatabase(dbPath) {
224
+ if (db) {
225
+ return db;
226
+ }
227
+ const resolvedPath = dbPath || process.env.GATEOPS_DB_PATH || SDK_DEFAULTS.DEFAULT_DB_PATH;
228
+ const absolutePath = path.isAbsolute(resolvedPath) ? resolvedPath : path.join(process.cwd(), resolvedPath);
229
+ const dir = path.dirname(absolutePath);
230
+ if (!fs2.existsSync(dir)) {
231
+ fs2.mkdirSync(dir, { recursive: true });
232
+ }
233
+ sqliteDb = new Database(absolutePath);
234
+ sqliteDb.pragma("journal_mode = WAL");
235
+ db = drizzle(sqliteDb, { schema: schema_exports });
236
+ return db;
237
+ }
238
+ function initializeSchema(database) {
239
+ const targetDb = database || getDatabase();
240
+ targetDb.run(sql`
241
+ CREATE TABLE IF NOT EXISTS gateops_traffic_logs (
242
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
243
+ trace_id TEXT NOT NULL,
244
+ endpoint TEXT NOT NULL,
245
+ method TEXT NOT NULL,
246
+ status INTEGER NOT NULL,
247
+ duration_ms INTEGER NOT NULL,
248
+ req_headers TEXT,
249
+ res_headers TEXT,
250
+ req_body TEXT,
251
+ res_body TEXT,
252
+ query_params TEXT,
253
+ ip_address TEXT,
254
+ user_agent TEXT,
255
+ created_at INTEGER NOT NULL
256
+ )
257
+ `);
258
+ targetDb.run(sql`
259
+ CREATE INDEX IF NOT EXISTS idx_traffic_logs_created_at
260
+ ON gateops_traffic_logs(created_at)
261
+ `);
262
+ targetDb.run(sql`
263
+ CREATE INDEX IF NOT EXISTS idx_traffic_logs_endpoint
264
+ ON gateops_traffic_logs(endpoint, method)
265
+ `);
266
+ targetDb.run(sql`
267
+ CREATE INDEX IF NOT EXISTS idx_traffic_logs_status
268
+ ON gateops_traffic_logs(status)
269
+ `);
270
+ targetDb.run(sql`
271
+ CREATE TABLE IF NOT EXISTS gateops_endpoints (
272
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
273
+ path TEXT NOT NULL,
274
+ method TEXT NOT NULL,
275
+ detected_schema TEXT,
276
+ middleware_names TEXT,
277
+ last_seen INTEGER NOT NULL,
278
+ UNIQUE(path, method)
279
+ )
280
+ `);
281
+ targetDb.run(sql`
282
+ CREATE TABLE IF NOT EXISTS gateops_config (
283
+ key TEXT PRIMARY KEY,
284
+ value TEXT NOT NULL,
285
+ updated_at INTEGER
286
+ )
287
+ `);
288
+ }
289
+ function closeDatabase() {
290
+ if (sqliteDb) {
291
+ sqliteDb.close();
292
+ sqliteDb = null;
293
+ db = null;
294
+ }
295
+ }
296
+ function cleanupOldLogs(ttlDays = SDK_DEFAULTS.LOG_TTL_DAYS) {
297
+ const database = getDatabase();
298
+ const cutoffDate = /* @__PURE__ */ new Date();
299
+ cutoffDate.setDate(cutoffDate.getDate() - ttlDays);
300
+ const result = database.run(sql`
301
+ DELETE FROM gateops_traffic_logs
302
+ WHERE created_at < ${Math.floor(cutoffDate.getTime() / 1e3)}
303
+ `);
304
+ return result.changes;
305
+ }
306
+ var BUFFER_VERSION = "1.0.0";
307
+ function getBufferFilePath() {
308
+ return path.join(process.cwd(), SDK_DEFAULTS.BUFFER_FILE_PATH);
309
+ }
310
+ function saveToDisk(logs) {
311
+ if (logs.length === 0) {
312
+ return true;
313
+ }
314
+ try {
315
+ const filePath = getBufferFilePath();
316
+ const dir = path.dirname(filePath);
317
+ if (!fs2.existsSync(dir)) {
318
+ fs2.mkdirSync(dir, { recursive: true });
319
+ }
320
+ const state = {
321
+ logs,
322
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
323
+ version: BUFFER_VERSION
324
+ };
325
+ fs2.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
326
+ return true;
327
+ } catch (error) {
328
+ console.error("[GateOps] Failed to save buffer to disk:", error);
329
+ return false;
330
+ }
331
+ }
332
+ function loadFromDisk() {
333
+ try {
334
+ const filePath = getBufferFilePath();
335
+ if (!fs2.existsSync(filePath)) {
336
+ return [];
337
+ }
338
+ const content = fs2.readFileSync(filePath, "utf-8");
339
+ const state = JSON.parse(content);
340
+ if (state.version !== BUFFER_VERSION) {
341
+ console.warn("[GateOps] Buffer file version mismatch, skipping recovery");
342
+ clearDiskBuffer();
343
+ return [];
344
+ }
345
+ const logs = state.logs.map((log) => ({
346
+ ...log,
347
+ created_at: new Date(log.created_at)
348
+ }));
349
+ console.log(`[GateOps] Recovered ${logs.length} logs from disk buffer`);
350
+ return logs;
351
+ } catch (error) {
352
+ console.error("[GateOps] Failed to load buffer from disk:", error);
353
+ return [];
354
+ }
355
+ }
356
+ function clearDiskBuffer() {
357
+ try {
358
+ const filePath = getBufferFilePath();
359
+ if (fs2.existsSync(filePath)) {
360
+ fs2.unlinkSync(filePath);
361
+ }
362
+ return true;
363
+ } catch (error) {
364
+ console.error("[GateOps] Failed to clear disk buffer:", error);
365
+ return false;
366
+ }
367
+ }
368
+ function hasDiskBuffer() {
369
+ return fs2.existsSync(getBufferFilePath());
370
+ }
371
+ var BufferManager = class {
372
+ buffer = [];
373
+ flushInterval = null;
374
+ isInitialized = false;
375
+ isFlushing = false;
376
+ maxSize;
377
+ intervalMs;
378
+ localOnly;
379
+ username;
380
+ apiKey;
381
+ constructor() {
382
+ this.maxSize = SDK_DEFAULTS.BUFFER_MAX_SIZE;
383
+ this.intervalMs = SDK_DEFAULTS.BUFFER_FLUSH_INTERVAL;
384
+ this.localOnly = false;
385
+ this.username = "";
386
+ this.apiKey = "";
387
+ }
388
+ /**
389
+ * Initialize the buffer manager
390
+ */
391
+ initialize(options = {}) {
392
+ if (this.isInitialized) {
393
+ return;
394
+ }
395
+ this.maxSize = options.maxSize || SDK_DEFAULTS.BUFFER_MAX_SIZE;
396
+ this.intervalMs = options.intervalMs || SDK_DEFAULTS.BUFFER_FLUSH_INTERVAL;
397
+ this.localOnly = options.localOnly || false;
398
+ this.username = options.username || process.env.GATEOPS_USERNAME || "";
399
+ this.apiKey = options.apiKey || process.env.GATEOPS_API_KEY || "";
400
+ this.recoverFromDisk();
401
+ this.startFlushInterval();
402
+ this.registerShutdownHandlers();
403
+ this.isInitialized = true;
404
+ }
405
+ /**
406
+ * Push a log entry to the buffer
407
+ */
408
+ push(log) {
409
+ this.buffer.push(log);
410
+ if (this.buffer.length >= this.maxSize) {
411
+ this.flush();
412
+ }
413
+ }
414
+ /**
415
+ * Flush the buffer to database and Panel
416
+ */
417
+ async flush() {
418
+ if (this.isFlushing || this.buffer.length === 0) {
419
+ return;
420
+ }
421
+ this.isFlushing = true;
422
+ const logsToFlush = [...this.buffer];
423
+ this.buffer = [];
424
+ try {
425
+ await this.writeToDatabase(logsToFlush);
426
+ if (!this.localOnly && this.username && this.apiKey) {
427
+ await this.sendToPanel(logsToFlush);
428
+ }
429
+ clearDiskBuffer();
430
+ } catch (error) {
431
+ console.error("[GateOps] Flush failed:", error);
432
+ this.buffer = [...logsToFlush, ...this.buffer];
433
+ saveToDisk(this.buffer);
434
+ } finally {
435
+ this.isFlushing = false;
436
+ }
437
+ }
438
+ /**
439
+ * Write logs to local SQLite database
440
+ */
441
+ async writeToDatabase(logs) {
442
+ const db2 = getDatabase();
443
+ for (const log of logs) {
444
+ await db2.insert(trafficLogs).values({
445
+ trace_id: log.trace_id,
446
+ endpoint: log.endpoint,
447
+ method: log.method,
448
+ status: log.status,
449
+ duration_ms: log.duration_ms,
450
+ req_headers: log.req_headers,
451
+ res_headers: log.res_headers,
452
+ req_body: log.req_body,
453
+ res_body: log.res_body,
454
+ query_params: log.query_params,
455
+ ip_address: log.ip_address,
456
+ user_agent: log.user_agent,
457
+ created_at: log.created_at
458
+ });
459
+ }
460
+ }
461
+ /**
462
+ * Send logs to Panel API
463
+ */
464
+ async sendToPanel(logs) {
465
+ const url = getPanelUrl("LOGS");
466
+ try {
467
+ await axios2.post(url, {
468
+ logs: logs.map((log) => ({
469
+ ...log,
470
+ created_at: log.created_at.toISOString()
471
+ }))
472
+ }, {
473
+ headers: {
474
+ "Content-Type": "application/json",
475
+ "x-gateops-username": this.username,
476
+ "x-gateops-api-key": this.apiKey
477
+ },
478
+ timeout: 1e4
479
+ // 10 second timeout
480
+ });
481
+ } catch (error) {
482
+ console.warn(
483
+ "[GateOps] Failed to send logs to Panel:",
484
+ axios2.isAxiosError(error) ? error.message : error
485
+ );
486
+ }
487
+ }
488
+ /**
489
+ * Recover logs from disk buffer
490
+ */
491
+ recoverFromDisk() {
492
+ if (hasDiskBuffer()) {
493
+ const recoveredLogs = loadFromDisk();
494
+ if (recoveredLogs.length > 0) {
495
+ this.buffer = [...recoveredLogs, ...this.buffer];
496
+ clearDiskBuffer();
497
+ setTimeout(() => this.flush(), 1e3);
498
+ }
499
+ }
500
+ }
501
+ /**
502
+ * Start the periodic flush interval
503
+ */
504
+ startFlushInterval() {
505
+ if (this.flushInterval) {
506
+ clearInterval(this.flushInterval);
507
+ }
508
+ this.flushInterval = setInterval(() => {
509
+ this.flush();
510
+ }, this.intervalMs);
511
+ this.flushInterval.unref();
512
+ }
513
+ /**
514
+ * Register handlers for graceful shutdown
515
+ */
516
+ registerShutdownHandlers() {
517
+ const gracefulShutdown = () => {
518
+ if (this.buffer.length > 0) {
519
+ saveToDisk(this.buffer);
520
+ }
521
+ this.flush();
522
+ if (this.flushInterval) {
523
+ clearInterval(this.flushInterval);
524
+ }
525
+ };
526
+ process.on("SIGINT", gracefulShutdown);
527
+ process.on("SIGTERM", gracefulShutdown);
528
+ process.on("beforeExit", gracefulShutdown);
529
+ process.on("uncaughtException", (error) => {
530
+ console.error("[GateOps] Uncaught exception, saving buffer to disk:", error);
531
+ saveToDisk(this.buffer);
532
+ });
533
+ }
534
+ /**
535
+ * Get current buffer size
536
+ */
537
+ getSize() {
538
+ return this.buffer.length;
539
+ }
540
+ /**
541
+ * Shutdown the buffer manager
542
+ */
543
+ shutdown() {
544
+ if (this.flushInterval) {
545
+ clearInterval(this.flushInterval);
546
+ this.flushInterval = null;
547
+ }
548
+ if (this.buffer.length > 0) {
549
+ saveToDisk(this.buffer);
550
+ }
551
+ this.isInitialized = false;
552
+ }
553
+ };
554
+ var buffer = new BufferManager();
555
+
556
+ // src/sanitizer.ts
557
+ function sanitize(data, maxSize = SDK_DEFAULTS.MAX_BODY_SIZE, customFields = []) {
558
+ if (data === null || data === void 0) {
559
+ return data;
560
+ }
561
+ const sensitiveFields = [
562
+ ...SENSITIVE_PATTERNS.FIELD_NAMES,
563
+ ...customFields.map((f) => f.toLowerCase())
564
+ ];
565
+ const sanitized = deepSanitize(data, sensitiveFields);
566
+ const jsonString = JSON.stringify(sanitized);
567
+ if (jsonString.length > maxSize) {
568
+ return {
569
+ _truncated: true,
570
+ _originalSize: jsonString.length,
571
+ _message: `Payload exceeded ${maxSize} bytes, truncated`,
572
+ _preview: jsonString.substring(0, 500) + "..."
573
+ };
574
+ }
575
+ return sanitized;
576
+ }
577
+ function deepSanitize(data, sensitiveFields) {
578
+ if (typeof data !== "object" || data === null) {
579
+ if (typeof data === "string") {
580
+ return sanitizeString(data);
581
+ }
582
+ return data;
583
+ }
584
+ if (Array.isArray(data)) {
585
+ return data.map((item) => deepSanitize(item, sensitiveFields));
586
+ }
587
+ const result = {};
588
+ for (const [key, value] of Object.entries(data)) {
589
+ const lowerKey = key.toLowerCase();
590
+ if (sensitiveFields.some((field) => lowerKey.includes(field))) {
591
+ result[key] = SENSITIVE_PATTERNS.REDACTED;
592
+ } else if (typeof value === "object" && value !== null) {
593
+ result[key] = deepSanitize(value, sensitiveFields);
594
+ } else if (typeof value === "string") {
595
+ result[key] = sanitizeString(value);
596
+ } else {
597
+ result[key] = value;
598
+ }
599
+ }
600
+ return result;
601
+ }
602
+ function sanitizeString(value) {
603
+ let sanitized = value;
604
+ sanitized = sanitized.replace(
605
+ SENSITIVE_PATTERNS.VALUE_PATTERNS.CREDIT_CARD,
606
+ SENSITIVE_PATTERNS.MASKED_CARD
607
+ );
608
+ sanitized = sanitized.replace(
609
+ SENSITIVE_PATTERNS.VALUE_PATTERNS.SSN,
610
+ SENSITIVE_PATTERNS.REDACTED
611
+ );
612
+ return sanitized;
613
+ }
614
+ function safeStringify(obj, maxSize) {
615
+ if (obj === void 0 || obj === null) {
616
+ return void 0;
617
+ }
618
+ try {
619
+ const seen = /* @__PURE__ */ new WeakSet();
620
+ const result = JSON.stringify(obj, (key, value) => {
621
+ if (typeof value === "object" && value !== null) {
622
+ if (seen.has(value)) {
623
+ return "[Circular]";
624
+ }
625
+ seen.add(value);
626
+ }
627
+ if (typeof value === "bigint") {
628
+ return value.toString();
629
+ }
630
+ if (typeof value === "function") {
631
+ return "[Function]";
632
+ }
633
+ return value;
634
+ });
635
+ if (maxSize && result.length > maxSize) {
636
+ return result.substring(0, maxSize) + "...[truncated]";
637
+ }
638
+ return result;
639
+ } catch (error) {
640
+ return "[Unable to stringify]";
641
+ }
642
+ }
643
+ function sanitizeHeaders(headers) {
644
+ const sensitiveHeaders = [
645
+ "authorization",
646
+ "cookie",
647
+ "set-cookie",
648
+ "x-api-key",
649
+ "x-auth-token",
650
+ "x-access-token",
651
+ "proxy-authorization"
652
+ ];
653
+ const result = {};
654
+ for (const [key, value] of Object.entries(headers)) {
655
+ if (sensitiveHeaders.includes(key.toLowerCase())) {
656
+ result[key] = SENSITIVE_PATTERNS.REDACTED;
657
+ } else {
658
+ result[key] = value;
659
+ }
660
+ }
661
+ return result;
662
+ }
663
+
664
+ // src/middleware.ts
665
+ function createMiddleware(options = {}) {
666
+ const isEnabled = options.enabled ?? process.env.GATEOPS_ENABLED !== "false";
667
+ if (!isEnabled) {
668
+ return (_req, _res, next) => {
669
+ next();
670
+ };
671
+ }
672
+ initializeSchema();
673
+ buffer.initialize({
674
+ maxSize: options.bufferSize || SDK_DEFAULTS.BUFFER_MAX_SIZE,
675
+ intervalMs: options.flushInterval || SDK_DEFAULTS.BUFFER_FLUSH_INTERVAL,
676
+ localOnly: options.localOnly,
677
+ username: process.env.GATEOPS_USERNAME,
678
+ apiKey: process.env.GATEOPS_API_KEY
679
+ });
680
+ const excludePaths = /* @__PURE__ */ new Set([
681
+ ...options.excludePaths || [],
682
+ SDK_DEFAULTS.ROUTE_PREFIX,
683
+ // Don't log our own routes
684
+ "/.well-known/gateops"
685
+ ]);
686
+ const maxBodySize = options.maxBodySize || SDK_DEFAULTS.MAX_BODY_SIZE;
687
+ const sensitiveFields = options.sensitiveFields || [];
688
+ return function gateopsMiddleware(req, res, next) {
689
+ if (shouldExclude(req.path, excludePaths)) {
690
+ return next();
691
+ }
692
+ const traceId = v4();
693
+ const startTime = Date.now();
694
+ req.gateops = {
695
+ traceId,
696
+ startTime
697
+ };
698
+ const chunks = [];
699
+ const originalWrite = res.write.bind(res);
700
+ const originalEnd = res.end.bind(res);
701
+ res.write = function(chunk, encodingOrCallback, callback) {
702
+ if (chunk) {
703
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
704
+ }
705
+ if (typeof encodingOrCallback === "function") {
706
+ return originalWrite(chunk, encodingOrCallback);
707
+ }
708
+ return originalWrite(chunk, encodingOrCallback, callback);
709
+ };
710
+ res.end = function(chunk, encodingOrCallback, callback) {
711
+ if (chunk) {
712
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
713
+ }
714
+ const duration = Date.now() - startTime;
715
+ const responseBody = Buffer.concat(chunks).toString("utf-8");
716
+ const log = createLogEntry(
717
+ req,
718
+ res,
719
+ traceId,
720
+ duration,
721
+ responseBody,
722
+ maxBodySize,
723
+ sensitiveFields
724
+ );
725
+ buffer.push(log);
726
+ if (typeof encodingOrCallback === "function") {
727
+ return originalEnd(chunk, encodingOrCallback);
728
+ }
729
+ return originalEnd(chunk, encodingOrCallback, callback);
730
+ };
731
+ next();
732
+ };
733
+ }
734
+ function shouldExclude(path3, excludePaths) {
735
+ if (excludePaths.has(path3)) {
736
+ return true;
737
+ }
738
+ for (const excluded of excludePaths) {
739
+ if (path3.startsWith(excluded)) {
740
+ return true;
741
+ }
742
+ }
743
+ return false;
744
+ }
745
+ function createLogEntry(req, res, traceId, duration, responseBody, maxBodySize, sensitiveFields) {
746
+ let reqBody = req.body;
747
+ if (typeof reqBody === "object") {
748
+ reqBody = sanitize(reqBody, maxBodySize, sensitiveFields);
749
+ }
750
+ let resBody;
751
+ try {
752
+ resBody = JSON.parse(responseBody);
753
+ resBody = sanitize(resBody, maxBodySize, sensitiveFields);
754
+ } catch {
755
+ resBody = responseBody.length > maxBodySize ? responseBody.substring(0, maxBodySize) + "...[truncated]" : responseBody;
756
+ }
757
+ const ip = req.ip || req.headers["x-forwarded-for"]?.toString().split(",")[0] || req.socket?.remoteAddress || "unknown";
758
+ return {
759
+ trace_id: traceId,
760
+ endpoint: req.path,
761
+ method: req.method,
762
+ status: res.statusCode,
763
+ duration_ms: duration,
764
+ req_headers: safeStringify(sanitizeHeaders(req.headers)),
765
+ res_headers: safeStringify(sanitizeHeaders(res.getHeaders())),
766
+ req_body: safeStringify(reqBody, maxBodySize),
767
+ res_body: safeStringify(resBody, maxBodySize),
768
+ query_params: safeStringify(req.query),
769
+ ip_address: ip,
770
+ user_agent: req.headers["user-agent"],
771
+ created_at: /* @__PURE__ */ new Date()
772
+ };
773
+ }
774
+ var middleware_default = createMiddleware;
775
+ var router = Router();
776
+ function authenticate(req, res, next) {
777
+ const username = req.headers["x-gateops-username"];
778
+ const apiKey = req.headers["x-gateops-api-key"];
779
+ const expectedUsername = process.env.GATEOPS_USERNAME;
780
+ const expectedApiKey = process.env.GATEOPS_API_KEY;
781
+ if (!expectedUsername || !expectedApiKey) {
782
+ res.status(503).json({
783
+ error: "GateOps not configured",
784
+ message: "GATEOPS_USERNAME and GATEOPS_API_KEY must be set"
785
+ });
786
+ return;
787
+ }
788
+ if (username !== expectedUsername || apiKey !== expectedApiKey) {
789
+ res.status(401).json({
790
+ error: "Unauthorized",
791
+ message: "Invalid credentials"
792
+ });
793
+ return;
794
+ }
795
+ next();
796
+ }
797
+ router.use(authenticate);
798
+ router.get("/health", (_req, res) => {
799
+ res.json({
800
+ status: "ok",
801
+ version: require_package().version,
802
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
803
+ });
804
+ });
805
+ router.get("/logs", async (req, res) => {
806
+ try {
807
+ const db2 = getDatabase();
808
+ const page = Math.max(1, parseInt(req.query.page) || 1);
809
+ const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 50));
810
+ const offset = (page - 1) * limit;
811
+ let query = db2.select().from(trafficLogs);
812
+ const conditions = [];
813
+ if (req.query.status) {
814
+ conditions.push(eq(trafficLogs.status, parseInt(req.query.status)));
815
+ }
816
+ if (req.query.method) {
817
+ conditions.push(eq(trafficLogs.method, req.query.method.toUpperCase()));
818
+ }
819
+ if (req.query.endpoint) {
820
+ conditions.push(like(trafficLogs.endpoint, `%${req.query.endpoint}%`));
821
+ }
822
+ if (req.query.from) {
823
+ conditions.push(gte(trafficLogs.created_at, new Date(req.query.from)));
824
+ }
825
+ if (req.query.to) {
826
+ conditions.push(lte(trafficLogs.created_at, new Date(req.query.to)));
827
+ }
828
+ if (req.query.minDuration) {
829
+ conditions.push(gte(trafficLogs.duration_ms, parseInt(req.query.minDuration)));
830
+ }
831
+ const results = await query.where(conditions.length > 0 ? and(...conditions) : void 0).orderBy(desc(trafficLogs.created_at)).limit(limit).offset(offset);
832
+ const countResult = await db2.select({ count: trafficLogs.id }).from(trafficLogs).where(conditions.length > 0 ? and(...conditions) : void 0);
833
+ const total = countResult.length;
834
+ res.json({
835
+ logs: results,
836
+ pagination: {
837
+ page,
838
+ limit,
839
+ total,
840
+ totalPages: Math.ceil(total / limit)
841
+ }
842
+ });
843
+ } catch (error) {
844
+ console.error("[GateOps] Error fetching logs:", error);
845
+ res.status(500).json({ error: "Failed to fetch logs" });
846
+ }
847
+ });
848
+ router.get("/logs/:traceId", async (req, res) => {
849
+ try {
850
+ const db2 = getDatabase();
851
+ const result = await db2.select().from(trafficLogs).where(eq(trafficLogs.trace_id, req.params.traceId)).limit(1);
852
+ if (result.length === 0) {
853
+ res.status(404).json({ error: "Log not found" });
854
+ return;
855
+ }
856
+ res.json(result[0]);
857
+ } catch (error) {
858
+ console.error("[GateOps] Error fetching log:", error);
859
+ res.status(500).json({ error: "Failed to fetch log" });
860
+ }
861
+ });
862
+ router.get("/endpoints", async (_req, res) => {
863
+ try {
864
+ const db2 = getDatabase();
865
+ const results = await db2.select().from(endpoints).orderBy(endpoints.path);
866
+ res.json({
867
+ endpoints: results,
868
+ total: results.length
869
+ });
870
+ } catch (error) {
871
+ console.error("[GateOps] Error fetching endpoints:", error);
872
+ res.status(500).json({ error: "Failed to fetch endpoints" });
873
+ }
874
+ });
875
+ router.get("/config", async (_req, res) => {
876
+ try {
877
+ const db2 = getDatabase();
878
+ const results = await db2.select().from(config);
879
+ const configObj = {};
880
+ for (const row of results) {
881
+ try {
882
+ configObj[row.key] = JSON.parse(row.value);
883
+ } catch {
884
+ configObj[row.key] = row.value;
885
+ }
886
+ }
887
+ res.json({
888
+ config: configObj,
889
+ defaults: {
890
+ bufferSize: SDK_DEFAULTS.BUFFER_MAX_SIZE,
891
+ flushInterval: SDK_DEFAULTS.BUFFER_FLUSH_INTERVAL,
892
+ maxBodySize: SDK_DEFAULTS.MAX_BODY_SIZE,
893
+ logTtlDays: SDK_DEFAULTS.LOG_TTL_DAYS
894
+ }
895
+ });
896
+ } catch (error) {
897
+ console.error("[GateOps] Error fetching config:", error);
898
+ res.status(500).json({ error: "Failed to fetch config" });
899
+ }
900
+ });
901
+ router.get("/stats", async (_req, res) => {
902
+ try {
903
+ const db2 = getDatabase();
904
+ const logsCount = await db2.select().from(trafficLogs);
905
+ const endpointsCount = await db2.select().from(endpoints);
906
+ const errors = logsCount.filter((l) => l.status >= 500);
907
+ const avgDuration = logsCount.length > 0 ? logsCount.reduce((sum, l) => sum + l.duration_ms, 0) / logsCount.length : 0;
908
+ res.json({
909
+ totalLogs: logsCount.length,
910
+ totalEndpoints: endpointsCount.length,
911
+ errorCount: errors.length,
912
+ errorRate: logsCount.length > 0 ? (errors.length / logsCount.length * 100).toFixed(2) + "%" : "0%",
913
+ avgDurationMs: Math.round(avgDuration)
914
+ });
915
+ } catch (error) {
916
+ console.error("[GateOps] Error fetching stats:", error);
917
+ res.status(500).json({ error: "Failed to fetch stats" });
918
+ }
919
+ });
920
+ function scanRoutes(app) {
921
+ const routes = [];
922
+ const expressApp = app;
923
+ if (!expressApp._router) {
924
+ console.warn("[GateOps] No router found on app. Routes will be discovered on first request.");
925
+ return routes;
926
+ }
927
+ traverseStack(expressApp._router.stack, "", routes);
928
+ return routes;
929
+ }
930
+ function traverseStack(stack, basePath, routes) {
931
+ for (const layer of stack) {
932
+ if (layer.route) {
933
+ const routePath = joinPaths(basePath, layer.route.path);
934
+ const methods = Object.keys(layer.route.methods).filter((m) => layer.route.methods[m]).map((m) => m.toUpperCase());
935
+ const middlewareNames = layer.route.stack.map((l) => l.name).filter((n) => n && n !== "<anonymous>");
936
+ for (const method of methods) {
937
+ routes.push({
938
+ path: normalizePath(routePath),
939
+ method,
940
+ middleware_names: JSON.stringify(middlewareNames),
941
+ last_seen: /* @__PURE__ */ new Date()
942
+ });
943
+ }
944
+ } else if (layer.name === "router" && layer.handle) {
945
+ const routerPath = getLayerPath(layer);
946
+ const nestedRouter = layer.handle;
947
+ if (nestedRouter.stack) {
948
+ traverseStack(
949
+ nestedRouter.stack,
950
+ joinPaths(basePath, routerPath),
951
+ routes
952
+ );
953
+ }
954
+ }
955
+ }
956
+ }
957
+ function getLayerPath(layer) {
958
+ if (layer.path) {
959
+ return layer.path;
960
+ }
961
+ if (layer.regexp) {
962
+ const match = layer.regexp.toString().match(/^\/\^(.*?)\\\//);
963
+ if (match) {
964
+ return match[1].replace(/\\\//g, "/");
965
+ }
966
+ }
967
+ return "";
968
+ }
969
+ function joinPaths(base, path3) {
970
+ if (!base) return path3;
971
+ if (!path3) return base;
972
+ const normalizedBase = base.endsWith("/") ? base.slice(0, -1) : base;
973
+ const normalizedPath = path3.startsWith("/") ? path3 : `/${path3}`;
974
+ return normalizedBase + normalizedPath;
975
+ }
976
+ function normalizePath(path3) {
977
+ return path3.replace(/:(\w+)/g, "{$1}");
978
+ }
979
+ async function syncRoutesToDatabase(routes) {
980
+ const db2 = getDatabase();
981
+ const now = /* @__PURE__ */ new Date();
982
+ for (const route of routes) {
983
+ try {
984
+ const existing = await db2.select().from(endpoints).where(and(
985
+ eq(endpoints.path, route.path),
986
+ eq(endpoints.method, route.method)
987
+ )).limit(1);
988
+ if (existing.length > 0) {
989
+ await db2.update(endpoints).set({
990
+ last_seen: now,
991
+ middleware_names: route.middleware_names
992
+ }).where(and(
993
+ eq(endpoints.path, route.path),
994
+ eq(endpoints.method, route.method)
995
+ ));
996
+ } else {
997
+ await db2.insert(endpoints).values({
998
+ path: route.path,
999
+ method: route.method,
1000
+ middleware_names: route.middleware_names,
1001
+ last_seen: now
1002
+ });
1003
+ }
1004
+ } catch (error) {
1005
+ console.error(`[GateOps] Failed to sync route ${route.method} ${route.path}:`, error);
1006
+ }
1007
+ }
1008
+ }
1009
+ async function getEndpoints() {
1010
+ const db2 = getDatabase();
1011
+ const rows = await db2.select().from(endpoints);
1012
+ return rows.map((row) => ({
1013
+ id: row.id,
1014
+ path: row.path,
1015
+ method: row.method,
1016
+ detected_schema: row.detected_schema || void 0,
1017
+ middleware_names: row.middleware_names || void 0,
1018
+ last_seen: row.last_seen
1019
+ }));
1020
+ }
1021
+ async function verifyCredentials(username, apiKey) {
1022
+ try {
1023
+ const response = await axios2.post(
1024
+ getPanelUrl("VERIFY"),
1025
+ { username },
1026
+ {
1027
+ headers: {
1028
+ "Content-Type": "application/json",
1029
+ "x-gateops-username": username,
1030
+ "x-gateops-api-key": apiKey
1031
+ },
1032
+ timeout: 1e4
1033
+ }
1034
+ );
1035
+ return response.data;
1036
+ } catch (error) {
1037
+ if (axios2.isAxiosError(error)) {
1038
+ if (error.response?.status === 401) {
1039
+ return {
1040
+ success: false,
1041
+ message: "Invalid credentials"
1042
+ };
1043
+ }
1044
+ return {
1045
+ success: false,
1046
+ message: error.message
1047
+ };
1048
+ }
1049
+ return {
1050
+ success: false,
1051
+ message: "Connection failed"
1052
+ };
1053
+ }
1054
+ }
1055
+ async function sendLogs(logs, username, apiKey, retryCount = 0) {
1056
+ try {
1057
+ const response = await axios2.post(
1058
+ getPanelUrl("LOGS"),
1059
+ {
1060
+ logs: logs.map((log) => ({
1061
+ ...log,
1062
+ created_at: log.created_at instanceof Date ? log.created_at.toISOString() : log.created_at
1063
+ }))
1064
+ },
1065
+ {
1066
+ headers: {
1067
+ "Content-Type": "application/json",
1068
+ "x-gateops-username": username,
1069
+ "x-gateops-api-key": apiKey
1070
+ },
1071
+ timeout: 15e3
1072
+ }
1073
+ );
1074
+ return response.data;
1075
+ } catch (error) {
1076
+ if (retryCount < SDK_DEFAULTS.MAX_RETRY_ATTEMPTS) {
1077
+ const delay = SDK_DEFAULTS.RETRY_DELAY_BASE * Math.pow(2, retryCount);
1078
+ await sleep(delay);
1079
+ return sendLogs(logs, username, apiKey, retryCount + 1);
1080
+ }
1081
+ if (axios2.isAxiosError(error)) {
1082
+ return {
1083
+ success: false,
1084
+ received: 0,
1085
+ message: error.message
1086
+ };
1087
+ }
1088
+ return {
1089
+ success: false,
1090
+ received: 0,
1091
+ message: "Failed to send logs"
1092
+ };
1093
+ }
1094
+ }
1095
+ async function sendEndpoints(routes, username, apiKey) {
1096
+ try {
1097
+ await axios2.post(
1098
+ getPanelUrl("ENDPOINTS"),
1099
+ {
1100
+ endpoints: routes.map((r) => ({
1101
+ path: r.path,
1102
+ method: r.method,
1103
+ detected_schema: r.detected_schema,
1104
+ middleware_names: r.middleware_names,
1105
+ last_seen: r.last_seen instanceof Date ? r.last_seen.toISOString() : r.last_seen
1106
+ }))
1107
+ },
1108
+ {
1109
+ headers: {
1110
+ "Content-Type": "application/json",
1111
+ "x-gateops-username": username,
1112
+ "x-gateops-api-key": apiKey
1113
+ },
1114
+ timeout: 1e4
1115
+ }
1116
+ );
1117
+ return true;
1118
+ } catch (error) {
1119
+ console.warn(
1120
+ "[GateOps] Failed to send endpoints to Panel:",
1121
+ axios2.isAxiosError(error) ? error.message : error
1122
+ );
1123
+ return false;
1124
+ }
1125
+ }
1126
+ function sleep(ms) {
1127
+ return new Promise((resolve) => setTimeout(resolve, ms));
1128
+ }
1129
+
1130
+ // src/index.ts
1131
+ var routesScanned = false;
1132
+ function init(options = {}) {
1133
+ const middleware = middleware_default(options);
1134
+ return function gateopsInit(req, res, next) {
1135
+ if (options.exposeRoutes !== false && req.app && !req.app._gateopsRoutesRegistered) {
1136
+ req.app.use(SDK_DEFAULTS.ROUTE_PREFIX, router);
1137
+ req.app._gateopsRoutesRegistered = true;
1138
+ }
1139
+ return middleware(req, res, next);
1140
+ };
1141
+ }
1142
+ async function scan(app) {
1143
+ if (routesScanned) {
1144
+ return;
1145
+ }
1146
+ const routes = scanRoutes(app);
1147
+ if (routes.length === 0) {
1148
+ console.warn("[GateOps] No routes found. Make sure to call scan() after registering routes.");
1149
+ return;
1150
+ }
1151
+ console.log(`[GateOps] Discovered ${routes.length} routes`);
1152
+ await syncRoutesToDatabase(routes);
1153
+ const username = process.env.GATEOPS_USERNAME;
1154
+ const apiKey = process.env.GATEOPS_API_KEY;
1155
+ if (username && apiKey) {
1156
+ await sendEndpoints(routes, username, apiKey);
1157
+ }
1158
+ routesScanned = true;
1159
+ }
1160
+ async function flush() {
1161
+ await buffer.flush();
1162
+ }
1163
+ function shutdown() {
1164
+ buffer.shutdown();
1165
+ }
1166
+ var gateops = {
1167
+ init,
1168
+ scan,
1169
+ flush,
1170
+ shutdown
1171
+ };
1172
+ var index_default = gateops;
1173
+
1174
+ export { PANEL_ENDPOINTS, SDK_DEFAULTS, buffer, cleanupOldLogs, closeDatabase, index_default as default, flush, router as gateopsRouter, getDatabase, getEndpoints, getPanelUrl, init, initializeSchema, safeStringify, sanitize, sanitizeHeaders, scan, scanRoutes, sendEndpoints, sendLogs, shutdown, syncRoutesToDatabase, verifyCredentials };
1175
+ //# sourceMappingURL=index.mjs.map
1176
+ //# sourceMappingURL=index.mjs.map