@ngn-net/nestjs-telescope 0.1.12 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @ngn-net/nestjs-telescope
2
2
 
3
+ ![NestJS Telescope Banner](assets/hero.png)
4
+
3
5
  A full-featured developer assistant and application monitoring suite for NestJS, inspired by Laravel Telescope. It monitors incoming HTTP requests, database queries, cache operations, background queue jobs, application events, outgoing mail, logs, unhandled exceptions, cron job schedules, and Redis commands.
4
6
 
5
7
  All telemetry records are persisted to a database table and accessible via a modern, high-fidelity dark-themed single-page application dashboard.
@@ -93,6 +95,33 @@ import { TelescopeModule, EntryType } from '@ngn-net/nestjs-telescope';
93
95
  export class AppModule {}
94
96
  ```
95
97
 
98
+ ### Async Configuration
99
+
100
+ You can also register `TelescopeModule` asynchronously using a factory (e.g. inject `ConfigService`):
101
+
102
+ ```typescript
103
+ import { Module } from '@nestjs/common';
104
+ import { ConfigModule, ConfigService } from '@nestjs/config';
105
+ import { TelescopeModule } from '@ngn-net/nestjs-telescope';
106
+
107
+ @Module({
108
+ imports: [
109
+ ConfigModule.forRoot(),
110
+ TelescopeModule.forRootAsync({
111
+ imports: [ConfigModule],
112
+ inject: [ConfigService],
113
+ useFactory: (configService: ConfigService) => ({
114
+ path: configService.get('TELESCOPE_PATH', 'telescope'),
115
+ password: configService.get('TELESCOPE_PASSWORD', 'password'),
116
+ jwtSecret: configService.get('TELESCOPE_JWT_SECRET', 'my-secret'),
117
+ maxEntries: 1000,
118
+ }),
119
+ }),
120
+ ],
121
+ })
122
+ export class AppModule {}
123
+ ```
124
+
96
125
  ---
97
126
 
98
127
  ## Dashboard Access
@@ -0,0 +1,5 @@
1
+ import { NestMiddleware } from '@nestjs/common';
2
+ import { Request, Response, NextFunction } from 'express';
3
+ export declare class DashboardStaticMiddleware implements NestMiddleware {
4
+ use(req: Request, res: Response, next: NextFunction): void;
5
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.DashboardStaticMiddleware = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ let DashboardStaticMiddleware = class DashboardStaticMiddleware {
12
+ use(req, res, next) {
13
+ // Let Nest's ServeStatic handle static files; fallback to index.html for SPA routing
14
+ if (req.baseUrl && req.path.startsWith(req.baseUrl)) {
15
+ return next();
16
+ }
17
+ next();
18
+ }
19
+ };
20
+ exports.DashboardStaticMiddleware = DashboardStaticMiddleware;
21
+ exports.DashboardStaticMiddleware = DashboardStaticMiddleware = __decorate([
22
+ (0, common_1.Injectable)()
23
+ ], DashboardStaticMiddleware);
@@ -1,5 +1,6 @@
1
1
  export declare enum EntryType {
2
2
  REQUEST = "request",
3
+ HTTP_CLIENT = "http_client",
3
4
  QUERY = "query",
4
5
  CACHE = "cache",
5
6
  JOB = "job",
@@ -5,6 +5,7 @@ exports.EntryType = void 0;
5
5
  var EntryType;
6
6
  (function (EntryType) {
7
7
  EntryType["REQUEST"] = "request";
8
+ EntryType["HTTP_CLIENT"] = "http_client";
8
9
  EntryType["QUERY"] = "query";
9
10
  EntryType["CACHE"] = "cache";
10
11
  EntryType["JOB"] = "job";
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export * from './interfaces/telescope-options.interface';
7
7
  export * from './interfaces/entry.interface';
8
8
  export * from './guards/telescope-jwt.guard';
9
9
  export * from './constants';
10
+ export * from './dashboard.provider';
10
11
  export * from './watchers/http-request.watcher';
11
12
  export * from './watchers/query.watcher';
12
13
  export * from './watchers/cache.watcher';
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ __exportStar(require("./interfaces/telescope-options.interface"), exports);
24
24
  __exportStar(require("./interfaces/entry.interface"), exports);
25
25
  __exportStar(require("./guards/telescope-jwt.guard"), exports);
26
26
  __exportStar(require("./constants"), exports);
27
+ __exportStar(require("./dashboard.provider"), exports);
27
28
  // Export all watchers for custom usage/extension
28
29
  __exportStar(require("./watchers/http-request.watcher"), exports);
29
30
  __exportStar(require("./watchers/query.watcher"), exports);
@@ -4,6 +4,8 @@ export interface TelescopeOptions {
4
4
  path?: string;
5
5
  /** JWT secret used for UI authentication */
6
6
  jwtSecret?: string;
7
+ /** Enable the dashboard UI (default: true) */
8
+ enableDashboard?: boolean;
7
9
  /** Password for dashboard login (default: 'password', or env TELESCOPE_PASSWORD) */
8
10
  password?: string;
9
11
  /** Which entry types to enable (default: all) */
@@ -17,3 +19,13 @@ export interface TelescopeOptions {
17
19
  /** Additional middleware to apply to UI routes */
18
20
  uiMiddleware?: any[];
19
21
  }
22
+ export interface TelescopeOptionsFactory {
23
+ createTelescopeOptions(): Promise<TelescopeOptions> | TelescopeOptions;
24
+ }
25
+ export interface TelescopeModuleAsyncOptions {
26
+ imports?: any[];
27
+ useExisting?: any;
28
+ useClass?: any;
29
+ useFactory?: (...args: any[]) => Promise<TelescopeOptions> | TelescopeOptions;
30
+ inject?: any[];
31
+ }
@@ -1,10 +1,14 @@
1
1
  import { DynamicModule, OnModuleInit } from '@nestjs/common';
2
2
  import { ModuleRef } from '@nestjs/core';
3
+ import { TelescopeOptions, TelescopeModuleAsyncOptions } from './interfaces/telescope-options.interface';
3
4
  export declare class TelescopeModule implements OnModuleInit {
4
5
  private readonly moduleRef;
5
- constructor(moduleRef: ModuleRef);
6
+ private readonly options;
7
+ constructor(moduleRef: ModuleRef, options: TelescopeOptions);
6
8
  onModuleInit(): void;
7
- static forRoot(options?: any): DynamicModule;
8
- private static registerWatchers;
9
- static forRootAsync(optionsFactory: any): DynamicModule;
9
+ static forRoot(options?: TelescopeOptions): DynamicModule;
10
+ static forRootAsync(options: TelescopeModuleAsyncOptions): DynamicModule;
11
+ private static createAsyncProviders;
12
+ private static registerWatchersAndModules;
13
+ private static registerWatchersAndModulesAsync;
10
14
  }
@@ -8,10 +8,12 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
11
14
  var TelescopeModule_1;
12
15
  Object.defineProperty(exports, "__esModule", { value: true });
13
16
  exports.TelescopeModule = void 0;
14
- // src/telescope.module.ts
15
17
  const common_1 = require("@nestjs/common");
16
18
  const serve_static_1 = require("@nestjs/serve-static");
17
19
  const path_1 = require("path");
@@ -37,10 +39,37 @@ const constants_1 = require("./constants");
37
39
  const entry_type_enum_1 = require("./enums/entry-type.enum");
38
40
  let TelescopeModule = TelescopeModule_1 = class TelescopeModule {
39
41
  moduleRef;
40
- constructor(moduleRef) {
42
+ options;
43
+ constructor(moduleRef, options) {
41
44
  this.moduleRef = moduleRef;
45
+ this.options = options;
46
+ const path = this.options.path || constants_1.DEFAULT_TELESCOPE_PATH;
47
+ Reflect.defineMetadata('path', path, telescope_controller_1.TelescopeController);
42
48
  }
43
49
  onModuleInit() {
50
+ // Dynamic peer dependency validation at bootstrap
51
+ const warnedTypes = new Set();
52
+ const hasDependency = (type, packageName) => {
53
+ try {
54
+ require(packageName);
55
+ return true;
56
+ }
57
+ catch (e) {
58
+ if (this.options.enabledEntryTypes && this.options.enabledEntryTypes.includes(type)) {
59
+ if (!warnedTypes.has(type)) {
60
+ warnedTypes.add(type);
61
+ common_1.Logger.warn(`EntryType '${type}' was explicitly enabled in config, but the required peer dependency '${packageName}' is not installed. The '${type}' watcher has been disabled.`, 'TelescopeModule');
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+ };
67
+ hasDependency(entry_type_enum_1.EntryType.CACHE, '@nestjs/cache-manager');
68
+ hasDependency(entry_type_enum_1.EntryType.JOB, '@nestjs/bullmq');
69
+ hasDependency(entry_type_enum_1.EntryType.JOB, 'bullmq');
70
+ hasDependency(entry_type_enum_1.EntryType.EVENT, '@nestjs/event-emitter');
71
+ hasDependency(entry_type_enum_1.EntryType.SCHEDULED_TASK, '@nestjs/schedule');
72
+ hasDependency(entry_type_enum_1.EntryType.MAIL, 'nodemailer');
44
73
  // Dynamic EventEmitter2 configuration
45
74
  try {
46
75
  const eventWatcher = this.moduleRef.get(event_watcher_1.EventWatcher, { strict: false });
@@ -81,7 +110,7 @@ let TelescopeModule = TelescopeModule_1 = class TelescopeModule {
81
110
  // Serve static UI assets if dashboard is enabled (default true)
82
111
  if (options.enableDashboard !== false) {
83
112
  const staticPath = (0, path_1.join)(__dirname, '..', 'ui');
84
- const routePath = options.path || 'telescope';
113
+ const routePath = options.path || constants_1.DEFAULT_TELESCOPE_PATH;
85
114
  imports.push(serve_static_1.ServeStaticModule.forRoot({
86
115
  rootPath: staticPath,
87
116
  serveRoot: `/${routePath}`,
@@ -96,145 +125,209 @@ let TelescopeModule = TelescopeModule_1 = class TelescopeModule {
96
125
  telescope_repository_service_1.TelescopeRepository,
97
126
  telescope_jwt_guard_1.JwtAuthGuard,
98
127
  ];
99
- // Always registered watchers (no external peer dependencies needed)
100
- const watchersToRegister = [
128
+ // Register watchers (both core and optional)
129
+ this.registerWatchersAndModules(options, providers, imports);
130
+ return {
131
+ module: TelescopeModule_1,
132
+ imports,
133
+ providers,
134
+ controllers: [telescope_controller_1.TelescopeController],
135
+ exports: [telescope_service_1.TelescopeService, telescope_repository_service_1.TelescopeRepository],
136
+ };
137
+ }
138
+ static forRootAsync(options) {
139
+ const providers = [
140
+ ...this.createAsyncProviders(options),
141
+ telescope_service_1.TelescopeService,
142
+ telescope_repository_service_1.TelescopeRepository,
143
+ telescope_jwt_guard_1.JwtAuthGuard,
144
+ ];
145
+ const imports = [
146
+ typeorm_1.TypeOrmModule.forFeature([telescope_entry_entity_1.TelescopeEntry]),
147
+ jwt_1.JwtModule.registerAsync({
148
+ imports: options.imports || [],
149
+ inject: [constants_1.TELESCOPE_OPTIONS],
150
+ useFactory: async (telescopeOpts) => ({
151
+ secret: telescopeOpts.jwtSecret || process.env.TELESCOPE_JWT_SECRET || constants_1.DEFAULT_JWT_SECRET,
152
+ signOptions: { expiresIn: '1d' },
153
+ }),
154
+ }),
155
+ ];
156
+ // Serve static UI assets if dashboard is enabled (default true)
157
+ imports.push(serve_static_1.ServeStaticModule.forRootAsync({
158
+ imports: options.imports || [],
159
+ inject: [constants_1.TELESCOPE_OPTIONS],
160
+ useFactory: (telescopeOpts) => {
161
+ if (telescopeOpts.enableDashboard === false) {
162
+ return [];
163
+ }
164
+ const staticPath = (0, path_1.join)(__dirname, '..', 'ui');
165
+ const routePath = telescopeOpts.path || constants_1.DEFAULT_TELESCOPE_PATH;
166
+ return [
167
+ {
168
+ rootPath: staticPath,
169
+ serveRoot: `/${routePath}`,
170
+ },
171
+ ];
172
+ },
173
+ }));
174
+ // Register all available/supported watchers & modules dynamically
175
+ this.registerWatchersAndModulesAsync(providers, imports);
176
+ return {
177
+ module: TelescopeModule_1,
178
+ imports,
179
+ providers,
180
+ controllers: [telescope_controller_1.TelescopeController],
181
+ exports: [telescope_service_1.TelescopeService, telescope_repository_service_1.TelescopeRepository],
182
+ };
183
+ }
184
+ static createAsyncProviders(options) {
185
+ if (options.useFactory) {
186
+ return [
187
+ {
188
+ provide: constants_1.TELESCOPE_OPTIONS,
189
+ useFactory: options.useFactory,
190
+ inject: options.inject || [],
191
+ },
192
+ ];
193
+ }
194
+ const useClass = options.useClass || options.useExisting;
195
+ if (useClass) {
196
+ return [
197
+ {
198
+ provide: constants_1.TELESCOPE_OPTIONS,
199
+ useFactory: async (optionsFactory) => await optionsFactory.createTelescopeOptions(),
200
+ inject: [useClass],
201
+ },
202
+ {
203
+ provide: useClass,
204
+ useClass: options.useClass,
205
+ },
206
+ ];
207
+ }
208
+ return [];
209
+ }
210
+ static registerWatchersAndModules(options, providers, imports) {
211
+ // 1. Core watchers (always registered if enabled or not specified)
212
+ const coreWatchers = [
101
213
  { type: entry_type_enum_1.EntryType.REQUEST, watcher: http_request_watcher_1.HttpRequestWatcher, isInterceptor: true },
102
214
  { type: entry_type_enum_1.EntryType.QUERY, watcher: query_watcher_1.QueryWatcher },
103
215
  { type: entry_type_enum_1.EntryType.LOG, watcher: log_watcher_1.LogWatcher },
104
216
  { type: entry_type_enum_1.EntryType.EXCEPTION, watcher: exception_watcher_1.ExceptionWatcher, isInterceptor: true },
105
217
  { type: entry_type_enum_1.EntryType.REDIS, watcher: redis_watcher_1.RedisWatcher },
106
218
  ];
107
- // Register core watchers
108
- this.registerWatchers(options, providers);
109
- // Optional dependencies checking:
110
- // 1. CacheWatcher (@nestjs/cache-manager)
111
- let hasCache = false;
219
+ for (const item of coreWatchers) {
220
+ if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(item.type)) {
221
+ if (item.isInterceptor) {
222
+ providers.push({ provide: core_1.APP_INTERCEPTOR, useClass: item.watcher });
223
+ }
224
+ else {
225
+ providers.push(item.watcher);
226
+ }
227
+ }
228
+ }
229
+ // 2. CacheWatcher
112
230
  try {
113
231
  require('@nestjs/cache-manager');
114
- hasCache = true;
115
- }
116
- catch { }
117
- if (hasCache && (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.CACHE))) {
118
- try {
232
+ if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.CACHE)) {
119
233
  const { CacheModule } = require('@nestjs/cache-manager');
120
- if (CacheModule) {
121
- imports.push(CacheModule.register({}));
122
- providers.push(cache_watcher_1.CacheWatcher);
123
- }
234
+ imports.push(CacheModule.register({}));
235
+ providers.push(cache_watcher_1.CacheWatcher);
124
236
  }
125
- catch { }
126
237
  }
127
- // 2. QueueWatcher (@nestjs/bullmq & bullmq)
128
- let hasBull = false;
238
+ catch (e) { }
239
+ // 3. QueueWatcher
129
240
  try {
130
241
  require('@nestjs/bullmq');
131
242
  require('bullmq');
132
- hasBull = true;
133
- }
134
- catch { }
135
- if (hasBull && (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.JOB))) {
136
- try {
243
+ if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.JOB)) {
137
244
  const { BullModule } = require('@nestjs/bullmq');
138
- if (BullModule) {
139
- imports.push(BullModule.registerQueue({
140
- name: 'telescope-queue',
141
- connection: {
142
- host: process.env.REDIS_HOST || '127.0.0.1',
143
- port: Number(process.env.REDIS_PORT) || 6379,
144
- },
145
- }));
146
- providers.push(queue_watcher_1.QueueWatcher);
147
- }
245
+ imports.push(BullModule.registerQueue({
246
+ name: 'telescope-queue',
247
+ connection: {
248
+ host: process.env.REDIS_HOST || '127.0.0.1',
249
+ port: Number(process.env.REDIS_PORT) || 6379,
250
+ },
251
+ }));
252
+ providers.push(queue_watcher_1.QueueWatcher);
148
253
  }
149
- catch { }
150
254
  }
151
- // 3. EventWatcher (@nestjs/event-emitter)
152
- let hasEvents = false;
255
+ catch (e) { }
256
+ // 4. EventWatcher
153
257
  try {
154
258
  require('@nestjs/event-emitter');
155
- hasEvents = true;
259
+ if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.EVENT)) {
260
+ providers.push(event_watcher_1.EventWatcher);
261
+ }
262
+ }
263
+ catch (e) { }
264
+ // 5. ScheduleWatcher
265
+ try {
266
+ require('@nestjs/schedule');
267
+ if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.SCHEDULED_TASK)) {
268
+ providers.push(schedule_watcher_1.ScheduleWatcher);
269
+ }
270
+ }
271
+ catch (e) { }
272
+ // 6. MailWatcher
273
+ try {
274
+ require('nodemailer');
275
+ if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.MAIL)) {
276
+ providers.push(mail_watcher_1.MailWatcher);
277
+ }
156
278
  }
157
- catch { }
158
- if (hasEvents && (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.EVENT))) {
279
+ catch (e) { }
280
+ }
281
+ static registerWatchersAndModulesAsync(providers, imports) {
282
+ // Core interceptors and watchers
283
+ providers.push({ provide: core_1.APP_INTERCEPTOR, useClass: http_request_watcher_1.HttpRequestWatcher });
284
+ providers.push({ provide: core_1.APP_INTERCEPTOR, useClass: exception_watcher_1.ExceptionWatcher });
285
+ providers.push(query_watcher_1.QueryWatcher);
286
+ providers.push(log_watcher_1.LogWatcher);
287
+ providers.push(redis_watcher_1.RedisWatcher);
288
+ // Optional modules/watchers if packages are present
289
+ try {
290
+ require('@nestjs/cache-manager');
291
+ const { CacheModule } = require('@nestjs/cache-manager');
292
+ imports.push(CacheModule.register({}));
293
+ providers.push(cache_watcher_1.CacheWatcher);
294
+ }
295
+ catch (e) { }
296
+ try {
297
+ require('@nestjs/bullmq');
298
+ require('bullmq');
299
+ const { BullModule } = require('@nestjs/bullmq');
300
+ imports.push(BullModule.registerQueue({
301
+ name: 'telescope-queue',
302
+ connection: {
303
+ host: process.env.REDIS_HOST || '127.0.0.1',
304
+ port: Number(process.env.REDIS_PORT) || 6379,
305
+ },
306
+ }));
307
+ providers.push(queue_watcher_1.QueueWatcher);
308
+ }
309
+ catch (e) { }
310
+ try {
311
+ require('@nestjs/event-emitter');
159
312
  providers.push(event_watcher_1.EventWatcher);
160
313
  }
161
- // 4. ScheduleWatcher (@nestjs/schedule)
162
- let hasSchedule = false;
314
+ catch (e) { }
163
315
  try {
164
316
  require('@nestjs/schedule');
165
- hasSchedule = true;
166
- }
167
- catch { }
168
- if (hasSchedule && (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.SCHEDULED_TASK))) {
169
317
  providers.push(schedule_watcher_1.ScheduleWatcher);
170
318
  }
171
- // 5. MailWatcher (nodemailer)
172
- let hasNodemailer = false;
319
+ catch (e) { }
173
320
  try {
174
321
  require('nodemailer');
175
- hasNodemailer = true;
176
- }
177
- catch { }
178
- if (hasNodemailer && (!options.enabledEntryTypes || options.enabledEntryTypes.includes(entry_type_enum_1.EntryType.MAIL))) {
179
322
  providers.push(mail_watcher_1.MailWatcher);
180
323
  }
181
- // Dynamic Route Prefix Setup using Reflect metadata on TelescopeController
182
- const path = options.path || 'telescope';
183
- Reflect.defineMetadata('path', path, telescope_controller_1.TelescopeController);
184
- // Ensure static assets are served under the same path when dashboard enabled
185
- if (options.enableDashboard !== false) {
186
- // ServeStaticModule already handles static files; no extra code needed here.
187
- }
188
- return {
189
- module: TelescopeModule_1,
190
- imports,
191
- providers,
192
- controllers: [telescope_controller_1.TelescopeController],
193
- exports: [telescope_service_1.TelescopeService, telescope_repository_service_1.TelescopeRepository],
194
- };
195
- }
196
- // Helper to register core watchers
197
- static registerWatchers(options, providers) {
198
- const watchersToRegister = [
199
- { type: entry_type_enum_1.EntryType.REQUEST, watcher: http_request_watcher_1.HttpRequestWatcher, isInterceptor: true },
200
- { type: entry_type_enum_1.EntryType.QUERY, watcher: query_watcher_1.QueryWatcher },
201
- { type: entry_type_enum_1.EntryType.LOG, watcher: log_watcher_1.LogWatcher },
202
- { type: entry_type_enum_1.EntryType.EXCEPTION, watcher: exception_watcher_1.ExceptionWatcher, isInterceptor: true },
203
- { type: entry_type_enum_1.EntryType.REDIS, watcher: redis_watcher_1.RedisWatcher },
204
- ];
205
- for (const item of watchersToRegister) {
206
- if (!options.enabledEntryTypes || options.enabledEntryTypes.includes(item.type)) {
207
- if (item.isInterceptor) {
208
- providers.push({ provide: core_1.APP_INTERCEPTOR, useClass: item.watcher });
209
- }
210
- else {
211
- providers.push(item.watcher);
212
- }
213
- }
214
- }
215
- }
216
- static forRootAsync(optionsFactory) {
217
- // Allows async injection of TelescopeOptions (e.g., from ConfigService)
218
- return {
219
- module: TelescopeModule_1,
220
- imports: [
221
- typeorm_1.TypeOrmModule.forFeature([telescope_entry_entity_1.TelescopeEntry]),
222
- jwt_1.JwtModule.registerAsync({
223
- useFactory: async (options) => ({
224
- secret: options.jwtSecret || process.env.TELESCOPE_JWT_SECRET || constants_1.DEFAULT_JWT_SECRET,
225
- signOptions: { expiresIn: '1d' },
226
- }),
227
- inject: [optionsFactory],
228
- }),
229
- ],
230
- providers: [],
231
- controllers: [telescope_controller_1.TelescopeController],
232
- };
324
+ catch (e) { }
233
325
  }
234
326
  };
235
327
  exports.TelescopeModule = TelescopeModule;
236
328
  exports.TelescopeModule = TelescopeModule = TelescopeModule_1 = __decorate([
237
329
  (0, common_1.Global)(),
238
330
  (0, common_1.Module)({}),
239
- __metadata("design:paramtypes", [core_1.ModuleRef])
331
+ __param(1, (0, common_1.Inject)(constants_1.TELESCOPE_OPTIONS)),
332
+ __metadata("design:paramtypes", [core_1.ModuleRef, Object])
240
333
  ], TelescopeModule);
@@ -1,4 +1,5 @@
1
1
  import { OnModuleInit } from '@nestjs/common';
2
+ import { HttpAdapterHost } from '@nestjs/core';
2
3
  import { TelescopeRepository } from './storage/telescope-repository.service';
3
4
  import { TelescopeEntry } from './storage/entities/telescope-entry.entity';
4
5
  import { EntryType } from './enums/entry-type.enum';
@@ -6,10 +7,15 @@ import { TelescopeOptions } from './interfaces/telescope-options.interface';
6
7
  export declare class TelescopeService implements OnModuleInit {
7
8
  private readonly repo;
8
9
  private readonly options?;
10
+ private readonly adapterHost?;
9
11
  private lastPruneTime;
10
12
  private readonly asyncLocalStorage;
11
- constructor(repo: TelescopeRepository, options?: TelescopeOptions | undefined);
13
+ constructor(repo: TelescopeRepository, options?: TelescopeOptions | undefined, adapterHost?: HttpAdapterHost | undefined);
12
14
  onModuleInit(): Promise<void>;
15
+ /**
16
+ * Get the full URL to the Telescope dashboard.
17
+ */
18
+ getDashboardUrl(): string;
13
19
  /**
14
20
  * Check if a specific entry type is enabled in the options.
15
21
  * If `enabledEntryTypes` is not configured, all types are enabled.
@@ -16,20 +16,52 @@ exports.TelescopeService = void 0;
16
16
  // src/telescope.service.ts
17
17
  const common_1 = require("@nestjs/common");
18
18
  const async_hooks_1 = require("async_hooks");
19
+ const core_1 = require("@nestjs/core");
19
20
  const telescope_repository_service_1 = require("./storage/telescope-repository.service");
20
21
  const constants_1 = require("./constants");
21
22
  let TelescopeService = class TelescopeService {
22
23
  repo;
23
24
  options;
25
+ adapterHost;
24
26
  lastPruneTime = 0;
25
27
  asyncLocalStorage = new async_hooks_1.AsyncLocalStorage();
26
- constructor(repo, options) {
28
+ constructor(repo, options, adapterHost) {
27
29
  this.repo = repo;
28
30
  this.options = options;
31
+ this.adapterHost = adapterHost;
29
32
  }
30
33
  async onModuleInit() {
31
34
  // Module is initialized; TypeORM will handle schema sync
32
35
  }
36
+ /**
37
+ * Get the full URL to the Telescope dashboard.
38
+ */
39
+ getDashboardUrl() {
40
+ const path = this.options?.path || 'telescope';
41
+ if (!this.adapterHost) {
42
+ return `/${path}`;
43
+ }
44
+ try {
45
+ const httpAdapter = this.adapterHost.httpAdapter;
46
+ if (!httpAdapter) {
47
+ return `/${path}`;
48
+ }
49
+ const instance = httpAdapter.getInstance();
50
+ if (instance && typeof instance.address === 'function') {
51
+ const address = instance.address();
52
+ if (address) {
53
+ const host = typeof address === 'string' ? address : address.address === '::' ? 'localhost' : address.address;
54
+ const port = typeof address === 'string' ? '' : `:${address.port}`;
55
+ const protocol = 'http';
56
+ return `${protocol}://${host}${port}/${path}`;
57
+ }
58
+ }
59
+ }
60
+ catch (e) {
61
+ // Fallback
62
+ }
63
+ return `/${path}`;
64
+ }
33
65
  /**
34
66
  * Check if a specific entry type is enabled in the options.
35
67
  * If `enabledEntryTypes` is not configured, all types are enabled.
@@ -110,5 +142,6 @@ exports.TelescopeService = TelescopeService = __decorate([
110
142
  (0, common_1.Injectable)(),
111
143
  __param(1, (0, common_1.Inject)(constants_1.TELESCOPE_OPTIONS)),
112
144
  __param(1, (0, common_1.Optional)()),
113
- __metadata("design:paramtypes", [telescope_repository_service_1.TelescopeRepository, Object])
145
+ __param(2, (0, common_1.Optional)()),
146
+ __metadata("design:paramtypes", [telescope_repository_service_1.TelescopeRepository, Object, core_1.HttpAdapterHost])
114
147
  ], TelescopeService);
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@ngn-net/nestjs-telescope",
3
+ "version": "0.2.1",
4
+ "builtAt": "2026-06-08T11:03:02.519Z"
5
+ }
@@ -0,0 +1,11 @@
1
+ import { OnModuleInit } from '@nestjs/common';
2
+ import { TelescopeService } from '../telescope.service';
3
+ import { TelescopeOptions } from '../interfaces/telescope-options.interface';
4
+ export declare class HttpClientWatcher implements OnModuleInit {
5
+ private readonly telescope;
6
+ private readonly options?;
7
+ private patched;
8
+ constructor(telescope: TelescopeService, options?: TelescopeOptions | undefined);
9
+ onModuleInit(): void;
10
+ private patch;
11
+ }
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __metadata = (this && this.__metadata) || function (k, v) {
42
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
43
+ };
44
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
45
+ return function (target, key) { decorator(target, key, paramIndex); }
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.HttpClientWatcher = void 0;
49
+ // src/watchers/http-client.watcher.ts
50
+ const common_1 = require("@nestjs/common");
51
+ const http = __importStar(require("http"));
52
+ const https = __importStar(require("https"));
53
+ const telescope_service_1 = require("../telescope.service");
54
+ const entry_type_enum_1 = require("../enums/entry-type.enum");
55
+ const constants_1 = require("../constants");
56
+ let HttpClientWatcher = class HttpClientWatcher {
57
+ telescope;
58
+ options;
59
+ patched = false;
60
+ constructor(telescope, options) {
61
+ this.telescope = telescope;
62
+ this.options = options;
63
+ }
64
+ onModuleInit() {
65
+ if (this.patched)
66
+ return;
67
+ this.patched = true;
68
+ this.patch(http);
69
+ this.patch(https);
70
+ }
71
+ patch(module) {
72
+ const originalRequest = module.request.bind(module);
73
+ const self = this;
74
+ // Override module.request
75
+ module.request = function (...args) {
76
+ const req = originalRequest(...args);
77
+ const startTime = Date.now();
78
+ // Resolve URL and method
79
+ let urlStr = '';
80
+ let method = 'GET';
81
+ try {
82
+ const firstArg = args[0];
83
+ if (typeof firstArg === 'string') {
84
+ urlStr = firstArg;
85
+ }
86
+ else if (firstArg instanceof URL) {
87
+ urlStr = firstArg.toString();
88
+ }
89
+ else if (typeof firstArg === 'object' && firstArg !== null) {
90
+ const { protocol, hostname, host, port, path: urlPath } = firstArg;
91
+ const proto = protocol || 'http:';
92
+ const h = hostname || host || 'localhost';
93
+ const p = port ? `:${port}` : '';
94
+ urlStr = `${proto}//${h}${p}${urlPath || '/'}`;
95
+ method = (firstArg.method || 'GET').toUpperCase();
96
+ }
97
+ }
98
+ catch {
99
+ urlStr = '';
100
+ }
101
+ // Ignore telescope's own internal paths and configured ignore list
102
+ if (self.telescope.shouldIgnorePath(urlStr)) {
103
+ return req;
104
+ }
105
+ // Skip if this type is disabled
106
+ if (!self.telescope.isEnabled(entry_type_enum_1.EntryType.HTTP_CLIENT)) {
107
+ return req;
108
+ }
109
+ // Capture request body chunks written
110
+ const reqChunks = [];
111
+ const originalWrite = req.write.bind(req);
112
+ const originalEnd = req.end.bind(req);
113
+ req.write = function (chunk, ...rest) {
114
+ if (chunk)
115
+ reqChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
116
+ return originalWrite(chunk, ...rest);
117
+ };
118
+ req.end = function (chunk, ...rest) {
119
+ if (chunk)
120
+ reqChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
121
+ return originalEnd(chunk, ...rest);
122
+ };
123
+ // Capture request headers snapshot after end
124
+ req.on('finish', () => {
125
+ // headers captured at response time
126
+ });
127
+ req.on('response', (res) => {
128
+ const resChunks = [];
129
+ res.on('data', (chunk) => {
130
+ resChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
131
+ });
132
+ res.on('end', () => {
133
+ const duration = Date.now() - startTime;
134
+ const requestBodyRaw = Buffer.concat(reqChunks).toString('utf8');
135
+ const responseBodyRaw = Buffer.concat(resChunks).toString('utf8');
136
+ let requestBody = requestBodyRaw;
137
+ let responseBody = responseBodyRaw;
138
+ try {
139
+ requestBody = JSON.parse(requestBodyRaw);
140
+ }
141
+ catch { /* keep raw */ }
142
+ try {
143
+ responseBody = JSON.parse(responseBodyRaw);
144
+ }
145
+ catch { /* keep raw */ }
146
+ // Sanitize headers — remove Authorization tokens from logs
147
+ const reqHeaders = { ...req.getHeaders?.() };
148
+ if (reqHeaders['authorization']) {
149
+ reqHeaders['authorization'] = '[REDACTED]';
150
+ }
151
+ self.telescope.record({
152
+ type: entry_type_enum_1.EntryType.HTTP_CLIENT,
153
+ content: {
154
+ method: method || (req.method || 'GET').toUpperCase(),
155
+ url: urlStr,
156
+ requestHeaders: reqHeaders,
157
+ requestBody,
158
+ responseStatus: res.statusCode,
159
+ responseHeaders: res.headers,
160
+ responseBody,
161
+ duration,
162
+ },
163
+ }).catch(() => { });
164
+ });
165
+ res.on('error', () => { });
166
+ });
167
+ req.on('error', (err) => {
168
+ const duration = Date.now() - startTime;
169
+ self.telescope.record({
170
+ type: entry_type_enum_1.EntryType.HTTP_CLIENT,
171
+ content: {
172
+ method,
173
+ url: urlStr,
174
+ requestHeaders: {},
175
+ requestBody: null,
176
+ responseStatus: 0,
177
+ responseBody: null,
178
+ error: err.message,
179
+ duration,
180
+ },
181
+ }).catch(() => { });
182
+ });
183
+ return req;
184
+ };
185
+ // Also handle module.get (convenience method)
186
+ const originalGet = module.get.bind(module);
187
+ module.get = function (...args) {
188
+ const req = module.request(...args);
189
+ req.end();
190
+ return req;
191
+ };
192
+ }
193
+ };
194
+ exports.HttpClientWatcher = HttpClientWatcher;
195
+ exports.HttpClientWatcher = HttpClientWatcher = __decorate([
196
+ (0, common_1.Injectable)(),
197
+ __param(1, (0, common_1.Inject)(constants_1.TELESCOPE_OPTIONS)),
198
+ __param(1, (0, common_1.Optional)()),
199
+ __metadata("design:paramtypes", [telescope_service_1.TelescopeService, Object])
200
+ ], HttpClientWatcher);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngn-net/nestjs-telescope",
3
- "version": "0.1.12",
3
+ "version": "0.2.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -73,16 +73,21 @@
73
73
  "@nestjs/event-emitter": "^2",
74
74
  "@nestjs/jwt": "^11",
75
75
  "@nestjs/passport": "^11",
76
+ "@nestjs/platform-express": "^11.1.25",
76
77
  "@nestjs/schedule": "^4",
78
+ "@nestjs/testing": "^11.1.25",
77
79
  "@nestjs/typeorm": "^11",
78
80
  "@types/express": "^4.17.21",
79
81
  "@types/jest": "^29",
80
82
  "@types/node": "^20",
81
83
  "@types/nodemailer": "^6.4.15",
84
+ "@types/supertest": "^7.2.0",
82
85
  "jest": "^29",
83
86
  "nodemailer": "^6",
84
87
  "rimraf": "^5",
85
88
  "rxjs": "^7",
89
+ "sqlite3": "^6.0.1",
90
+ "supertest": "^7.2.2",
86
91
  "ts-jest": "^29",
87
92
  "ts-node": "^10",
88
93
  "typeorm": "^0.3",