@lenne.tech/nest-server 11.31.2 → 11.31.3
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.md +28 -1
- package/FRAMEWORK-API.md +4 -1
- 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/interfaces/server-options.interface.d.ts +9 -0
- 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/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 +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +1 -0
- package/migration-guides/11.31.2-to-11.31.3.md +135 -0
- package/package.json +5 -4
- 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/interfaces/server-options.interface.ts +46 -0
- package/src/core/common/services/email.service.ts +33 -1
- package/src/core/common/services/template.service.ts +21 -16
- 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 +6 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { Controller, Get, Headers, Optional, Param, Query, Res } from '@nestjs/common';
|
|
2
|
+
import { Response } from 'express';
|
|
3
|
+
|
|
4
|
+
import { Roles } from '../../common/decorators/roles.decorator';
|
|
5
|
+
import { RoleEnum } from '../../common/enums/role.enum';
|
|
6
|
+
|
|
7
|
+
import { CoreHubHtmlService } from './core-hub-html.service';
|
|
8
|
+
import { CoreHubService } from './core-hub.service';
|
|
9
|
+
import { HUB_PANELS } from './hub-nav';
|
|
10
|
+
import { CoreHubDbService } from './services/core-hub-db.service';
|
|
11
|
+
import { CoreHubEmailService } from './services/core-hub-email.service';
|
|
12
|
+
import { CoreHubMailboxService } from './services/core-hub-mailbox.service';
|
|
13
|
+
import { CoreHubMigrationsService } from './services/core-hub-migrations.service';
|
|
14
|
+
import { CoreHubSourcesService } from './services/core-hub-sources.service';
|
|
15
|
+
import { HubLogBufferService } from './services/hub-log-buffer.service';
|
|
16
|
+
import { HubQueryProfilerService } from './services/hub-query-profiler.service';
|
|
17
|
+
import { HubTraceBufferService } from './services/hub-trace-buffer.service';
|
|
18
|
+
import { buildHubSecurityHeaders, escapeHtml, generateNonce, injectNonce } from './helpers/hub-shell.helper';
|
|
19
|
+
|
|
20
|
+
/** Route sub-paths of every non-root panel — the shell handler answers all of them. */
|
|
21
|
+
const HUB_PAGE_PATHS = HUB_PANELS.filter((panel) => panel.path).map((panel) => panel.path);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Serves the Hub SPA shell (all panel routes), the shared client runtime and the read-only JSON
|
|
25
|
+
* sidecars. Mutating operations live in `CoreHubActionsController`.
|
|
26
|
+
*
|
|
27
|
+
* The controller path and required roles are assigned at runtime in `CoreHubModule.forRoot()` via
|
|
28
|
+
* `Reflect.defineMetadata` (same pattern as the permissions module), so they follow the configured
|
|
29
|
+
* `hub.path` / `hub.roles`.
|
|
30
|
+
*
|
|
31
|
+
* Every response uses `@Res()` and is written manually: this bypasses the four global response
|
|
32
|
+
* interceptors (which would otherwise walk/mutate the payload — `CheckSecurityInterceptor` deletes
|
|
33
|
+
* secret-named keys in place) and lets the Hub set its own strict CSP + no-store headers.
|
|
34
|
+
*
|
|
35
|
+
* Overridable via `overrides.hub.controller`.
|
|
36
|
+
*/
|
|
37
|
+
@Controller()
|
|
38
|
+
export class CoreHubController {
|
|
39
|
+
private cachedClientScript?: string;
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
protected readonly hubService: CoreHubService,
|
|
43
|
+
protected readonly htmlService: CoreHubHtmlService,
|
|
44
|
+
protected readonly dbService: CoreHubDbService,
|
|
45
|
+
protected readonly migrationsService: CoreHubMigrationsService,
|
|
46
|
+
protected readonly sourcesService: CoreHubSourcesService,
|
|
47
|
+
protected readonly emailService: CoreHubEmailService,
|
|
48
|
+
protected readonly logBuffer: HubLogBufferService,
|
|
49
|
+
protected readonly traceBuffer: HubTraceBufferService,
|
|
50
|
+
protected readonly queryProfiler: HubQueryProfilerService,
|
|
51
|
+
// Only provided when the mailbox is enabled.
|
|
52
|
+
@Optional() protected readonly mailboxService?: CoreHubMailboxService,
|
|
53
|
+
) {}
|
|
54
|
+
|
|
55
|
+
@Get('ai.json')
|
|
56
|
+
async aiJson(@Res() res: Response): Promise<void> {
|
|
57
|
+
this.sendJson(res, await this.sourcesService.getAi());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Get('auth-migration.json')
|
|
61
|
+
async authMigrationJson(@Res() res: Response): Promise<void> {
|
|
62
|
+
this.sendJson(res, await this.sourcesService.getAuthMigration());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Get('config.json')
|
|
66
|
+
configJson(@Res() res: Response): void {
|
|
67
|
+
this.sendJson(res, this.hubService.getConfigMasked());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Get('cron.json')
|
|
71
|
+
cronJson(@Res() res: Response): void {
|
|
72
|
+
this.sendJson(res, this.sourcesService.getCron());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Get('emails.json')
|
|
76
|
+
emailsJson(@Res() res: Response): void {
|
|
77
|
+
if (!this.emailService.previewEnabled) {
|
|
78
|
+
this.sendJson(res, { available: false, hint: 'The email preview panel is disabled (hub.emailPreview: false).' });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
this.sendJson(res, this.emailService.getTemplates());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Get('error-codes.json')
|
|
85
|
+
errorCodesJson(@Query('locale') locale: string | undefined, @Res() res: Response): void {
|
|
86
|
+
this.sendJson(res, this.sourcesService.getErrorCodes(locale || 'en'));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Get('dashboard.json')
|
|
90
|
+
dashboardJson(@Res() res: Response): void {
|
|
91
|
+
this.sendJson(res, this.hubService.getDashboard());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Get('db.json')
|
|
95
|
+
async dbJson(@Res() res: Response): Promise<void> {
|
|
96
|
+
this.sendJson(res, await this.dbService.getDbStats());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Get('diagnostics.json')
|
|
100
|
+
diagnosticsJson(@Res() res: Response): void {
|
|
101
|
+
this.sendJson(res, this.hubService.getDiagnostics(this.collectorBuffers()));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@Get('files.json')
|
|
105
|
+
async filesJson(
|
|
106
|
+
@Query('bucket') bucket: string | undefined,
|
|
107
|
+
@Query('skip') skip: string | undefined,
|
|
108
|
+
@Query('limit') limit: string | undefined,
|
|
109
|
+
@Res() res: Response,
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
this.sendJson(res, await this.dbService.getFiles(bucket || 'fs', Number(skip) || 0, Number(limit) || 100));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@Get('migrations.json')
|
|
115
|
+
async migrationsJson(@Res() res: Response): Promise<void> {
|
|
116
|
+
this.sendJson(res, await this.migrationsService.getStatus());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Get('mailbox.json')
|
|
120
|
+
mailboxJson(@Query('since') since: string | undefined, @Res() res: Response): void {
|
|
121
|
+
if (!this.mailboxService) {
|
|
122
|
+
this.sendJson(res, { available: false, hint: 'The mailbox is not enabled.' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.sendJson(res, this.mailboxService.getMailbox(since !== undefined ? Number(since) : undefined));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Get('mailbox/:seq/html')
|
|
129
|
+
mailboxHtml(@Param('seq') seq: string, @Res() res: Response): void {
|
|
130
|
+
const html = this.mailboxService?.getMailHtml(Number(seq));
|
|
131
|
+
res.set({
|
|
132
|
+
'Cache-Control': 'no-store',
|
|
133
|
+
'Content-Security-Policy': "default-src 'none'; img-src * data:; style-src 'unsafe-inline'; font-src data:",
|
|
134
|
+
'X-Content-Type-Options': 'nosniff',
|
|
135
|
+
'X-Frame-Options': 'SAMEORIGIN',
|
|
136
|
+
});
|
|
137
|
+
res
|
|
138
|
+
.type('html')
|
|
139
|
+
.send(
|
|
140
|
+
html ??
|
|
141
|
+
'<!DOCTYPE html><html><body style="font-family:sans-serif;color:#8b97a8;padding:20px">No preview available.</body></html>',
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@Get('logs.json')
|
|
146
|
+
logsJson(@Query('since') since: string | undefined, @Res() res: Response): void {
|
|
147
|
+
if (!this.logBuffer.enabled) {
|
|
148
|
+
this.sendJson(res, { available: false, hint: 'The logs collector is disabled.' });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.sendJson(res, this.logBuffer.getData(since !== undefined ? Number(since) : undefined));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@Get('models.json')
|
|
155
|
+
modelsJson(@Res() res: Response): void {
|
|
156
|
+
this.sendJson(res, this.dbService.getModels());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* ADMIN-gated auth probe AND cockpit-chrome payload: 200 returns the navigation, environment badge,
|
|
161
|
+
* version and external links the client uses to build the layout; 401/403 keeps the shell showing
|
|
162
|
+
* only the login form. The public shell HTML deliberately carries none of this data.
|
|
163
|
+
*/
|
|
164
|
+
@Get('session.json')
|
|
165
|
+
sessionJson(@Res() res: Response): void {
|
|
166
|
+
this.sendJson(res, this.hubService.getSessionPayload());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Get('queries.json')
|
|
170
|
+
queriesJson(@Res() res: Response): void {
|
|
171
|
+
if (!this.queryProfiler.enabled) {
|
|
172
|
+
this.sendJson(res, {
|
|
173
|
+
available: false,
|
|
174
|
+
hint: 'The query profiler is disabled (opt-in via hub.collectors.queries).',
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.sendJson(res, this.queryProfiler.getData());
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@Get('traces.json')
|
|
182
|
+
tracesJson(@Query('since') since: string | undefined, @Res() res: Response): void {
|
|
183
|
+
if (!this.traceBuffer.enabled) {
|
|
184
|
+
this.sendJson(res, { available: false, hint: 'The traces collector is disabled.' });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.sendJson(res, this.traceBuffer.getData(since !== undefined ? Number(since) : undefined));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@Get('routes.json')
|
|
191
|
+
async routesJson(@Res() res: Response): Promise<void> {
|
|
192
|
+
this.sendJson(res, await this.sourcesService.getRoutes());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Server-rendered email preview, delivered as sandboxed HTML for an <iframe>. A dedicated,
|
|
197
|
+
* stricter CSP (no scripts) applies to the rendered template markup.
|
|
198
|
+
*/
|
|
199
|
+
@Get('emails/preview')
|
|
200
|
+
async emailPreview(
|
|
201
|
+
@Query('template') template: string | undefined,
|
|
202
|
+
@Query('locale') locale: string | undefined,
|
|
203
|
+
@Res() res: Response,
|
|
204
|
+
): Promise<void> {
|
|
205
|
+
const result = await this.emailService.renderPreview(template || '', locale);
|
|
206
|
+
res.set({
|
|
207
|
+
'Cache-Control': 'no-store',
|
|
208
|
+
'Content-Security-Policy': "default-src 'none'; img-src * data:; style-src 'unsafe-inline'; font-src data:",
|
|
209
|
+
'X-Content-Type-Options': 'nosniff',
|
|
210
|
+
'X-Frame-Options': 'SAMEORIGIN',
|
|
211
|
+
});
|
|
212
|
+
if ('available' in result && result.available === false) {
|
|
213
|
+
res
|
|
214
|
+
.type('html')
|
|
215
|
+
.send(
|
|
216
|
+
`<!DOCTYPE html><html><body style="font-family:sans-serif;color:#8b97a8;padding:20px">${escapeHtml(result.hint)}</body></html>`,
|
|
217
|
+
);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// The rendered template is trusted framework/project EJS output; the sandbox CSP blocks scripts.
|
|
221
|
+
res.type('html').send((result as { html: string }).html);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Public: the shared client runtime carries no secrets, and the public /hub/auth token-paste page
|
|
225
|
+
// must be able to load it in cookie-less setups.
|
|
226
|
+
@Get('hub.js')
|
|
227
|
+
@Roles(RoleEnum.S_EVERYONE)
|
|
228
|
+
hubJs(@Res() res: Response): void {
|
|
229
|
+
if (!this.cachedClientScript) {
|
|
230
|
+
this.cachedClientScript = this.htmlService.buildClientScript();
|
|
231
|
+
}
|
|
232
|
+
res.set(buildHubSecurityHeaders(generateNonce(), { cacheable: true }));
|
|
233
|
+
res.type('application/javascript').send(this.cachedClientScript);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Root panel (dashboard). PUBLIC — the shell is only the cockpit chrome (no data), and the client
|
|
238
|
+
* shows a login form when the ADMIN-gated data sidecars answer 401. This makes the Hub
|
|
239
|
+
* self-sufficient: an admin can log in directly at the API without the frontend. The panel
|
|
240
|
+
* structure is already discoverable via the public `hub.js`, so serving the shell adds no exposure;
|
|
241
|
+
* every `*.json` sidecar and every action stays ADMIN-gated.
|
|
242
|
+
*/
|
|
243
|
+
@Get()
|
|
244
|
+
@Roles(RoleEnum.S_EVERYONE)
|
|
245
|
+
root(@Headers('authorization') authHeader: string | undefined, @Res() res: Response): void {
|
|
246
|
+
this.sendShell(res, authHeader);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** All other panel routes serve the same (public) shell; the client router picks the active panel. */
|
|
250
|
+
@Get(HUB_PAGE_PATHS)
|
|
251
|
+
@Roles(RoleEnum.S_EVERYONE)
|
|
252
|
+
page(@Headers('authorization') authHeader: string | undefined, @Res() res: Response): void {
|
|
253
|
+
this.sendShell(res, authHeader);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Backward-compatible alias for the login/token-paste entry (now handled inline by the shell). */
|
|
257
|
+
@Get('auth')
|
|
258
|
+
@Roles(RoleEnum.S_EVERYONE)
|
|
259
|
+
authPage(@Res() res: Response): void {
|
|
260
|
+
this.sendShell(res, undefined);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Buffer fill levels for the diagnostics panel. */
|
|
264
|
+
protected collectorBuffers(): Record<string, { capacity: number; enabled: boolean; size: number }> {
|
|
265
|
+
const logs = this.logBuffer.getData();
|
|
266
|
+
const traces = this.traceBuffer.getData();
|
|
267
|
+
const queries = this.queryProfiler.getData();
|
|
268
|
+
return {
|
|
269
|
+
logs: { capacity: 0, enabled: this.logBuffer.enabled, size: logs.records.length },
|
|
270
|
+
queries: { capacity: 0, enabled: this.queryProfiler.enabled, size: queries.recent.length },
|
|
271
|
+
traces: { capacity: 0, enabled: this.traceBuffer.enabled, size: traces.traces.length },
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
protected sendJson(res: Response, data: unknown): void {
|
|
276
|
+
res.set(buildHubSecurityHeaders(generateNonce()));
|
|
277
|
+
res.type('application/json').send(JSON.stringify(data));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
protected sendShell(res: Response, authHeader?: string): void {
|
|
281
|
+
const nonce = generateNonce();
|
|
282
|
+
const html = injectNonce(this.htmlService.buildShell(authHeader), nonce);
|
|
283
|
+
res.set(buildHubSecurityHeaders(nonce));
|
|
284
|
+
res.type('html').send(html);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { PATH_METADATA } from '@nestjs/common/constants';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { CoreHubController } from './core-hub.controller';
|
|
5
|
+
import { CoreHubModule } from './core-hub.module';
|
|
6
|
+
|
|
7
|
+
const ctx = { env: 'local', version: '1.0.0' };
|
|
8
|
+
|
|
9
|
+
describe('CoreHubModule.forRoot', () => {
|
|
10
|
+
it('builds a dynamic module with the hub controller', () => {
|
|
11
|
+
const mod = CoreHubModule.forRoot({ config: {}, ctx });
|
|
12
|
+
expect(mod.module).toBe(CoreHubModule);
|
|
13
|
+
expect(mod.controllers?.length).toBeGreaterThan(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('becomes global and provides the email-capture binding when the mailbox is enabled', () => {
|
|
17
|
+
const mod = CoreHubModule.forRoot({ config: { mailbox: { mode: 'capture' } }, ctx });
|
|
18
|
+
expect(mod.global).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('is not global when the mailbox is disabled', () => {
|
|
22
|
+
const mod = CoreHubModule.forRoot({ config: {}, ctx });
|
|
23
|
+
expect(mod.global).toBeFalsy();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('throws when mailbox capture mode is used in production (silent mail suppression guard)', () => {
|
|
27
|
+
expect(() =>
|
|
28
|
+
CoreHubModule.forRoot({ config: { mailbox: { mode: 'capture' } }, ctx: { env: 'production', version: '1' } }),
|
|
29
|
+
).toThrow(/capture.*not permitted/i);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('allows mailbox copy mode in production', () => {
|
|
33
|
+
expect(() =>
|
|
34
|
+
CoreHubModule.forRoot({ config: { mailbox: { mode: 'copy' } }, ctx: { env: 'production', version: '1' } }),
|
|
35
|
+
).not.toThrow();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('public-access guard (roles: false in a reachable env)', () => {
|
|
39
|
+
it('throws when roles:false in production without acknowledgment', () => {
|
|
40
|
+
expect(() =>
|
|
41
|
+
CoreHubModule.forRoot({ config: { roles: false }, ctx: { env: 'production', version: '1' } }),
|
|
42
|
+
).toThrow(/roles: false.*not permitted/i);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('throws when roles:false in staging without acknowledgment', () => {
|
|
46
|
+
expect(() => CoreHubModule.forRoot({ config: { roles: false }, ctx: { env: 'staging', version: '1' } })).toThrow(
|
|
47
|
+
/not permitted/i,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('throws for CUSTOM reachable env names too (fail-safe — not just production/staging)', () => {
|
|
52
|
+
// Regression guard for the exact-string bypass: prod / live / preprod / production-eu / staging-2
|
|
53
|
+
// must all be treated as reachable.
|
|
54
|
+
for (const env of ['prod', 'live', 'preprod', 'production-eu', 'staging-2']) {
|
|
55
|
+
expect(() => CoreHubModule.forRoot({ config: { roles: false }, ctx: { env, version: '1' } })).toThrow(
|
|
56
|
+
/not permitted/i,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('allows roles:false in production ONLY with explicit allowPublicAccessInProduction', () => {
|
|
62
|
+
expect(() =>
|
|
63
|
+
CoreHubModule.forRoot({
|
|
64
|
+
config: { allowPublicAccessInProduction: true, roles: false },
|
|
65
|
+
ctx: { env: 'production', version: '1' },
|
|
66
|
+
}),
|
|
67
|
+
).not.toThrow();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('allows roles:false in non-reachable envs (local, development)', () => {
|
|
71
|
+
expect(() => CoreHubModule.forRoot({ config: { roles: false }, ctx })).not.toThrow(); // ctx = local
|
|
72
|
+
expect(() =>
|
|
73
|
+
CoreHubModule.forRoot({ config: { roles: false }, ctx: { env: 'development', version: '1' } }),
|
|
74
|
+
).not.toThrow();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('allows the default ADMIN gate in production (no roles set)', () => {
|
|
78
|
+
expect(() => CoreHubModule.forRoot({ config: {}, ctx: { env: 'production', version: '1' } })).not.toThrow();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('runtime path/roles metadata (shared controller class)', () => {
|
|
83
|
+
it('writes the configured base path onto the controller class', () => {
|
|
84
|
+
CoreHubModule.forRoot({ config: { path: 'cockpit' }, ctx });
|
|
85
|
+
expect(Reflect.getMetadata(PATH_METADATA, CoreHubController)).toBe('cockpit');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('fully overwrites stale metadata on a second forRoot (no cross-instance pollution)', () => {
|
|
89
|
+
// The controller class is shared across app instances in the same process (parallel tests). A
|
|
90
|
+
// second forRoot must overwrite the first, never leak the previous path/roles.
|
|
91
|
+
CoreHubModule.forRoot({ config: { path: 'first', roles: 'admin' }, ctx });
|
|
92
|
+
expect(Reflect.getMetadata(PATH_METADATA, CoreHubController)).toBe('first');
|
|
93
|
+
expect(Reflect.getMetadata('roles', CoreHubController)).toEqual(['admin']);
|
|
94
|
+
|
|
95
|
+
CoreHubModule.forRoot({ config: { path: 'second', roles: ['ops', 'admin'] }, ctx });
|
|
96
|
+
expect(Reflect.getMetadata(PATH_METADATA, CoreHubController)).toBe('second');
|
|
97
|
+
expect(Reflect.getMetadata('roles', CoreHubController)).toEqual(['ops', 'admin']);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('clears the roles metadata when roles: false (public opt-out)', () => {
|
|
101
|
+
CoreHubModule.forRoot({ config: { roles: 'admin' }, ctx });
|
|
102
|
+
expect(Reflect.getMetadata('roles', CoreHubController)).toEqual(['admin']);
|
|
103
|
+
|
|
104
|
+
CoreHubModule.forRoot({ config: { roles: false }, ctx });
|
|
105
|
+
expect(Reflect.getMetadata('roles', CoreHubController)).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { DynamicModule, MiddlewareConsumer, Module, NestModule, Provider, Type } from '@nestjs/common';
|
|
2
|
+
import { PATH_METADATA } from '@nestjs/common/constants';
|
|
3
|
+
|
|
4
|
+
import { CoreHubActionsController } from './core-hub-actions.controller';
|
|
5
|
+
import { CoreHubHtmlService } from './core-hub-html.service';
|
|
6
|
+
import { CoreHubController } from './core-hub.controller';
|
|
7
|
+
import { CoreHubService } from './core-hub.service';
|
|
8
|
+
import { HUB_CONFIG, HUB_EMAIL_CAPTURE } from './hub.constants';
|
|
9
|
+
import { HubConfigContext, isReachableEnv, normalizeHubConfig } from './hub-config.helper';
|
|
10
|
+
import { HubTraceMiddleware } from './middleware/hub-trace.middleware';
|
|
11
|
+
import { CoreHubActionsService } from './services/core-hub-actions.service';
|
|
12
|
+
import { CoreHubDbService } from './services/core-hub-db.service';
|
|
13
|
+
import { CoreHubEmailService } from './services/core-hub-email.service';
|
|
14
|
+
import { CoreHubMailboxService } from './services/core-hub-mailbox.service';
|
|
15
|
+
import { CoreHubMigrationsService } from './services/core-hub-migrations.service';
|
|
16
|
+
import { CoreHubSourcesService } from './services/core-hub-sources.service';
|
|
17
|
+
import { HubLogBufferService } from './services/hub-log-buffer.service';
|
|
18
|
+
import { HubQueryProfilerService } from './services/hub-query-profiler.service';
|
|
19
|
+
import { HubTraceBufferService } from './services/hub-trace-buffer.service';
|
|
20
|
+
import { IHubConfig } from './interfaces/hub-config.interface';
|
|
21
|
+
|
|
22
|
+
/** Options passed by `CoreModule.forRoot()` when it registers the Hub. */
|
|
23
|
+
export interface CoreHubModuleOptions {
|
|
24
|
+
/** Override the actions controller (must extend `CoreHubActionsController`). */
|
|
25
|
+
actionsController?: Type<any>;
|
|
26
|
+
/** Override the actions service (must extend `CoreHubActionsService`). */
|
|
27
|
+
actionsService?: Type<any>;
|
|
28
|
+
/** Raw hub config from `IServerOptions.hub`. */
|
|
29
|
+
config: boolean | IHubConfig;
|
|
30
|
+
/** Override the page/sidecar controller (must extend `CoreHubController`). */
|
|
31
|
+
controller?: Type<any>;
|
|
32
|
+
/** Environment-derived defaults (env name, version, GraphQL/permissions link resolution). */
|
|
33
|
+
ctx: HubConfigContext;
|
|
34
|
+
/** Override the HTML shell service (must extend `CoreHubHtmlService`). */
|
|
35
|
+
htmlService?: Type<any>;
|
|
36
|
+
/** Override the aggregator service (must extend `CoreHubService`). */
|
|
37
|
+
service?: Type<any>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The Hub admin area (operator cockpit).
|
|
42
|
+
*
|
|
43
|
+
* Registered by `CoreModule.forRoot()` when `IServerOptions.hub` is enabled. The controller path and
|
|
44
|
+
* required roles are assigned to the controller class at runtime (via `Reflect.defineMetadata`)
|
|
45
|
+
* because they come from configuration — the same pattern the permissions module uses.
|
|
46
|
+
*/
|
|
47
|
+
@Module({})
|
|
48
|
+
export class CoreHubModule implements NestModule {
|
|
49
|
+
// Write-once-per-forRoot (same accepted pattern as CoreModule.graphQlEnabled). Read by configure()
|
|
50
|
+
// to decide whether to bind the trace middleware. The last forRoot wins in multi-app processes.
|
|
51
|
+
private static traceEnabled = false;
|
|
52
|
+
|
|
53
|
+
configure(consumer: MiddlewareConsumer): void {
|
|
54
|
+
if (CoreHubModule.traceEnabled) {
|
|
55
|
+
// Bind before RequestContextMiddleware so timing starts early; the trace path never reads
|
|
56
|
+
// RequestContext (it uses req.user directly), so ordering is a non-issue.
|
|
57
|
+
consumer.apply(HubTraceMiddleware).forRoutes('*');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static forRoot(options: CoreHubModuleOptions): DynamicModule {
|
|
62
|
+
const resolved = normalizeHubConfig(options.config, options.ctx);
|
|
63
|
+
CoreHubModule.traceEnabled = resolved.collectors.traces !== false;
|
|
64
|
+
|
|
65
|
+
// Guard: capture mode silently suppresses outgoing mail — forbid it in production/staging so a
|
|
66
|
+
// misconfiguration cannot make password-reset, 2FA and verification mail vanish (mirrors the
|
|
67
|
+
// JSONTransport guard in EmailService).
|
|
68
|
+
if (resolved.mailbox !== false && resolved.mailbox.mode === 'capture' && isReachableEnv(resolved.env)) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Hub mailbox mode 'capture' is not permitted in a reachable environment (${resolved.env}): it ` +
|
|
71
|
+
"suppresses all outgoing mail. Use mode 'copy' (send + record) or disable the mailbox.",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Guard: a PUBLIC Hub (roles: false → no auth check whatsoever) in a reachable environment exposes
|
|
76
|
+
// the config viewer, logs and DESTRUCTIVE admin actions (migrations, file delete, cron control) to
|
|
77
|
+
// ANY unauthenticated request. Forbid it in production/staging unless the operator has EXPLICITLY
|
|
78
|
+
// acknowledged it via `allowPublicAccessInProduction` (only sane behind a fully-controlled network
|
|
79
|
+
// boundary). This mirrors the mailbox-capture guard above and closes the single most dangerous
|
|
80
|
+
// Hub misconfiguration (e.g. a `roles: false` copied from a local config).
|
|
81
|
+
if (resolved.roles === false && isReachableEnv(resolved.env) && !resolved.allowPublicAccessInProduction) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Hub 'roles: false' (no auth check) is not permitted in a reachable environment (${resolved.env}): ` +
|
|
84
|
+
'it exposes the config viewer, logs and destructive admin actions to unauthenticated requests. ' +
|
|
85
|
+
'Keep the default ADMIN gate (omit `roles`), or — ONLY behind a fully-controlled network ' +
|
|
86
|
+
'boundary — set hub.allowPublicAccessInProduction: true to acknowledge the risk.',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const ControllerClass = options.controller || CoreHubController;
|
|
91
|
+
const ActionsControllerClass = options.actionsController || CoreHubActionsController;
|
|
92
|
+
|
|
93
|
+
// Runtime path + roles metadata (see permissions module), applied to BOTH controllers. Written
|
|
94
|
+
// UNCONDITIONALLY so a second forRoot on the same class fully overrides the first — otherwise
|
|
95
|
+
// stale metadata leaks across app instances that share the class (a real hazard in multi-app
|
|
96
|
+
// tests). `roles: false` clears the metadata, which the guards read as "public" — a deliberate,
|
|
97
|
+
// dangerous opt-out (documented in IHubConfig.roles).
|
|
98
|
+
for (const Ctrl of resolved.actions ? [ControllerClass, ActionsControllerClass] : [ControllerClass]) {
|
|
99
|
+
Reflect.defineMetadata(PATH_METADATA, resolved.path, Ctrl);
|
|
100
|
+
if (resolved.roles === false) {
|
|
101
|
+
Reflect.deleteMetadata('roles', Ctrl);
|
|
102
|
+
} else {
|
|
103
|
+
Reflect.defineMetadata('roles', resolved.roles, Ctrl);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const providers: Provider[] = [
|
|
108
|
+
{ provide: HUB_CONFIG, useValue: resolved },
|
|
109
|
+
{ provide: CoreHubService, useClass: options.service || CoreHubService },
|
|
110
|
+
{ provide: CoreHubHtmlService, useClass: options.htmlService || CoreHubHtmlService },
|
|
111
|
+
CoreHubDbService,
|
|
112
|
+
CoreHubEmailService,
|
|
113
|
+
CoreHubMigrationsService,
|
|
114
|
+
CoreHubSourcesService,
|
|
115
|
+
// Collectors are always provided; each is inert when its own config slice is disabled, so the
|
|
116
|
+
// sidecar can answer `{ enabled: false }` rather than 404.
|
|
117
|
+
HubLogBufferService,
|
|
118
|
+
HubQueryProfilerService,
|
|
119
|
+
HubTraceBufferService,
|
|
120
|
+
HubTraceMiddleware,
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
// Actions controller/service only when actions are enabled (disabling removes the routes → 404).
|
|
124
|
+
const controllers: Type<any>[] = [ControllerClass];
|
|
125
|
+
if (resolved.actions) {
|
|
126
|
+
controllers.push(ActionsControllerClass);
|
|
127
|
+
providers.push({ provide: CoreHubActionsService, useClass: options.actionsService || CoreHubActionsService });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// When the mailbox is enabled, provide it AND bind it to the EMAIL_CAPTURE token so EmailService
|
|
131
|
+
// (which injects the token optionally) starts capturing. The module must be global for the token
|
|
132
|
+
// to reach EmailService, which lives in a different module.
|
|
133
|
+
const global = resolved.mailbox !== false;
|
|
134
|
+
if (global) {
|
|
135
|
+
providers.push(CoreHubMailboxService);
|
|
136
|
+
providers.push({ provide: HUB_EMAIL_CAPTURE, useExisting: CoreHubMailboxService });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
controllers,
|
|
141
|
+
exports: [
|
|
142
|
+
CoreHubService,
|
|
143
|
+
CoreHubHtmlService,
|
|
144
|
+
CoreHubDbService,
|
|
145
|
+
CoreHubEmailService,
|
|
146
|
+
CoreHubMigrationsService,
|
|
147
|
+
CoreHubSourcesService,
|
|
148
|
+
HubLogBufferService,
|
|
149
|
+
HubQueryProfilerService,
|
|
150
|
+
HubTraceBufferService,
|
|
151
|
+
HUB_CONFIG,
|
|
152
|
+
...(global ? [CoreHubMailboxService, HUB_EMAIL_CAPTURE] : []),
|
|
153
|
+
],
|
|
154
|
+
global,
|
|
155
|
+
module: CoreHubModule,
|
|
156
|
+
providers,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|