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