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