@lenne.tech/nest-server 11.31.2 → 11.32.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/.claude/rules/architecture.md +2 -0
- package/.claude/rules/configurable-features.md +2 -0
- package/.claude/rules/versioning.md +5 -8
- package/CLAUDE.md +31 -4
- package/FRAMEWORK-API.md +7 -2
- package/README.md +1 -0
- package/dist/config.env.d.ts +4 -0
- package/dist/config.env.js +32 -2
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/logging.helper.d.ts +1 -0
- package/dist/core/common/helpers/logging.helper.js +12 -0
- package/dist/core/common/helpers/logging.helper.js.map +1 -1
- package/dist/core/common/helpers/meta.helper.d.ts +1 -0
- package/dist/core/common/helpers/meta.helper.js +32 -1
- package/dist/core/common/helpers/meta.helper.js.map +1 -1
- package/dist/core/common/helpers/process-diagnostics.helper.d.ts +18 -0
- package/dist/core/common/helpers/process-diagnostics.helper.js +88 -0
- package/dist/core/common/helpers/process-diagnostics.helper.js.map +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +12 -0
- package/dist/core/common/services/brevo.service.d.ts +7 -1
- package/dist/core/common/services/brevo.service.js +37 -16
- package/dist/core/common/services/brevo.service.js.map +1 -1
- package/dist/core/common/services/email.service.d.ts +3 -1
- package/dist/core/common/services/email.service.js +33 -2
- package/dist/core/common/services/email.service.js.map +1 -1
- package/dist/core/common/services/template.service.js +9 -4
- package/dist/core/common/services/template.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +9 -4
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
- package/dist/core/modules/hub/core-hub-actions.controller.d.ts +22 -0
- package/dist/core/modules/hub/core-hub-actions.controller.js +141 -0
- package/dist/core/modules/hub/core-hub-actions.controller.js.map +1 -0
- package/dist/core/modules/hub/core-hub-html.service.d.ts +8 -0
- package/dist/core/modules/hub/core-hub-html.service.js +123 -0
- package/dist/core/modules/hub/core-hub-html.service.js.map +1 -0
- package/dist/core/modules/hub/core-hub.controller.d.ts +56 -0
- package/dist/core/modules/hub/core-hub.controller.js +398 -0
- package/dist/core/modules/hub/core-hub.controller.js.map +1 -0
- package/dist/core/modules/hub/core-hub.module.d.ts +17 -0
- package/dist/core/modules/hub/core-hub.module.js +109 -0
- package/dist/core/modules/hub/core-hub.module.js.map +1 -0
- package/dist/core/modules/hub/core-hub.service.d.ts +18 -0
- package/dist/core/modules/hub/core-hub.service.js +153 -0
- package/dist/core/modules/hub/core-hub.service.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-client-js.helper.d.ts +1 -0
- package/dist/core/modules/hub/helpers/hub-client-js.helper.js +755 -0
- package/dist/core/modules/hub/helpers/hub-client-js.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-command-shape.helper.d.ts +1 -0
- package/dist/core/modules/hub/helpers/hub-command-shape.helper.js +33 -0
- package/dist/core/modules/hub/helpers/hub-command-shape.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-mask.helper.d.ts +1 -0
- package/dist/core/modules/hub/helpers/hub-mask.helper.js +46 -0
- package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-mermaid.helper.d.ts +10 -0
- package/dist/core/modules/hub/helpers/hub-mermaid.helper.js +35 -0
- package/dist/core/modules/hub/helpers/hub-mermaid.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-shell.helper.d.ts +9 -0
- package/dist/core/modules/hub/helpers/hub-shell.helper.js +62 -0
- package/dist/core/modules/hub/helpers/hub-shell.helper.js.map +1 -0
- package/dist/core/modules/hub/hub-action-messages.d.ts +17 -0
- package/dist/core/modules/hub/hub-action-messages.js +21 -0
- package/dist/core/modules/hub/hub-action-messages.js.map +1 -0
- package/dist/core/modules/hub/hub-config.helper.d.ts +11 -0
- package/dist/core/modules/hub/hub-config.helper.js +164 -0
- package/dist/core/modules/hub/hub-config.helper.js.map +1 -0
- package/dist/core/modules/hub/hub-nav.d.ts +12 -0
- package/dist/core/modules/hub/hub-nav.js +43 -0
- package/dist/core/modules/hub/hub-nav.js.map +1 -0
- package/dist/core/modules/hub/hub-ring-buffer.d.ts +22 -0
- package/dist/core/modules/hub/hub-ring-buffer.js +59 -0
- package/dist/core/modules/hub/hub-ring-buffer.js.map +1 -0
- package/dist/core/modules/hub/hub.constants.d.ts +18 -0
- package/dist/core/modules/hub/hub.constants.js +22 -0
- package/dist/core/modules/hub/hub.constants.js.map +1 -0
- package/dist/core/modules/hub/index.d.ts +25 -0
- package/dist/core/modules/hub/index.js +42 -0
- package/dist/core/modules/hub/index.js.map +1 -0
- package/dist/core/modules/hub/interfaces/hub-config.interface.d.ts +115 -0
- package/dist/core/modules/hub/interfaces/hub-config.interface.js +3 -0
- package/dist/core/modules/hub/interfaces/hub-config.interface.js.map +1 -0
- package/dist/core/modules/hub/interfaces/hub-panels.interface.d.ts +231 -0
- package/dist/core/modules/hub/interfaces/hub-panels.interface.js +3 -0
- package/dist/core/modules/hub/interfaces/hub-panels.interface.js.map +1 -0
- package/dist/core/modules/hub/middleware/hub-trace.middleware.d.ts +8 -0
- package/dist/core/modules/hub/middleware/hub-trace.middleware.js +50 -0
- package/dist/core/modules/hub/middleware/hub-trace.middleware.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.d.ts +48 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.js +136 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-db.service.d.ts +19 -0
- package/dist/core/modules/hub/services/core-hub-db.service.js +180 -0
- package/dist/core/modules/hub/services/core-hub-db.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-email.service.d.ts +24 -0
- package/dist/core/modules/hub/services/core-hub-email.service.js +148 -0
- package/dist/core/modules/hub/services/core-hub-email.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +20 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js +103 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-migrations.service.d.ts +25 -0
- package/dist/core/modules/hub/services/core-hub-migrations.service.js +115 -0
- package/dist/core/modules/hub/services/core-hub-migrations.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-sources.service.d.ts +28 -0
- package/dist/core/modules/hub/services/core-hub-sources.service.js +187 -0
- package/dist/core/modules/hub/services/core-hub-sources.service.js.map +1 -0
- package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +24 -0
- package/dist/core/modules/hub/services/hub-log-buffer.service.js +210 -0
- package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -0
- package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +38 -0
- package/dist/core/modules/hub/services/hub-query-profiler.service.js +235 -0
- package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -0
- package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +18 -0
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js +123 -0
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -0
- package/dist/core.module.js +42 -1
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/main.js +5 -2
- package/dist/main.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +2 -0
- package/docs/brevo-manual-test.md +166 -0
- package/docs/security-overrides.md +90 -0
- package/migration-guides/11.31.2-to-11.31.3.md +135 -0
- package/migration-guides/11.31.3-to-11.32.0.md +254 -0
- package/package.json +16 -14
- package/src/config.env.ts +75 -2
- package/src/core/common/helpers/logging.helper.spec.ts +61 -0
- package/src/core/common/helpers/logging.helper.ts +48 -0
- package/src/core/common/helpers/meta.helper.ts +46 -1
- package/src/core/common/helpers/process-diagnostics.helper.spec.ts +310 -0
- package/src/core/common/helpers/process-diagnostics.helper.ts +321 -0
- package/src/core/common/interfaces/server-options.interface.ts +78 -0
- package/src/core/common/services/brevo.service.spec.ts +266 -0
- package/src/core/common/services/brevo.service.ts +100 -17
- package/src/core/common/services/email.service.ts +33 -1
- package/src/core/common/services/template.service.ts +21 -16
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +14 -4
- package/src/core/modules/hub/INTEGRATION-CHECKLIST.md +64 -0
- package/src/core/modules/hub/README.md +159 -0
- package/src/core/modules/hub/core-hub-actions.controller.ts +137 -0
- package/src/core/modules/hub/core-hub-html.service.ts +135 -0
- package/src/core/modules/hub/core-hub.controller.ts +286 -0
- package/src/core/modules/hub/core-hub.module.spec.ts +108 -0
- package/src/core/modules/hub/core-hub.module.ts +159 -0
- package/src/core/modules/hub/core-hub.service.ts +169 -0
- package/src/core/modules/hub/helpers/hub-client-js.helper.ts +768 -0
- package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +48 -0
- package/src/core/modules/hub/helpers/hub-command-shape.helper.ts +47 -0
- package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +67 -0
- package/src/core/modules/hub/helpers/hub-mask.helper.ts +78 -0
- package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +54 -0
- package/src/core/modules/hub/helpers/hub-mermaid.helper.ts +62 -0
- package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +106 -0
- package/src/core/modules/hub/helpers/hub-shell.helper.ts +90 -0
- package/src/core/modules/hub/hub-action-messages.ts +47 -0
- package/src/core/modules/hub/hub-config.helper.spec.ts +108 -0
- package/src/core/modules/hub/hub-config.helper.ts +233 -0
- package/src/core/modules/hub/hub-nav.ts +66 -0
- package/src/core/modules/hub/hub-ring-buffer.spec.ts +95 -0
- package/src/core/modules/hub/hub-ring-buffer.ts +101 -0
- package/src/core/modules/hub/hub.constants.ts +84 -0
- package/src/core/modules/hub/index.ts +25 -0
- package/src/core/modules/hub/interfaces/hub-config.interface.ts +265 -0
- package/src/core/modules/hub/interfaces/hub-panels.interface.ts +186 -0
- package/src/core/modules/hub/middleware/hub-trace.middleware.ts +45 -0
- package/src/core/modules/hub/services/core-hub-actions.service.ts +133 -0
- package/src/core/modules/hub/services/core-hub-db.service.ts +185 -0
- package/src/core/modules/hub/services/core-hub-email.service.ts +158 -0
- package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +116 -0
- package/src/core/modules/hub/services/core-hub-mailbox.service.ts +121 -0
- package/src/core/modules/hub/services/core-hub-migrations.service.ts +112 -0
- package/src/core/modules/hub/services/core-hub-sources.service.ts +194 -0
- package/src/core/modules/hub/services/hub-log-buffer.service.ts +252 -0
- package/src/core/modules/hub/services/hub-query-profiler.service.ts +274 -0
- package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +112 -0
- package/src/core/modules/hub/services/hub-trace-buffer.service.ts +134 -0
- package/src/core.module.ts +61 -1
- package/src/index.ts +7 -0
- package/src/main.ts +22 -3
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
|
|
2
|
+
import { InjectConnection } from '@nestjs/mongoose';
|
|
3
|
+
import { readdir } from 'fs/promises';
|
|
4
|
+
import { Connection } from 'mongoose';
|
|
5
|
+
import { resolve } from 'path';
|
|
6
|
+
|
|
7
|
+
import { ConfigService } from '../../../common/services/config.service';
|
|
8
|
+
import { DEFAULT_MIGRATION_FILE_PATTERN, MigrationRunner } from '../../migrate/migration-runner';
|
|
9
|
+
import { MongoStateStore } from '../../migrate/mongo-state-store';
|
|
10
|
+
import { HubActionMessage } from '../hub-action-messages';
|
|
11
|
+
import { HUB_CONFIG } from '../hub.constants';
|
|
12
|
+
import { HubMigrationsData, HubUnavailable } from '../interfaces/hub-panels.interface';
|
|
13
|
+
import { ResolvedHubConfig } from '../interfaces/hub-config.interface';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Reads migration status for the Migrations panel.
|
|
17
|
+
*
|
|
18
|
+
* Status is read from the migration-state collection directly (works without a configured runner);
|
|
19
|
+
* pending migrations are detected by diffing the completed set against the migrations directory
|
|
20
|
+
* listing. The `up()`/`down()` actions (Phase 7) instantiate a `MigrationRunner` on demand.
|
|
21
|
+
*/
|
|
22
|
+
@Injectable()
|
|
23
|
+
export class CoreHubMigrationsService {
|
|
24
|
+
protected readonly logger = new Logger(CoreHubMigrationsService.name);
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
@Inject(HUB_CONFIG) protected readonly config: ResolvedHubConfig,
|
|
28
|
+
protected readonly configService: ConfigService,
|
|
29
|
+
@Optional() @InjectConnection() protected readonly connection?: Connection,
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
/** Run all pending migrations. Returns the titles that were applied. */
|
|
33
|
+
async runPending(): Promise<{ ran: string[] }> {
|
|
34
|
+
const runner = this.buildRunner();
|
|
35
|
+
const before = new Set(((await this.getStatus()) as HubMigrationsData).completed ?? []);
|
|
36
|
+
await runner.up();
|
|
37
|
+
const after = ((await this.getStatus()) as HubMigrationsData).completed ?? [];
|
|
38
|
+
return { ran: after.filter((title) => !before.has(title)) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Roll back the most recently applied migration. Returns the title that was reverted. */
|
|
42
|
+
async rollbackLast(): Promise<{ rolledBack?: string }> {
|
|
43
|
+
const before = ((await this.getStatus()) as HubMigrationsData).completed ?? [];
|
|
44
|
+
const runner = this.buildRunner();
|
|
45
|
+
await runner.down();
|
|
46
|
+
const after = new Set(((await this.getStatus()) as HubMigrationsData).completed ?? []);
|
|
47
|
+
return { rolledBack: before.find((title) => !after.has(title)) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Build a MigrationRunner from the hub config + the configured Mongo URI. */
|
|
51
|
+
protected buildRunner(): MigrationRunner {
|
|
52
|
+
if (this.config.migrations === false) {
|
|
53
|
+
throw new Error(HubActionMessage.migrationsDisabled);
|
|
54
|
+
}
|
|
55
|
+
const uri = this.configService.getFastButReadOnly<string>('mongoose.uri');
|
|
56
|
+
if (!uri) {
|
|
57
|
+
throw new Error(HubActionMessage.mongoUriMissing);
|
|
58
|
+
}
|
|
59
|
+
return new MigrationRunner({
|
|
60
|
+
migrationsDirectory: resolve(process.cwd(), this.config.migrations.dir),
|
|
61
|
+
stateStore: new MongoStateStore({
|
|
62
|
+
collectionName: this.config.migrations.collectionName,
|
|
63
|
+
lockCollectionName: this.config.migrations.lockCollectionName,
|
|
64
|
+
uri,
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Completed + pending migrations. */
|
|
70
|
+
async getStatus(): Promise<HubMigrationsData | HubUnavailable> {
|
|
71
|
+
if (this.config.migrations === false) {
|
|
72
|
+
return { available: false, hint: 'The migrations panel is disabled.' };
|
|
73
|
+
}
|
|
74
|
+
const db = this.connection?.db;
|
|
75
|
+
if (!db) {
|
|
76
|
+
return { available: false, hint: 'No MongoDB connection is available.' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const collectionName = this.config.migrations.collectionName;
|
|
80
|
+
let completed: string[] = [];
|
|
81
|
+
let lastRun: string | undefined;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const [stateDoc] = await db.collection(collectionName).find({}).limit(1).toArray();
|
|
85
|
+
completed = (stateDoc?.migrations ?? []).map((m: { title: string }) => m.title);
|
|
86
|
+
lastRun = stateDoc?.lastRun;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
this.logger.warn(`Failed to read migration state: ${error instanceof Error ? error.message : String(error)}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { dirAvailable, files } = await this.listMigrationFiles();
|
|
92
|
+
const pending = dirAvailable ? files.filter((f) => !completed.includes(f)) : [];
|
|
93
|
+
|
|
94
|
+
return { completed, dirAvailable, lastRun, pending, source: 'collection' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** List migration file titles from the configured directory (empty when the dir is missing). */
|
|
98
|
+
protected async listMigrationFiles(): Promise<{ dirAvailable: boolean; files: string[] }> {
|
|
99
|
+
if (this.config.migrations === false) {
|
|
100
|
+
return { dirAvailable: false, files: [] };
|
|
101
|
+
}
|
|
102
|
+
const dir = resolve(process.cwd(), this.config.migrations.dir);
|
|
103
|
+
try {
|
|
104
|
+
// A missing or unreadable directory rejects here (ENOENT) → treated as "no dir available".
|
|
105
|
+
const entries = await readdir(dir);
|
|
106
|
+
const files = entries.filter((file) => DEFAULT_MIGRATION_FILE_PATTERN.test(file)).sort();
|
|
107
|
+
return { dirAvailable: true, files };
|
|
108
|
+
} catch {
|
|
109
|
+
return { dirAvailable: false, files: [] };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Injectable, Logger, Optional } from '@nestjs/common';
|
|
2
|
+
import { ModuleRef } from '@nestjs/core';
|
|
3
|
+
import { SchedulerRegistry } from '@nestjs/schedule';
|
|
4
|
+
|
|
5
|
+
import { CoreAiInteractionService } from '../../ai/services/core-ai-interaction.service';
|
|
6
|
+
import { CoreBetterAuthUserMapper } from '../../better-auth/core-better-auth-user.mapper';
|
|
7
|
+
import { CoreErrorCodeService } from '../../error-code/core-error-code.service';
|
|
8
|
+
import { CorePermissionsService } from '../../permissions/core-permissions.service';
|
|
9
|
+
import { HubCronData, HubErrorCodesData, HubUnavailable } from '../interfaces/hub-panels.interface';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Single choke point for the Hub's OPTIONAL data sources.
|
|
13
|
+
*
|
|
14
|
+
* Providers that live in non-global modules (error-code, better-auth, ai, permissions) cannot be
|
|
15
|
+
* `@Optional() @Inject()`-ed — that would always resolve to `undefined` even when the module exists.
|
|
16
|
+
* Instead they are resolved lazily via `ModuleRef.get(Token, { strict: false })` inside method bodies
|
|
17
|
+
* (call time — SWC/TDZ-safe), cached after the first hit, and every panel degrades to
|
|
18
|
+
* `{ available: false }` when its source is absent. `SchedulerRegistry` IS global, so it is injected
|
|
19
|
+
* normally.
|
|
20
|
+
*/
|
|
21
|
+
@Injectable()
|
|
22
|
+
export class CoreHubSourcesService {
|
|
23
|
+
protected readonly logger = new Logger(CoreHubSourcesService.name);
|
|
24
|
+
private readonly resolveCache = new Map<unknown, unknown>();
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
protected readonly moduleRef: ModuleRef,
|
|
28
|
+
@Optional() protected readonly schedulerRegistry?: SchedulerRegistry,
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
/** AI availability + a light interaction/usage summary. */
|
|
32
|
+
async getAi(): Promise<HubUnavailable | { available: true; totalInteractions: number }> {
|
|
33
|
+
const interactionService = this.resolve<CoreAiInteractionService>(CoreAiInteractionService);
|
|
34
|
+
if (!interactionService) {
|
|
35
|
+
return { available: false, hint: 'The AI module is not enabled.' };
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const model = (interactionService as unknown as { mainDbModel?: { estimatedDocumentCount(): Promise<number> } })
|
|
39
|
+
.mainDbModel;
|
|
40
|
+
const totalInteractions = model ? await model.estimatedDocumentCount() : 0;
|
|
41
|
+
return { available: true, totalInteractions };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
this.logger.warn(`Failed to read AI interactions: ${error instanceof Error ? error.message : String(error)}`);
|
|
44
|
+
return { available: true, totalInteractions: 0 };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Legacy → IAM auth migration status. */
|
|
49
|
+
async getAuthMigration(): Promise<HubUnavailable | Record<string, unknown>> {
|
|
50
|
+
const mapper = this.resolve<CoreBetterAuthUserMapper>(CoreBetterAuthUserMapper);
|
|
51
|
+
if (!mapper) {
|
|
52
|
+
return { available: false, hint: 'BetterAuth is not enabled.' };
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return { available: true, ...(await mapper.getMigrationStatus()) };
|
|
56
|
+
} catch (error) {
|
|
57
|
+
this.logger.warn(
|
|
58
|
+
`Failed to read auth migration status: ${error instanceof Error ? error.message : String(error)}`,
|
|
59
|
+
);
|
|
60
|
+
return { available: false, hint: 'Failed to read auth migration status.' };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Registered cron jobs, intervals and timeouts (via `@nestjs/schedule`). */
|
|
65
|
+
getCron(): HubCronData | HubUnavailable {
|
|
66
|
+
if (!this.schedulerRegistry) {
|
|
67
|
+
return { available: false, hint: 'ScheduleModule.forRoot() is not imported.' };
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const jobs = [];
|
|
71
|
+
for (const [name, job] of this.schedulerRegistry.getCronJobs()) {
|
|
72
|
+
let nextDate: string | undefined;
|
|
73
|
+
let lastDate: string | undefined;
|
|
74
|
+
try {
|
|
75
|
+
nextDate = job.nextDate?.()?.toISO?.() ?? undefined;
|
|
76
|
+
} catch {
|
|
77
|
+
/* a job may throw before first run */
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
lastDate = job.lastDate?.()?.toISOString?.() ?? undefined;
|
|
81
|
+
} catch {
|
|
82
|
+
/* no last run yet */
|
|
83
|
+
}
|
|
84
|
+
jobs.push({ lastDate, name, nextDate, running: Boolean((job as unknown as { running?: boolean }).running) });
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
intervals: [...this.schedulerRegistry.getIntervals()],
|
|
88
|
+
jobs,
|
|
89
|
+
timeouts: [...this.schedulerRegistry.getTimeouts()],
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
this.logger.warn(`Failed to read scheduler registry: ${error instanceof Error ? error.message : String(error)}`);
|
|
93
|
+
return { available: false, hint: 'Failed to read scheduled jobs.' };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** True when BetterAuth (IAM) is actually wired in — used by the dashboard feature matrix. */
|
|
98
|
+
isBetterAuthActive(): boolean {
|
|
99
|
+
return !!this.resolve(CoreBetterAuthUserMapper);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Whether each optional/degradable panel's backing source is actually present. Fed into the session
|
|
104
|
+
* payload so the client can grey out and DISABLE nav entries whose module is absent, instead of
|
|
105
|
+
* letting the user click through to an "unavailable" panel. Panels not listed here are always
|
|
106
|
+
* available (their data is intrinsic to the Hub).
|
|
107
|
+
*/
|
|
108
|
+
getPanelAvailability(): Record<string, boolean> {
|
|
109
|
+
return {
|
|
110
|
+
ai: !!this.resolve(CoreAiInteractionService),
|
|
111
|
+
'auth-migration': !!this.resolve(CoreBetterAuthUserMapper),
|
|
112
|
+
cron: !!this.schedulerRegistry,
|
|
113
|
+
'error-codes': !!this.resolve(CoreErrorCodeService),
|
|
114
|
+
routes: !!this.resolve(CorePermissionsService),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Control a cron job by name (start | stop | trigger). Throws when scheduling is unavailable. */
|
|
119
|
+
controlCron(name: string, action: 'start' | 'stop' | 'trigger'): void {
|
|
120
|
+
if (!this.schedulerRegistry) {
|
|
121
|
+
throw new Error('ScheduleModule.forRoot() is not imported.');
|
|
122
|
+
}
|
|
123
|
+
const job = this.schedulerRegistry.getCronJob(name);
|
|
124
|
+
if (action === 'start') {
|
|
125
|
+
job.start();
|
|
126
|
+
} else if (action === 'stop') {
|
|
127
|
+
job.stop();
|
|
128
|
+
} else {
|
|
129
|
+
// Fire the job's callback once now.
|
|
130
|
+
const fire = (job as unknown as { fireOnTick?: () => void }).fireOnTick;
|
|
131
|
+
if (typeof fire === 'function') {
|
|
132
|
+
fire.call(job);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Error-code catalog with de/en translations. */
|
|
138
|
+
getErrorCodes(locale = 'en'): HubErrorCodesData | HubUnavailable {
|
|
139
|
+
const service = this.resolve<CoreErrorCodeService>(CoreErrorCodeService);
|
|
140
|
+
if (!service) {
|
|
141
|
+
return { available: false, hint: 'The error-code module is not enabled.' };
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const codes = service.getErrorCodes();
|
|
145
|
+
const de = this.safeTranslations(service, 'de');
|
|
146
|
+
const en = this.safeTranslations(service, 'en');
|
|
147
|
+
return {
|
|
148
|
+
codes: codes.map((code) => ({ code, de: de[code], en: en[code] })),
|
|
149
|
+
locale,
|
|
150
|
+
};
|
|
151
|
+
} catch (error) {
|
|
152
|
+
this.logger.warn(`Failed to read error codes: ${error instanceof Error ? error.message : String(error)}`);
|
|
153
|
+
return { available: false, hint: 'Failed to read error codes.' };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The permissions report (modules + coverage stats), reusing the permissions module's cached scan. */
|
|
158
|
+
async getRoutes(): Promise<HubUnavailable | { modules: unknown[]; stats: unknown; warnings: unknown[] }> {
|
|
159
|
+
const service = this.resolve<CorePermissionsService>(CorePermissionsService);
|
|
160
|
+
if (!service) {
|
|
161
|
+
return { available: false, hint: 'The permissions module is not enabled — see /permissions.' };
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const report = service.getReport() ?? (await service.getOrScan());
|
|
165
|
+
return { modules: report.modules ?? [], stats: report.stats, warnings: report.warnings ?? [] };
|
|
166
|
+
} catch (error) {
|
|
167
|
+
this.logger.warn(`Failed to read routes report: ${error instanceof Error ? error.message : String(error)}`);
|
|
168
|
+
return { available: false, hint: 'Failed to read routes report.' };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Resolve an optional provider once, container-wide, and cache the result (including a miss). */
|
|
173
|
+
protected resolve<T>(token: unknown): T | undefined {
|
|
174
|
+
if (this.resolveCache.has(token)) {
|
|
175
|
+
return this.resolveCache.get(token) as T | undefined;
|
|
176
|
+
}
|
|
177
|
+
let instance: T | undefined;
|
|
178
|
+
try {
|
|
179
|
+
instance = this.moduleRef.get<T>(token as never, { strict: false });
|
|
180
|
+
} catch {
|
|
181
|
+
instance = undefined;
|
|
182
|
+
}
|
|
183
|
+
this.resolveCache.set(token, instance);
|
|
184
|
+
return instance;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private safeTranslations(service: CoreErrorCodeService, locale: string): Record<string, string> {
|
|
188
|
+
try {
|
|
189
|
+
return service.getTranslations(locale as never).errors;
|
|
190
|
+
} catch {
|
|
191
|
+
return {};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConsoleLogger,
|
|
3
|
+
Inject,
|
|
4
|
+
Injectable,
|
|
5
|
+
Logger,
|
|
6
|
+
LoggerService,
|
|
7
|
+
LogLevel,
|
|
8
|
+
OnModuleDestroy,
|
|
9
|
+
OnModuleInit,
|
|
10
|
+
} from '@nestjs/common';
|
|
11
|
+
import { inspect } from 'util';
|
|
12
|
+
|
|
13
|
+
import { redactSensitiveText } from '../../../common/helpers/logging.helper';
|
|
14
|
+
import { HUB_CONFIG } from '../hub.constants';
|
|
15
|
+
import { HubRingBuffer } from '../hub-ring-buffer';
|
|
16
|
+
import { HubLogRecord, HubLogsData } from '../interfaces/hub-panels.interface';
|
|
17
|
+
import { ResolvedHubConfig } from '../interfaces/hub-config.interface';
|
|
18
|
+
|
|
19
|
+
interface BufferedLog extends HubLogRecord {
|
|
20
|
+
timestamp: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A LoggerService that CHAINS to whatever logger was active before it, so nothing is swallowed, and
|
|
25
|
+
* feeds each record into the Hub's ring buffer. Installed process-globally via `Logger.overrideLogger`.
|
|
26
|
+
*/
|
|
27
|
+
class HubDelegatingLogger implements LoggerService {
|
|
28
|
+
detached = false;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly capture: (level: LogLevel, message: unknown, params: unknown[]) => void,
|
|
32
|
+
private readonly previous: LoggerService | undefined,
|
|
33
|
+
private readonly fallback: ConsoleLogger,
|
|
34
|
+
) {}
|
|
35
|
+
|
|
36
|
+
debug(message: unknown, ...params: unknown[]): void {
|
|
37
|
+
this.handle('debug', message, params);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
error(message: unknown, ...params: unknown[]): void {
|
|
41
|
+
this.handle('error', message, params);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fatal(message: unknown, ...params: unknown[]): void {
|
|
45
|
+
this.handle('fatal', message, params);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
log(message: unknown, ...params: unknown[]): void {
|
|
49
|
+
this.handle('log', message, params);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setLogLevels(levels: LogLevel[]): void {
|
|
53
|
+
this.fallback.setLogLevels?.(levels);
|
|
54
|
+
(this.previous as { setLogLevels?: (l: LogLevel[]) => void } | undefined)?.setLogLevels?.(levels);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
verbose(message: unknown, ...params: unknown[]): void {
|
|
58
|
+
this.handle('verbose', message, params);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
warn(message: unknown, ...params: unknown[]): void {
|
|
62
|
+
this.handle('warn', message, params);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private handle(level: LogLevel, message: unknown, params: unknown[]): void {
|
|
66
|
+
if (!this.detached) {
|
|
67
|
+
try {
|
|
68
|
+
this.capture(level, message, params);
|
|
69
|
+
} catch {
|
|
70
|
+
/* the capture path must NEVER throw into the caller's log statement */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const target = this.previous ?? this.fallback;
|
|
74
|
+
try {
|
|
75
|
+
(target as unknown as Record<string, (...a: unknown[]) => void>)[level]?.(message, ...params);
|
|
76
|
+
} catch {
|
|
77
|
+
/* chaining must not break either */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Captures NestJS `Logger` output into a ring buffer for the Logs panel — WITHOUT requiring the
|
|
84
|
+
* consumer to touch main.ts.
|
|
85
|
+
*
|
|
86
|
+
* `Logger.overrideLogger()` installs a delegating logger process-globally (the same mechanism
|
|
87
|
+
* `app.useLogger()` uses internally). It chains to the pre-existing logger (never swallows), applies
|
|
88
|
+
* secret redaction + truncation, and restores the previous logger on shutdown — owner-checked, so a
|
|
89
|
+
* newer override is never clobbered (the multi-app test-safety concern). Zero cost when disabled.
|
|
90
|
+
*/
|
|
91
|
+
@Injectable()
|
|
92
|
+
export class HubLogBufferService implements OnModuleDestroy, OnModuleInit {
|
|
93
|
+
private buffer?: HubRingBuffer<BufferedLog>;
|
|
94
|
+
private cfg?: Exclude<ResolvedHubConfig['collectors']['logs'], false>;
|
|
95
|
+
private delegating?: HubDelegatingLogger;
|
|
96
|
+
private excludeContexts = new Set<string>();
|
|
97
|
+
private previousRef?: LoggerService;
|
|
98
|
+
|
|
99
|
+
constructor(@Inject(HUB_CONFIG) protected readonly config: ResolvedHubConfig) {}
|
|
100
|
+
|
|
101
|
+
/** Idempotent install of the delegating logger. Public so reads can self-heal after a foreign override. */
|
|
102
|
+
attach(): void {
|
|
103
|
+
if (this.config.collectors.logs === false) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!this.cfg) {
|
|
107
|
+
this.cfg = this.config.collectors.logs;
|
|
108
|
+
this.buffer = new HubRingBuffer<BufferedLog>(this.cfg.capacity);
|
|
109
|
+
this.excludeContexts = new Set(this.cfg.excludeContexts);
|
|
110
|
+
}
|
|
111
|
+
const current = this.staticRef();
|
|
112
|
+
if (current === this.delegating) {
|
|
113
|
+
return; // still the owner
|
|
114
|
+
}
|
|
115
|
+
// Never chain onto another (foreign/stale) HubDelegatingLogger — that would accumulate a growing
|
|
116
|
+
// delegate chain across app instances in the same process (parallel tests). Fall back to the
|
|
117
|
+
// ConsoleLogger instead; a real, non-Hub logger is chained to as the previous target.
|
|
118
|
+
const previous = current instanceof HubDelegatingLogger ? undefined : current;
|
|
119
|
+
const fallback = new ConsoleLogger();
|
|
120
|
+
const levels = (Logger as unknown as { logLevels?: LogLevel[] }).logLevels;
|
|
121
|
+
if (levels) {
|
|
122
|
+
fallback.setLogLevels(levels);
|
|
123
|
+
}
|
|
124
|
+
this.previousRef = previous;
|
|
125
|
+
this.delegating = new HubDelegatingLogger(
|
|
126
|
+
(level, message, params) => this.push(level, message, params),
|
|
127
|
+
previous,
|
|
128
|
+
fallback,
|
|
129
|
+
);
|
|
130
|
+
Logger.overrideLogger(this.delegating);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
clear(): void {
|
|
134
|
+
this.buffer?.clear();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
detach(): void {
|
|
138
|
+
if (this.delegating) {
|
|
139
|
+
this.delegating.detached = true;
|
|
140
|
+
}
|
|
141
|
+
// Only restore if we are still the owner — never clobber a newer override.
|
|
142
|
+
if (this.delegating && this.staticRef() === this.delegating) {
|
|
143
|
+
Logger.overrideLogger(this.previousRef ?? (undefined as unknown as LoggerService));
|
|
144
|
+
}
|
|
145
|
+
this.delegating = undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get enabled(): boolean {
|
|
149
|
+
return this.config.collectors.logs !== false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
getData(since?: number): HubLogsData {
|
|
153
|
+
this.selfHeal();
|
|
154
|
+
const records = !this.buffer ? [] : since === undefined ? this.buffer.recent() : this.buffer.since(since);
|
|
155
|
+
return { cursor: this.buffer?.lastSeq ?? -1, dropped: this.buffer?.firstRetainedSeq ?? -1, records };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
onModuleDestroy(): void {
|
|
159
|
+
this.detach();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
onModuleInit(): void {
|
|
163
|
+
this.attach();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Extract the trailing string context param (ConsoleLogger convention) + an error stack. */
|
|
167
|
+
private extract(level: LogLevel, message: unknown, params: unknown[]): BufferedLog | undefined {
|
|
168
|
+
const parts = [...params];
|
|
169
|
+
let context: string | undefined;
|
|
170
|
+
if (parts.length && typeof parts[parts.length - 1] === 'string') {
|
|
171
|
+
context = parts.pop() as string;
|
|
172
|
+
}
|
|
173
|
+
if (context && this.excludeContexts.has(context)) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
let stack: string | undefined;
|
|
177
|
+
// For error(), Nest passes [stack, context]; after popping context a trailing stack-ish string remains.
|
|
178
|
+
if (
|
|
179
|
+
level === 'error' &&
|
|
180
|
+
parts.length &&
|
|
181
|
+
typeof parts[parts.length - 1] === 'string' &&
|
|
182
|
+
(parts[parts.length - 1] as string).includes('\n at ')
|
|
183
|
+
) {
|
|
184
|
+
stack = (parts.pop() as string).slice(0, 2000);
|
|
185
|
+
}
|
|
186
|
+
const text = this.stringify(message, parts);
|
|
187
|
+
const max = this.cfg?.maxMessageLength ?? 2048;
|
|
188
|
+
// Bound the redaction work: only the first `max` chars are ever shown after truncation, so
|
|
189
|
+
// redacting an arbitrarily large stringified payload is wasted effort. Cap the input to
|
|
190
|
+
// `max` + a margin (so a secret straddling the truncation point is still fully redacted).
|
|
191
|
+
const redactInput = text.length > max + 4096 ? text.slice(0, max + 4096) : text;
|
|
192
|
+
const redacted = redactSensitiveText(redactInput);
|
|
193
|
+
return {
|
|
194
|
+
context,
|
|
195
|
+
level,
|
|
196
|
+
message: redacted.length > max ? redacted.slice(0, max) + '… [truncated]' : redacted,
|
|
197
|
+
seq: 0,
|
|
198
|
+
stack,
|
|
199
|
+
timestamp: 0,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private push(level: LogLevel, message: unknown, params: unknown[]): void {
|
|
204
|
+
if (!this.buffer || !this.cfg) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!this.cfg.levels.includes(level)) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const record = this.extract(level, message, params);
|
|
211
|
+
if (record) {
|
|
212
|
+
this.buffer.add({
|
|
213
|
+
context: record.context,
|
|
214
|
+
level: record.level,
|
|
215
|
+
message: record.message,
|
|
216
|
+
stack: record.stack,
|
|
217
|
+
} as Omit<BufferedLog, 'seq' | 'timestamp'>);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Re-attach if a consumer installed their own logger after us (checked on each read; no timers). */
|
|
222
|
+
private selfHeal(): void {
|
|
223
|
+
if (!this.enabled) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const current = this.staticRef();
|
|
227
|
+
if (current !== this.delegating && current !== undefined) {
|
|
228
|
+
this.attach();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private staticRef(): LoggerService | undefined {
|
|
233
|
+
return (Logger as unknown as { staticInstanceRef?: LoggerService }).staticInstanceRef;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private stringify(message: unknown, params: unknown[]): string {
|
|
237
|
+
const one = (value: unknown): string => {
|
|
238
|
+
if (typeof value === 'string') {
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
if (value instanceof Error) {
|
|
242
|
+
return `${value.name}: ${value.message}`;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
return JSON.stringify(value);
|
|
246
|
+
} catch {
|
|
247
|
+
return inspect(value, { breakLength: Infinity, depth: 2 });
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
return [message, ...params].map(one).join(' | ');
|
|
251
|
+
}
|
|
252
|
+
}
|