@lenne.tech/nest-server 11.31.1 → 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/ai/core-ai.controller.d.ts +1 -1
- package/dist/core/modules/ai/core-ai.controller.js +4 -5
- package/dist/core/modules/ai/core-ai.controller.js.map +1 -1
- package/dist/core/modules/ai/core-ai.resolver.d.ts +1 -1
- package/dist/core/modules/ai/core-ai.resolver.js +7 -6
- package/dist/core/modules/ai/core-ai.resolver.js.map +1 -1
- package/dist/core/modules/ai/models/core-ai-conversation.model.d.ts +6 -0
- package/dist/core/modules/ai/models/core-ai-conversation.model.js +13 -1
- package/dist/core/modules/ai/models/core-ai-conversation.model.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +8 -2
- package/dist/core/modules/ai/services/core-ai-conversation.service.js +53 -2
- package/dist/core/modules/ai/services/core-ai-conversation.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.1-to-11.31.2.md +127 -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/ai/core-ai.controller.ts +12 -9
- package/src/core/modules/ai/core-ai.resolver.ts +14 -9
- package/src/core/modules/ai/models/core-ai-conversation.model.ts +18 -1
- package/src/core/modules/ai/services/core-ai-conversation.service.ts +110 -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 +6 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
-
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
|
2
|
+
import { Inject, Injectable, OnModuleDestroy, Optional } from '@nestjs/common';
|
|
3
3
|
import nodemailer = require('nodemailer');
|
|
4
4
|
import { Attachment } from 'nodemailer/lib/mailer';
|
|
5
5
|
|
|
6
6
|
import { isNonEmptyString, isTrue, returnFalse } from '../helpers/input.helper';
|
|
7
7
|
import { MailTransportOptions } from '../interfaces/server-options.interface';
|
|
8
|
+
import { HUB_EMAIL_CAPTURE } from '../../modules/hub/hub.constants';
|
|
8
9
|
import { ConfigService } from './config.service';
|
|
9
10
|
import { TemplateService } from './template.service';
|
|
11
|
+
import type { IHubEmailCapture } from '../../modules/hub/interfaces/hub-config.interface';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Email service
|
|
@@ -26,6 +28,9 @@ export class EmailService implements OnModuleDestroy {
|
|
|
26
28
|
constructor(
|
|
27
29
|
protected configService: ConfigService,
|
|
28
30
|
protected templateService: TemplateService,
|
|
31
|
+
// Optional Hub mailbox hook. When the Hub mailbox is enabled it captures outgoing mail (and, in
|
|
32
|
+
// capture mode, suppresses the actual send). Undefined otherwise — zero cost.
|
|
33
|
+
@Optional() @Inject(HUB_EMAIL_CAPTURE) protected readonly emailCapture?: IHubEmailCapture,
|
|
29
34
|
) {}
|
|
30
35
|
|
|
31
36
|
onModuleDestroy(): void {
|
|
@@ -104,6 +109,33 @@ export class EmailService implements OnModuleDestroy {
|
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
111
|
|
|
112
|
+
// Hub mailbox capture (Mailpit-style). Runs after templates are rendered, before the transport.
|
|
113
|
+
// In capture mode it records the mail and suppresses the send (returns a jsonTransport-like ack).
|
|
114
|
+
// Fully guarded: a broken mailbox hook must never break (or crash) the mail path.
|
|
115
|
+
if (this.emailCapture) {
|
|
116
|
+
let skipTransport = false;
|
|
117
|
+
try {
|
|
118
|
+
skipTransport = this.emailCapture.capture({
|
|
119
|
+
from: `"${senderName}" <${senderEmail}>`,
|
|
120
|
+
html,
|
|
121
|
+
subject,
|
|
122
|
+
templateName: htmlTemplate ?? textTemplate,
|
|
123
|
+
text,
|
|
124
|
+
to: Array.isArray(recipients) ? recipients.join(', ') : recipients,
|
|
125
|
+
});
|
|
126
|
+
} catch {
|
|
127
|
+
/* mailbox capture must never break mail sending */
|
|
128
|
+
}
|
|
129
|
+
if (skipTransport) {
|
|
130
|
+
return {
|
|
131
|
+
accepted: Array.isArray(recipients) ? recipients : [recipients],
|
|
132
|
+
captured: true,
|
|
133
|
+
messageId: 'hub-mailbox',
|
|
134
|
+
rejected: [],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
107
139
|
// Reuse transporter if SMTP config hasn't changed (avoids creating new connections per email)
|
|
108
140
|
// Use hash instead of raw JSON to avoid keeping credentials as a string in memory
|
|
109
141
|
const smtpKey = createHash('sha256').update(JSON.stringify(smtp)).digest('hex');
|
|
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|
|
2
2
|
import { TemplateFunction } from 'ejs';
|
|
3
3
|
import ejs = require('ejs');
|
|
4
4
|
import fs = require('fs');
|
|
5
|
-
import {
|
|
5
|
+
import { resolve, sep } from 'path';
|
|
6
6
|
|
|
7
7
|
import { ConfigService } from './config.service';
|
|
8
8
|
|
|
@@ -36,27 +36,32 @@ export class TemplateService {
|
|
|
36
36
|
* @param filePath Directory names (separated via '/' if template is in subdirectory) + name of the template file without extension
|
|
37
37
|
*/
|
|
38
38
|
protected async getTemplate(filePath: string): Promise<TemplateFunction> {
|
|
39
|
-
|
|
39
|
+
// Resolve the template file to an absolute path and enforce that it stays within the configured
|
|
40
|
+
// templates directory. Legitimate template names never contain '..'; this guard is a defense in
|
|
41
|
+
// depth against path traversal for any caller that forwards user-influenced template names.
|
|
42
|
+
const baseDir = resolve(this.configService.getFastButReadOnly('templates.path'));
|
|
43
|
+
const fullPath = resolve(baseDir, `${filePath}.ejs`);
|
|
44
|
+
if (fullPath !== baseDir && !fullPath.startsWith(baseDir + sep)) {
|
|
45
|
+
throw new Error(`Invalid template path "${filePath}".`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return new Promise<TemplateFunction>((res, reject) => {
|
|
40
49
|
// Get template from cache
|
|
41
50
|
if (this.templates[filePath]) {
|
|
42
|
-
|
|
51
|
+
res(this.templates[filePath]);
|
|
43
52
|
return;
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
// Get template file
|
|
47
|
-
fs.readFile(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
resolve(this.templates[filePath]);
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
);
|
|
56
|
+
fs.readFile(fullPath, { encoding: 'utf8' }, (err, data) => {
|
|
57
|
+
if (err) {
|
|
58
|
+
reject(err);
|
|
59
|
+
} else {
|
|
60
|
+
// Compile and return template
|
|
61
|
+
this.templates[filePath] = ejs.compile(data);
|
|
62
|
+
res(this.templates[filePath]);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
60
65
|
});
|
|
61
66
|
}
|
|
62
67
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Body, Controller, Delete, Get, Param, Post, Put, Res } from '@nestjs/common';
|
|
1
|
+
import { Body, Controller, Delete, Get, Param, Post, Put, Query, Res } from '@nestjs/common';
|
|
2
2
|
import { Response } from 'express';
|
|
3
3
|
|
|
4
4
|
import { RESTServiceOptions } from '../../common/decorators/rest-service-options.decorator';
|
|
@@ -266,7 +266,9 @@ export class CoreAiController {
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
/**
|
|
269
|
-
* Find the current user's AI conversations (
|
|
269
|
+
* Find the current user's AI conversations (own only by default). An admin may
|
|
270
|
+
* pass `?all=true` to list every user's conversations; each result carries its
|
|
271
|
+
* `createdBy` owner id for attribution. The flag is ignored for non-admins.
|
|
270
272
|
*
|
|
271
273
|
* The `messages` subdocument array is excluded from the list result — clients
|
|
272
274
|
* fetching the conversation detail via `getConversation` get the full message
|
|
@@ -274,13 +276,14 @@ export class CoreAiController {
|
|
|
274
276
|
*/
|
|
275
277
|
@Get('conversations')
|
|
276
278
|
@Roles(RoleEnum.S_USER)
|
|
277
|
-
async findConversations(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
279
|
+
async findConversations(
|
|
280
|
+
@RESTServiceOptions() serviceOptions: ServiceOptions,
|
|
281
|
+
@Query('all') all?: string,
|
|
282
|
+
): Promise<CoreAiConversation[]> {
|
|
283
|
+
// Owner-scoped list shared with the GraphQL resolver — see
|
|
284
|
+
// CoreAiConversationService.findForCurrentUser for the role/ownership rationale.
|
|
285
|
+
// Admins default to their own conversations and opt in to the cross-user view via ?all=true.
|
|
286
|
+
return this.conversationService.findForCurrentUser(serviceOptions, { all: all === 'true' });
|
|
284
287
|
}
|
|
285
288
|
|
|
286
289
|
/**
|
|
@@ -270,21 +270,26 @@ export class CoreAiResolver {
|
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
/**
|
|
273
|
-
* Find the current user's AI conversations (
|
|
273
|
+
* Find the current user's AI conversations (own only by default). An admin may
|
|
274
|
+
* pass `all: true` to list every user's conversations; each result carries its
|
|
275
|
+
* `createdBy` owner id for attribution. The argument is ignored for non-admins.
|
|
274
276
|
*
|
|
275
277
|
* The `messages` subdocument array is excluded from the list result — clients
|
|
276
278
|
* fetching the conversation detail via `getAiConversation` get the full message
|
|
277
279
|
* history. List payloads stay small even for users with many long conversations.
|
|
278
280
|
*/
|
|
279
|
-
@Query(() => [CoreAiConversation], {
|
|
281
|
+
@Query(() => [CoreAiConversation], {
|
|
282
|
+
description: "Find AI conversations of the current user (admins may pass all: true to see every user's)",
|
|
283
|
+
})
|
|
280
284
|
@Roles(RoleEnum.S_USER)
|
|
281
|
-
async findAiConversations(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
285
|
+
async findAiConversations(
|
|
286
|
+
@GraphQLServiceOptions() serviceOptions: ServiceOptions,
|
|
287
|
+
@Args('all', { nullable: true, type: () => Boolean }) all?: boolean,
|
|
288
|
+
): Promise<CoreAiConversation[]> {
|
|
289
|
+
// Owner-scoped list shared with the REST controller — see
|
|
290
|
+
// CoreAiConversationService.findForCurrentUser for the role/ownership rationale.
|
|
291
|
+
// Admins default to their own conversations and opt in to the cross-user view via all: true.
|
|
292
|
+
return this.conversationService.findForCurrentUser(serviceOptions, { all: all === true });
|
|
288
293
|
}
|
|
289
294
|
|
|
290
295
|
/**
|
|
@@ -6,6 +6,7 @@ import { Restricted } from '../../../common/decorators/restricted.decorator';
|
|
|
6
6
|
import { UnifiedField } from '../../../common/decorators/unified-field.decorator';
|
|
7
7
|
import { RoleEnum } from '../../../common/enums/role.enum';
|
|
8
8
|
import { CorePersistenceModel } from '../../../common/models/core-persistence.model';
|
|
9
|
+
import { JSON } from '../../../common/scalars/json.scalar';
|
|
9
10
|
import { CoreAiMessage } from './core-ai-message.model';
|
|
10
11
|
|
|
11
12
|
export type AiConversationDocument = CoreAiConversation & Document;
|
|
@@ -45,6 +46,22 @@ export class CoreAiConversation extends CorePersistenceModel {
|
|
|
45
46
|
})
|
|
46
47
|
createdBy?: string = undefined;
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Resolved creator identity (`{ id, email, firstName, lastName, username }`),
|
|
51
|
+
* attached at read time ONLY for an admin's cross-user list view (`all: true`)
|
|
52
|
+
* so each conversation is attributable to a named user. Not persisted and left
|
|
53
|
+
* unset on a normal own-only fetch, where the owner is the caller.
|
|
54
|
+
*/
|
|
55
|
+
@UnifiedField({
|
|
56
|
+
description:
|
|
57
|
+
'Resolved creator identity (id, email, name) — set only in the admin cross-user list view, not persisted',
|
|
58
|
+
gqlType: JSON,
|
|
59
|
+
isOptional: true,
|
|
60
|
+
roles: RoleEnum.S_USER,
|
|
61
|
+
type: () => Object,
|
|
62
|
+
})
|
|
63
|
+
createdByUser?: Record<string, any> = undefined;
|
|
64
|
+
|
|
48
65
|
/**
|
|
49
66
|
* Conversation messages (appended via $push).
|
|
50
67
|
*/
|
|
@@ -76,7 +93,7 @@ export class CoreAiConversation extends CorePersistenceModel {
|
|
|
76
93
|
if (force) {
|
|
77
94
|
return this;
|
|
78
95
|
}
|
|
79
|
-
if (user && (user.hasRole?.(RoleEnum.ADMIN) || (this.createdBy && String(this.createdBy) === user.id))) {
|
|
96
|
+
if (user && (user.hasRole?.(RoleEnum.ADMIN) || (this.createdBy && String(this.createdBy) === String(user.id)))) {
|
|
80
97
|
return this;
|
|
81
98
|
}
|
|
82
99
|
// Hide conversations owned by other users (filtered out of list responses).
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Inject, Injectable } from '@nestjs/common';
|
|
2
|
-
import { InjectModel } from '@nestjs/mongoose';
|
|
3
|
-
import { Model, Types } from 'mongoose';
|
|
2
|
+
import { InjectConnection, InjectModel } from '@nestjs/mongoose';
|
|
3
|
+
import { Connection, Model, Types } from 'mongoose';
|
|
4
4
|
|
|
5
|
+
import { RoleEnum } from '../../../common/enums/role.enum';
|
|
6
|
+
import { ServiceOptions } from '../../../common/interfaces/service-options.interface';
|
|
5
7
|
import { CrudService } from '../../../common/services/crud.service';
|
|
6
8
|
import { CoreModelConstructor } from '../../../common/types/core-model-constructor.type';
|
|
7
9
|
import { CoreAiConversationCreateInput } from '../inputs/core-ai-conversation-create.input';
|
|
@@ -21,8 +23,15 @@ export { AI_CONVERSATION_CLASS, AI_CONVERSATION_MODEL } from '../core-ai.constan
|
|
|
21
23
|
* CRUD service for multi-turn {@link CoreAiConversation}s.
|
|
22
24
|
*
|
|
23
25
|
* `appendMessage()` adds a turn via `$push` (never round-trips the subdocument
|
|
24
|
-
* array through `update()`).
|
|
25
|
-
*
|
|
26
|
+
* array through `update()`). `findForCurrentUser()` is the shared owner-scoped
|
|
27
|
+
* list used by both the REST controller and the GraphQL resolver.
|
|
28
|
+
*
|
|
29
|
+
* Ownership is enforced differently per operation shape:
|
|
30
|
+
* - single-object `get`/`delete`: the controller/resolver pass
|
|
31
|
+
* `S_CREATOR`/`S_SELF`, evaluated against the loaded `dbObject`.
|
|
32
|
+
* - list (`findForCurrentUser`): a list has no single `dbObject`, so the gate
|
|
33
|
+
* uses `S_USER` and ownership is scoped by the server-computed `createdBy`
|
|
34
|
+
* filterQuery plus the model's `securityCheck`.
|
|
26
35
|
*/
|
|
27
36
|
@Injectable()
|
|
28
37
|
export class CoreAiConversationService extends CrudService<
|
|
@@ -37,10 +46,107 @@ export class CoreAiConversationService extends CrudService<
|
|
|
37
46
|
@InjectModel(AI_CONVERSATION_MODEL) protected override readonly mainDbModel: Model<AiConversationDocument>,
|
|
38
47
|
@Inject(AI_CONVERSATION_CLASS)
|
|
39
48
|
protected override readonly mainModelConstructor: CoreModelConstructor<CoreAiConversation>,
|
|
49
|
+
@InjectConnection() protected readonly connection: Connection,
|
|
40
50
|
) {
|
|
41
51
|
super();
|
|
42
52
|
}
|
|
43
53
|
|
|
54
|
+
/**
|
|
55
|
+
* List the AI conversations visible to the current user. By default — for
|
|
56
|
+
* every user, admins included — this returns only the caller's own
|
|
57
|
+
* conversations, newest first. An admin may opt in to the cross-user view
|
|
58
|
+
* (every user's conversations) by passing `{ all: true }`; the flag is
|
|
59
|
+
* ignored for non-admins, so it can never widen a regular user's scope.
|
|
60
|
+
*
|
|
61
|
+
* Each result carries its `createdBy` owner id. In the admin cross-user view
|
|
62
|
+
* (`all: true`) each result additionally carries a resolved `createdByUser`
|
|
63
|
+
* (`{ id, email, firstName, lastName, username }`) so the list is attributable
|
|
64
|
+
* to named users; on a normal own-only fetch that lookup is skipped, since the
|
|
65
|
+
* owner is the caller. The heavy `messages` subdocument array is excluded from
|
|
66
|
+
* the list payload — fetch a single conversation via `get()` for the full
|
|
67
|
+
* message history.
|
|
68
|
+
*
|
|
69
|
+
* Shared by the REST controller (`GET /ai/conversations`) and the GraphQL
|
|
70
|
+
* resolver (`findAiConversations`) so both API surfaces enforce ownership
|
|
71
|
+
* identically and cannot drift apart.
|
|
72
|
+
*
|
|
73
|
+
* A LIST operation has no single `dbObject`, so the per-document roles
|
|
74
|
+
* `S_CREATOR` / `S_SELF` can never be satisfied at the operation level —
|
|
75
|
+
* passing them rejected every non-admin with a 403 before ownership scoping
|
|
76
|
+
* ran. The operation gate therefore uses `[S_USER]`; the server-computed
|
|
77
|
+
* `createdBy` filterQuery and the model `securityCheck` scope the result to
|
|
78
|
+
* the owner.
|
|
79
|
+
*/
|
|
80
|
+
async findForCurrentUser(
|
|
81
|
+
serviceOptions?: ServiceOptions,
|
|
82
|
+
options?: { all?: boolean },
|
|
83
|
+
): Promise<CoreAiConversation[]> {
|
|
84
|
+
const currentUser = serviceOptions?.currentUser;
|
|
85
|
+
const isAdmin = !!currentUser?.roles?.includes(RoleEnum.ADMIN);
|
|
86
|
+
// Default is own-only for everyone; only an admin may opt in to all users' conversations.
|
|
87
|
+
const seeAll = isAdmin && options?.all === true;
|
|
88
|
+
const filterQuery = seeAll ? {} : { createdBy: currentUser?.id };
|
|
89
|
+
const conversations = await this.find(
|
|
90
|
+
{ filterQuery, queryOptions: { sort: { createdAt: -1 } } },
|
|
91
|
+
{ ...serviceOptions, roles: [RoleEnum.S_USER], select: '-messages' },
|
|
92
|
+
);
|
|
93
|
+
// Resolve creators to named users ONLY for the admin cross-user view — a normal
|
|
94
|
+
// fetch returns just the caller's own conversations, so the owner is already known.
|
|
95
|
+
if (seeAll && conversations.length) {
|
|
96
|
+
await this.attachCreators(conversations);
|
|
97
|
+
}
|
|
98
|
+
return conversations;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Attach a lightweight resolved creator (`{ id, email, firstName, lastName,
|
|
103
|
+
* username }`) as `createdByUser` to each conversation, so an admin's
|
|
104
|
+
* cross-user list is attributable to named users. Read-only lookup of
|
|
105
|
+
* non-sensitive identity fields via the shared `User` model on the same
|
|
106
|
+
* connection; fails soft (leaves `createdByUser` unset) when no `User` model is
|
|
107
|
+
* registered. Never called on a normal own-only fetch.
|
|
108
|
+
*/
|
|
109
|
+
protected async attachCreators(conversations: CoreAiConversation[]): Promise<void> {
|
|
110
|
+
const ids = [
|
|
111
|
+
...new Set(
|
|
112
|
+
conversations
|
|
113
|
+
.map((conversation) => conversation.createdBy)
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.map(String),
|
|
116
|
+
),
|
|
117
|
+
];
|
|
118
|
+
if (!ids.length) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let userModel: Model<any>;
|
|
122
|
+
try {
|
|
123
|
+
userModel = this.connection.model('User');
|
|
124
|
+
} catch {
|
|
125
|
+
// No `User` model registered on this connection (exotic setup) — skip attribution.
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const users = await userModel
|
|
129
|
+
.find({ _id: { $in: ids } })
|
|
130
|
+
.select('email firstName lastName username')
|
|
131
|
+
.lean()
|
|
132
|
+
.exec();
|
|
133
|
+
const byId = new Map(
|
|
134
|
+
(users as any[]).map((user) => [
|
|
135
|
+
String(user._id),
|
|
136
|
+
{
|
|
137
|
+
email: user.email,
|
|
138
|
+
firstName: user.firstName,
|
|
139
|
+
id: String(user._id),
|
|
140
|
+
lastName: user.lastName,
|
|
141
|
+
username: user.username,
|
|
142
|
+
},
|
|
143
|
+
]),
|
|
144
|
+
);
|
|
145
|
+
for (const conversation of conversations) {
|
|
146
|
+
conversation.createdByUser = byId.get(String(conversation.createdBy));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
44
150
|
/**
|
|
45
151
|
* Append a message to a conversation via `$push` (system-internal; the user
|
|
46
152
|
* already authorized the prompt that produced it). The array is capped at
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Hub Integration Checklist
|
|
2
|
+
|
|
3
|
+
The Hub is **config-only** — no files need to be created in the consuming project. It is auto-registered
|
|
4
|
+
by `CoreModule.forRoot()` when `hub` is present in the config.
|
|
5
|
+
|
|
6
|
+
## Reference Implementation
|
|
7
|
+
|
|
8
|
+
- Local: `node_modules/@lenne.tech/nest-server/src/core/modules/hub/`
|
|
9
|
+
- The framework's own e2e config (`src/config.env.ts`) enables it in `local`/`development`/`e2e`/`ci`.
|
|
10
|
+
|
|
11
|
+
## Quick Setup
|
|
12
|
+
|
|
13
|
+
### 1. Enable per environment
|
|
14
|
+
|
|
15
|
+
**Edit:** `src/config.env.ts`
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// development / local
|
|
19
|
+
hub: {
|
|
20
|
+
collectors: { queries: true }, // opt-in query profiler
|
|
21
|
+
mailbox: { mode: 'capture' }, // capture outgoing mail locally (Mailpit replacement)
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// production — usually omit `hub` entirely, or (if you want it, still ADMIN-gated):
|
|
25
|
+
// hub: { collectors: { queries: false }, mailbox: false },
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That's it. Sign in as a user with `RoleEnum.ADMIN` and open `/hub`.
|
|
29
|
+
|
|
30
|
+
## Optional Enhancements
|
|
31
|
+
|
|
32
|
+
| Want | Do |
|
|
33
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
34
|
+
| **Cron panel populated** | Import `ScheduleModule.forRoot()` in your ServerModule (usually already present). |
|
|
35
|
+
| **Query profiler** | Set `hub.collectors.queries: true` (opts the driver into `monitorCommands`). |
|
|
36
|
+
| **Migrations run/rollback** | Set `hub.migrations.dir` to your migrations directory (default `./migrations`). In compiled deployments point it at the built JS. Use `lockCollectionName` for cluster safety. |
|
|
37
|
+
| **Custom look / behavior** | Pass `overrides.hub.{controller,actionsController,service,htmlService,actionsService}` to `CoreModule.forRoot()`. |
|
|
38
|
+
|
|
39
|
+
## Verification Checklist
|
|
40
|
+
|
|
41
|
+
- [ ] `pnpm run build` succeeds
|
|
42
|
+
- [ ] Sign in as ADMIN → `GET /hub` returns the dashboard HTML
|
|
43
|
+
- [ ] A non-admin user gets `403`; an anonymous request gets `401`
|
|
44
|
+
- [ ] `GET /hub/dashboard.json` returns build/memory/features
|
|
45
|
+
- [ ] `GET /hub/config.json` shows `***` for secrets (never the real values)
|
|
46
|
+
- [ ] Mutating actions without `X-Hub-Request: 1` return `403`
|
|
47
|
+
|
|
48
|
+
## Common Mistakes
|
|
49
|
+
|
|
50
|
+
| Mistake | Symptom | Fix |
|
|
51
|
+
| ------------------------------------------------ | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| `hub` not set in the target environment | `/hub` → 404 | Add `hub: true` (or an options object) to that env's config block |
|
|
53
|
+
| `mailbox.mode: 'capture'` in production | Startup error | Use `mode: 'copy'` or disable the mailbox in production — capture suppresses all mail |
|
|
54
|
+
| Query panel empty | queries collector off (default) | `hub.collectors.queries: true` |
|
|
55
|
+
| Cron panel empty | `ScheduleModule.forRoot()` not imported | Import it in your ServerModule |
|
|
56
|
+
| Hub path collides with a project route | 404 / wrong page | Set a distinct `hub.path` (e.g. `admin/hub`) |
|
|
57
|
+
| No roles guard registered (auth system disabled) | Every `/hub` sidecar/action returns 200 for anonymous requests | The Hub relies on the framework's `RolesGuard`/`BetterAuthRolesGuard` (registered by the auth/BetterAuth module) to enforce `@Roles(RoleEnum.ADMIN)`. If you disable BetterAuth AND legacy auth, no guard runs and the ADMIN gate is inert. Keep an auth module enabled, or do not expose the Hub. |
|
|
58
|
+
|
|
59
|
+
> **Auth dependency (why):** the Hub does not register its own guard — it assigns `@Roles(hub.roles)`
|
|
60
|
+
> (default `ADMIN`) as metadata and depends on the app-wide `RolesGuard` / `BetterAuthRolesGuard` to
|
|
61
|
+
> read it. That guard ships with the auth/BetterAuth modules. In a normal project one of them is
|
|
62
|
+
> active, so the gate works out of the box; but an app that runs with _no_ auth system has no guard
|
|
63
|
+
> to enforce the roles, and the sidecars/actions would be reachable unauthenticated. Either keep an
|
|
64
|
+
> auth module enabled or leave the Hub disabled in such a setup.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Hub — Admin Area (Operator Cockpit)
|
|
2
|
+
|
|
3
|
+
A build-free, ADMIN-gated dashboard of runtime information and admin tools, served directly by the
|
|
4
|
+
framework. Inspired by the sister project [nest-base](https://github.com/lenneTech/nest-base)'s Hub,
|
|
5
|
+
but adapted to this stack (NestJS + GraphQL + Mongoose) and shipped as **dependency-free server-side
|
|
6
|
+
HTML + a vanilla-JS SPA** (no React, no build step) so it works identically in npm- and vendor-mode.
|
|
7
|
+
|
|
8
|
+
## Enable it
|
|
9
|
+
|
|
10
|
+
The Hub is **never enabled implicitly** — switch it on per environment:
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// config.env.ts
|
|
14
|
+
{
|
|
15
|
+
hub: true, // enabled at /hub, admin-only, default collectors
|
|
16
|
+
// or, with options:
|
|
17
|
+
hub: {
|
|
18
|
+
path: 'hub', // default 'hub'
|
|
19
|
+
collectors: { queries: true }, // query profiler is opt-in (enables driver command monitoring)
|
|
20
|
+
mailbox: { mode: 'capture' }, // built-in Mailpit-style mail capture (dev/test)
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Do NOT set `hub` in the `production` block unless you intend the cockpit to be reachable there
|
|
26
|
+
(it stays ADMIN-gated). See `IHubConfig` in `interfaces/hub-config.interface.ts` for every option.
|
|
27
|
+
|
|
28
|
+
## Panels
|
|
29
|
+
|
|
30
|
+
| Panel | Route | Source |
|
|
31
|
+
| -------------------- | --------------------- | --------------------------------------------------------------- |
|
|
32
|
+
| Dashboard | `/hub` | health, build info, memory, feature matrix, links |
|
|
33
|
+
| Diagnostics | `/hub/diagnostics` | heap/rss, node/platform, collector buffer levels |
|
|
34
|
+
| Logs | `/hub/logs` | in-memory log ring buffer (redacted) |
|
|
35
|
+
| Request Traces | `/hub/traces` | HTTP timing middleware |
|
|
36
|
+
| Query Performance | `/hub/queries` | MongoDB driver command monitoring (opt-in) |
|
|
37
|
+
| Cron Jobs | `/hub/cron` | `@nestjs/schedule` `SchedulerRegistry` (optional) |
|
|
38
|
+
| Database | `/hub/db` | dbStats / per-collection collStats |
|
|
39
|
+
| Models / ERD | `/hub/models` | Mongoose schemas → Mermaid ER diagram |
|
|
40
|
+
| Migrations | `/hub/migrations` | `MigrationRunner` status + run/rollback |
|
|
41
|
+
| Files | `/hub/files` | GridFS listing + delete |
|
|
42
|
+
| Config | `/hub/config` | full config, secrets masked |
|
|
43
|
+
| Auth Migration | `/hub/auth-migration` | Legacy → IAM progress (BetterAuth, optional) |
|
|
44
|
+
| Routes / Permissions | `/hub/routes` | route + role + `@Restricted` map (Permissions module, optional) |
|
|
45
|
+
| Error Codes | `/hub/error-codes` | de/en catalog (ErrorCode module, optional) |
|
|
46
|
+
| Email Preview | `/hub/emails` | EJS templates rendered with sample data |
|
|
47
|
+
| Mailbox | `/hub/mailbox` | captured outgoing mail (Mailpit replacement) |
|
|
48
|
+
| AI | `/hub/ai` | AI usage summary (AI module, optional) |
|
|
49
|
+
|
|
50
|
+
Each panel has a `*.json` sidecar (the stable data contract) that the client polls. Optional sources
|
|
51
|
+
degrade to an "unavailable" state instead of erroring.
|
|
52
|
+
|
|
53
|
+
## Actions (mutating)
|
|
54
|
+
|
|
55
|
+
Enabled by default (`actions: true`). Every mutating request requires the `X-Hub-Request: 1` header
|
|
56
|
+
(CSRF defense) and destructive ones a server-validated `confirm` keyword:
|
|
57
|
+
|
|
58
|
+
| Action | Endpoint | Confirm |
|
|
59
|
+
| ----------------------- | ------------------------------------------ | ------------ |
|
|
60
|
+
| Run pending migrations | `POST /hub/actions/migrations/run` | `RUN` |
|
|
61
|
+
| Rollback last migration | `POST /hub/actions/migrations/down` | `DOWN` |
|
|
62
|
+
| Delete GridFS file | `DELETE /hub/actions/files/:id` | the filename |
|
|
63
|
+
| Cron start/stop/trigger | `POST /hub/actions/cron/:name/:action` | the job name |
|
|
64
|
+
| Clear collector buffer | `POST /hub/actions/collectors/:name/clear` | `CLEAR` |
|
|
65
|
+
| Send test mail | `POST /hub/actions/email/test` | — |
|
|
66
|
+
|
|
67
|
+
Every action writes an audit line: `[HUB-ACTION] <action> by user <id>`.
|
|
68
|
+
|
|
69
|
+
## Security
|
|
70
|
+
|
|
71
|
+
- **Auth**: the DATA sidecars (`*.json`) and all actions are `@Roles(RoleEnum.ADMIN)` (configurable
|
|
72
|
+
via `hub.roles`; `false` = public — dangerous). The **shell** (page routes) is public chrome only —
|
|
73
|
+
it shows a **login form** when the data is 401, so the Hub is self-sufficient: an admin can sign in
|
|
74
|
+
directly at the API (email/password → `loginEndpoint`, default `/iam/sign-in/email` → session cookie)
|
|
75
|
+
without the frontend, with a token-paste fallback for cookie-less setups. In fullstack, a
|
|
76
|
+
cross-subdomain session cookie from the app login already authenticates `/hub` — no separate login.
|
|
77
|
+
A **"Sign out"** button in the topbar POSTs to `hub.logoutEndpoint` (default `/iam/sign-out`),
|
|
78
|
+
clears the session cookie + any pasted token, and returns to the login gate. The logout endpoint is
|
|
79
|
+
delivered only in the ADMIN-gated `session.json` payload, never in the public shell.
|
|
80
|
+
- **Public-shell trade-off (by design):** because the shell must render before authentication, its
|
|
81
|
+
HTML source reveals the panel structure, the environment name (shown on the login card) and any
|
|
82
|
+
configured external links (`hub.links.*`) to an unauthenticated request. This is intentional — the
|
|
83
|
+
panel structure is already discoverable via the public `hub.js` — and carries no data: every
|
|
84
|
+
`*.json` sidecar and every action stays ADMIN-gated. Do not place secrets in `hub.links.*` (e.g.
|
|
85
|
+
an internal Mailpit URL) if the shell's origin is reachable by untrusted clients.
|
|
86
|
+
- **Action errors**: mutating actions return plain admin-facing messages (not `ErrorCode` catalog
|
|
87
|
+
entries) by design — the `ErrorCode` i18n module is optional and the Hub cannot hard-depend on it.
|
|
88
|
+
The messages are centralized in `hub-action-messages.ts` (`HubActionMessage`) so the wording stays
|
|
89
|
+
consistent and reviewable in one place.
|
|
90
|
+
- **CSP**: strict per-request nonce; no `unsafe-inline`; `X-Frame-Options: DENY`; `no-store`.
|
|
91
|
+
- **Secrets**: the config viewer deep-clones and masks by key pattern + `security.secretFields`.
|
|
92
|
+
- **Interceptor safety**: sidecars return pre-serialized JSON strings, so the global response
|
|
93
|
+
interceptors never walk/mutate live config.
|
|
94
|
+
- **Mailbox guard**: `mode: 'capture'` throws at startup in any **reachable** environment (anything
|
|
95
|
+
whose `env` is not `local`/`development`/`test`/`ci`/`e2e`) — it suppresses outgoing mail.
|
|
96
|
+
- **Public-access guard**: `roles: false` (no auth check — public config viewer, logs and destructive
|
|
97
|
+
actions) throws at startup in any **reachable** environment unless `hub.allowPublicAccessInProduction:
|
|
98
|
+
true` is set explicitly. This closes the single most dangerous Hub misconfiguration (a `roles: false`
|
|
99
|
+
copied from a local config into a reachable environment). The reachable check is fail-safe: it treats
|
|
100
|
+
every env name except the known local/test set as reachable, so a custom name (`prod`, `preprod`,
|
|
101
|
+
`staging-2`, …) cannot bypass it. Only acknowledge public access behind a fully-controlled network
|
|
102
|
+
boundary (VPN / IP allow-list / authenticating reverse proxy).
|
|
103
|
+
- **No-guard warning**: the Hub registers no guard of its own — its ADMIN gate is enforced by the
|
|
104
|
+
app-wide roles guard from BetterAuth (IAM) or the legacy Auth module. If the Hub is enabled and
|
|
105
|
+
gated but neither is active (and you have not registered your own `APP_GUARD`), `CoreModule` logs a
|
|
106
|
+
loud startup warning, because nothing would then enforce the gate.
|
|
107
|
+
- **CSRF**: mutating actions require the `X-Hub-Request` custom header, which forces a CORS preflight
|
|
108
|
+
that a restrictive allowlist rejects for foreign origins — **that header** (not the type-to-confirm
|
|
109
|
+
keyword, which is public UX safety, not a token) is the CSRF barrier. It holds with the default
|
|
110
|
+
`SameSite=Lax` session cookie or Bearer-token auth. Do NOT combine `cors.allowAll: true` with a
|
|
111
|
+
`SameSite=None` cookie (cross-subdomain fullstack): that lets any origin pass the preflight — pin
|
|
112
|
+
`cors` to your `appUrl` instead.
|
|
113
|
+
- **Redaction is best-effort (ADMIN-only surfaces)**: the config viewer masks by key pattern +
|
|
114
|
+
`security.secretFields` (a secret under an unusual key can slip); the logs/queries collectors and
|
|
115
|
+
the copy-mode mailbox redact patterned secrets (JWT / Bearer / `key=value` / cookie / reset-link
|
|
116
|
+
path tokens) but cannot catch an arbitrary value logged without a recognizable shape. All are
|
|
117
|
+
ADMIN-gated — treat Hub access as equivalent to config/log read access.
|
|
118
|
+
|
|
119
|
+
### Routes / Permissions panel vs. the standalone `/permissions` endpoint
|
|
120
|
+
|
|
121
|
+
The **Routes / Permissions** panel renders the same data as the standalone permissions module — the
|
|
122
|
+
full security map (every route + its required roles + `@Restricted` field rules) — but _inside_ the
|
|
123
|
+
Hub, so it inherits the Hub's ADMIN gate, per-environment opt-in and strict CSP. It reuses the
|
|
124
|
+
permissions **scanner** (`CorePermissionsService`), so it needs the `permissions` module enabled
|
|
125
|
+
(`config.permissions`); otherwise the panel degrades to an "unavailable" state.
|
|
126
|
+
|
|
127
|
+
The two surfaces are **independent** and answer the common "how do I reach it?" questions:
|
|
128
|
+
|
|
129
|
+
| Situation | How to reach the report |
|
|
130
|
+
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
131
|
+
| Hub **on**, `permissions` **on** | Hub → **Routes / Permissions** panel (ADMIN, login-gated by the Hub). The standalone `/permissions` also works per its own `role` (default ADMIN). |
|
|
132
|
+
| Hub **off**, `permissions` **on** (ADMIN) | The standalone `/permissions` is ADMIN-gated but has **no login page** — reach it with an ADMIN **session cookie** (be logged in via the app) or a **bearer token** (`Authorization: Bearer <jwt>` from `POST /iam/sign-in/email`), exactly like any other ADMIN endpoint. |
|
|
133
|
+
| Hub **off**, want frictionless **local** access | Set `permissions: { role: false }` — public, no auth. Legitimate **only** on a local, non-network-reachable machine, as a conscious opt-in. Never ship it to a reachable environment: the report is a reconnaissance goldmine. |
|
|
134
|
+
| `permissions` **off** | No report anywhere: `/permissions` is not registered (404) and the Hub panel shows "unavailable". |
|
|
135
|
+
|
|
136
|
+
**Security note:** the permissions report exposes your entire authorization model. Keep it ADMIN
|
|
137
|
+
(the default) everywhere it could be reachable; use `role: false` only behind a network boundary you
|
|
138
|
+
fully control. In production the framework's own config does not register the permissions module at all.
|
|
139
|
+
|
|
140
|
+
## Collectors
|
|
141
|
+
|
|
142
|
+
Three in-memory ring buffers (fixed capacity, no timers, per-app-instance — parallel-test-safe):
|
|
143
|
+
|
|
144
|
+
- **Logs** — installs a chaining `Logger.overrideLogger()` delegate (no main.ts change), restored on shutdown.
|
|
145
|
+
- **Traces** — an Express middleware registered only when enabled (zero cost otherwise).
|
|
146
|
+
- **Queries** — MongoDB driver command monitoring; records value-free query SHAPES (N+1 templates), never values.
|
|
147
|
+
Enabling it opts the driver into `monitorCommands` from `core.module.ts`.
|
|
148
|
+
|
|
149
|
+
## Overrides
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
CoreModule.forRoot(envConfig, {
|
|
153
|
+
hub: { service: MyHubService, htmlService: MyHubHtmlService },
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Fields: `controller`, `actionsController`, `service`, `htmlService`, `actionsService`.
|
|
158
|
+
|
|
159
|
+
See [INTEGRATION-CHECKLIST.md](./INTEGRATION-CHECKLIST.md) for setup in a consumer project.
|