@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,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { normalizeCommandShape } from './hub-command-shape.helper';
|
|
4
|
+
|
|
5
|
+
describe('normalizeCommandShape', () => {
|
|
6
|
+
it('replaces scalar values with a placeholder but keeps keys', () => {
|
|
7
|
+
const out = normalizeCommandShape({ email: 'alice@example.com', age: 30, active: true });
|
|
8
|
+
expect(out).toEqual({ active: '?', age: '?', email: '?' });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('keeps Mongo operators as keys (they are structure, not data)', () => {
|
|
12
|
+
const out = normalizeCommandShape({ age: { $gte: 18, $lt: 65 } });
|
|
13
|
+
expect(out).toEqual({ age: { $gte: '?', $lt: '?' } });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('collapses scalar arrays ($in) to a single placeholder', () => {
|
|
17
|
+
const out = normalizeCommandShape({ id: { $in: [1, 2, 3, 4] } });
|
|
18
|
+
expect(out).toEqual({ id: { $in: ['?'] } });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('keeps per-stage shape for pipeline-like object arrays', () => {
|
|
22
|
+
const out = normalizeCommandShape([{ $match: { x: 1 } }, { $group: { _id: '$y' } }]) as unknown[];
|
|
23
|
+
expect(out).toHaveLength(2);
|
|
24
|
+
expect(out[0]).toEqual({ $match: { x: '?' } });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('does NOT leak the original values (privacy)', () => {
|
|
28
|
+
const secret = 'super-secret@example.com';
|
|
29
|
+
const out = normalizeCommandShape({ filter: { email: secret } });
|
|
30
|
+
expect(JSON.stringify(out)).not.toContain(secret);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('caps recursion depth', () => {
|
|
34
|
+
let deep: any = 'leaf';
|
|
35
|
+
for (let i = 0; i < 20; i++) {
|
|
36
|
+
deep = { nested: deep };
|
|
37
|
+
}
|
|
38
|
+
const out = JSON.stringify(normalizeCommandShape(deep));
|
|
39
|
+
// Must terminate and not blow the stack; the sentinel marks the cut.
|
|
40
|
+
expect(out).toContain('…');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('produces identical output for identical query shapes (the N+1 template key)', () => {
|
|
44
|
+
const a = normalizeCommandShape({ filter: { email: 'a@x.com' }, limit: 1 });
|
|
45
|
+
const b = normalizeCommandShape({ filter: { email: 'b@y.com' }, limit: 999 });
|
|
46
|
+
expect(JSON.stringify(a)).toBe(JSON.stringify(b));
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a MongoDB command fragment into a value-free SHAPE.
|
|
3
|
+
*
|
|
4
|
+
* Every scalar becomes `'?'` while keys and `$operators` are preserved. This serves two purposes at
|
|
5
|
+
* once for the query profiler:
|
|
6
|
+
* 1. Privacy — no filter values, no document contents, ever enter the ring buffer.
|
|
7
|
+
* 2. N+1 detection — identical query shapes normalize to identical strings, so they can be grouped
|
|
8
|
+
* and counted (the "top templates" view).
|
|
9
|
+
*
|
|
10
|
+
* Bounds keep a pathological command from exhausting memory/stack: depth 5, 32 keys per object,
|
|
11
|
+
* object-arrays truncated to 8 elements.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const MAX_DEPTH = 5;
|
|
15
|
+
const MAX_KEYS = 32;
|
|
16
|
+
const MAX_ARRAY_ITEMS = 8;
|
|
17
|
+
const DEPTH_SENTINEL = '…';
|
|
18
|
+
const PLACEHOLDER = '?';
|
|
19
|
+
|
|
20
|
+
export function normalizeCommandShape(value: unknown, depth = 0): unknown {
|
|
21
|
+
if (depth >= MAX_DEPTH) {
|
|
22
|
+
return DEPTH_SENTINEL;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
// Object arrays (pipelines, bulk ops) keep per-element shape; scalar arrays ($in) collapse.
|
|
27
|
+
if (value.length > 0 && typeof value[0] === 'object' && value[0] !== null) {
|
|
28
|
+
return value.slice(0, MAX_ARRAY_ITEMS).map((item) => normalizeCommandShape(item, depth + 1));
|
|
29
|
+
}
|
|
30
|
+
return [PLACEHOLDER];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (value !== null && typeof value === 'object') {
|
|
34
|
+
const out: Record<string, unknown> = {};
|
|
35
|
+
let count = 0;
|
|
36
|
+
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
37
|
+
if (++count > MAX_KEYS) {
|
|
38
|
+
out[DEPTH_SENTINEL] = DEPTH_SENTINEL;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
out[key] = normalizeCommandShape((value as Record<string, unknown>)[key], depth + 1);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return PLACEHOLDER;
|
|
47
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { maskConfigDeep } from './hub-mask.helper';
|
|
4
|
+
|
|
5
|
+
describe('maskConfigDeep', () => {
|
|
6
|
+
it('masks values whose key matches the secret pattern', () => {
|
|
7
|
+
const out = maskConfigDeep({
|
|
8
|
+
jwt: { secret: 'SENTINEL_JWT', refresh: { secret: 'SENTINEL_REFRESH' } },
|
|
9
|
+
port: 3000,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(out.jwt.secret).toBe('***');
|
|
13
|
+
expect(out.jwt.refresh.secret).toBe('***');
|
|
14
|
+
expect(out.port).toBe(3000);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('masks a broad set of secret-ish key names', () => {
|
|
18
|
+
const out = maskConfigDeep({
|
|
19
|
+
apiKey: 'AK',
|
|
20
|
+
betterAuth: { secret: 'BA' },
|
|
21
|
+
encryptionSecret: 'ENC',
|
|
22
|
+
password: 'PW',
|
|
23
|
+
privateKey: 'PK',
|
|
24
|
+
token: 'TK',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
for (const value of Object.values(out)) {
|
|
28
|
+
const flat = JSON.stringify(value);
|
|
29
|
+
expect(flat).not.toMatch(/AK|BA|ENC|PW|PK|TK/);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('deep-clones — the input object is never mutated', () => {
|
|
34
|
+
const input = { jwt: { secret: 'SENTINEL' }, nested: { arr: [{ token: 'T' }] } };
|
|
35
|
+
const out = maskConfigDeep(input);
|
|
36
|
+
|
|
37
|
+
expect(input.jwt.secret).toBe('SENTINEL');
|
|
38
|
+
expect(input.nested.arr[0].token).toBe('T');
|
|
39
|
+
expect(out.jwt.secret).toBe('***');
|
|
40
|
+
expect(out.nested.arr[0].token).toBe('***');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('masks credentials embedded in URI-shaped values', () => {
|
|
44
|
+
const out = maskConfigDeep({ mongoose: { uri: 'mongodb://user:secretpw@host:27017/db' } });
|
|
45
|
+
|
|
46
|
+
expect(out.mongoose.uri).not.toContain('secretpw');
|
|
47
|
+
expect(out.mongoose.uri).toContain('mongodb://');
|
|
48
|
+
expect(out.mongoose.uri).toContain('host:27017');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('honors additional explicit secret field names', () => {
|
|
52
|
+
const out = maskConfigDeep({ customField: 'HIDE_ME', other: 'keep' }, ['customField']);
|
|
53
|
+
|
|
54
|
+
expect(out.customField).toBe('***');
|
|
55
|
+
expect(out.other).toBe('keep');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('passes through null, numbers, booleans and dates', () => {
|
|
59
|
+
const date = new Date('2026-01-01T00:00:00.000Z');
|
|
60
|
+
const out = maskConfigDeep({ a: null, b: 42, c: true, d: date });
|
|
61
|
+
|
|
62
|
+
expect(out.a).toBeNull();
|
|
63
|
+
expect(out.b).toBe(42);
|
|
64
|
+
expect(out.c).toBe(true);
|
|
65
|
+
expect(out.d).toBeInstanceOf(Date);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursive, non-mutating config masker for the Hub config viewer.
|
|
3
|
+
*
|
|
4
|
+
* CRITICAL: this deep-clones before masking. Hub JSON sidecars must never hand a live,
|
|
5
|
+
* service-owned object to the response pipeline — `CheckSecurityInterceptor` deletes secret-named
|
|
6
|
+
* keys IN PLACE, which would corrupt the running server config. Cloning here is the guarantee.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Key-name heuristics that mark a value as secret. Matched case-insensitively against each key. */
|
|
10
|
+
const SECRET_KEY_PATTERN =
|
|
11
|
+
/secret|passwd|password|passphrase|credential|api[-_]?key|access[-_]?token|refresh[-_]?token|private[-_]?key|encryption|\btoken\b|\bpass\b|(^|[^a-z])key([^a-z]|$)/i;
|
|
12
|
+
|
|
13
|
+
/** Value-shape heuristic: a connection URI carrying `scheme://user:password@host`. */
|
|
14
|
+
const URI_CREDENTIAL_PATTERN = /^([a-z][a-z0-9+.-]*:\/\/[^/@:\s]+:)([^@/\s]+)(@)/i;
|
|
15
|
+
|
|
16
|
+
const MASK = '***';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Deep-clone `value` and mask every secret it contains.
|
|
20
|
+
*
|
|
21
|
+
* A value is masked when its key matches {@link SECRET_KEY_PATTERN} or appears in `extraSecretKeys`.
|
|
22
|
+
* Additionally, any string that looks like a credentialed connection URI has its password segment
|
|
23
|
+
* masked regardless of key name.
|
|
24
|
+
*
|
|
25
|
+
* @param value - the object/value to mask (not mutated)
|
|
26
|
+
* @param extraSecretKeys - additional exact key names to always mask (e.g. `security.secretFields`)
|
|
27
|
+
*/
|
|
28
|
+
export function maskConfigDeep<T>(value: T, extraSecretKeys: string[] = []): T {
|
|
29
|
+
const extra = new Set(extraSecretKeys.map((k) => k.toLowerCase()));
|
|
30
|
+
return maskValue(value, false, extra) as T;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isSecretKey(key: string, extra: Set<string>): boolean {
|
|
34
|
+
return extra.has(key.toLowerCase()) || SECRET_KEY_PATTERN.test(key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function maskString(value: string): string {
|
|
38
|
+
const uriMatch = value.match(URI_CREDENTIAL_PATTERN);
|
|
39
|
+
if (uriMatch) {
|
|
40
|
+
return value.replace(URI_CREDENTIAL_PATTERN, `$1${MASK}$3`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function maskValue(value: unknown, keyIsSecret: boolean, extra: Set<string>): unknown {
|
|
46
|
+
if (value === null || value === undefined) {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Primitives.
|
|
51
|
+
if (typeof value !== 'object') {
|
|
52
|
+
if (keyIsSecret) {
|
|
53
|
+
return MASK;
|
|
54
|
+
}
|
|
55
|
+
return typeof value === 'string' ? maskString(value) : value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A whole secret-keyed object/array is collapsed to the mask (never leak its interior).
|
|
59
|
+
if (keyIsSecret) {
|
|
60
|
+
return MASK;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Preserve non-plain objects verbatim (Date, Buffer, etc.) — cloning would lose their type and
|
|
64
|
+
// they never carry framework secrets.
|
|
65
|
+
if (value instanceof Date || Buffer.isBuffer(value)) {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
return value.map((item) => maskValue(item, false, extra));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const out: Record<string, unknown> = {};
|
|
74
|
+
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
75
|
+
out[key] = maskValue(val, isSecretKey(key, extra), extra);
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { buildErDiagram, type HubModelDescriptor } from './hub-mermaid.helper';
|
|
4
|
+
|
|
5
|
+
describe('buildErDiagram', () => {
|
|
6
|
+
const models: HubModelDescriptor[] = [
|
|
7
|
+
{
|
|
8
|
+
fields: [
|
|
9
|
+
{ name: 'email', type: 'String' },
|
|
10
|
+
{ name: 'age', type: 'Number' },
|
|
11
|
+
{ name: 'company', ref: 'Company', type: 'ObjectId' },
|
|
12
|
+
],
|
|
13
|
+
name: 'User',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
fields: [{ name: 'name', type: 'String' }],
|
|
17
|
+
name: 'Company',
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
it('opens an erDiagram block', () => {
|
|
22
|
+
const out = buildErDiagram(models);
|
|
23
|
+
expect(out.startsWith('erDiagram')).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('emits one entity per model with its fields and types', () => {
|
|
27
|
+
const out = buildErDiagram(models);
|
|
28
|
+
expect(out).toContain('User {');
|
|
29
|
+
expect(out).toContain('Company {');
|
|
30
|
+
expect(out).toContain('String email');
|
|
31
|
+
expect(out).toContain('Number age');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('renders a relationship for a ref field', () => {
|
|
35
|
+
const out = buildErDiagram(models);
|
|
36
|
+
expect(out).toMatch(/User \|\|--o\{ Company : company/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('ignores refs pointing at models that are not present', () => {
|
|
40
|
+
const out = buildErDiagram([{ fields: [{ name: 'ghost', ref: 'Missing', type: 'ObjectId' }], name: 'Solo' }]);
|
|
41
|
+
expect(out).toContain('Solo {');
|
|
42
|
+
expect(out).not.toContain('Missing');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('sanitizes field/entity names so the diagram stays valid', () => {
|
|
46
|
+
const out = buildErDiagram([{ fields: [{ name: 'weird-field.name', type: 'String' }], name: 'Odd Name' }]);
|
|
47
|
+
expect(out).not.toContain('weird-field.name');
|
|
48
|
+
expect(out).not.toContain('Odd Name {');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('handles an empty model list without throwing', () => {
|
|
52
|
+
expect(buildErDiagram([])).toBe('erDiagram');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure builder for a Mermaid `erDiagram` from a normalized description of the Mongoose models.
|
|
3
|
+
*
|
|
4
|
+
* The CoreHubDbService walks `connection.models` and the ModelRegistry to produce the descriptors;
|
|
5
|
+
* keeping the string builder pure makes it unit-testable without a live database and CDN-free
|
|
6
|
+
* (the client shows the source + a mermaid.live link; no diagram library ships with the framework).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface HubModelDescriptor {
|
|
10
|
+
fields: HubModelField[];
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface HubModelField {
|
|
15
|
+
name: string;
|
|
16
|
+
/** Referenced model name (Mongoose `ref`), if this field is a relation. */
|
|
17
|
+
ref?: string;
|
|
18
|
+
/** Human-readable type label (String, Number, ObjectId, …). */
|
|
19
|
+
type: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Mermaid identifiers must be alphanumeric/underscore; collapse everything else. */
|
|
23
|
+
function sanitizeIdentifier(value: string): string {
|
|
24
|
+
const cleaned = String(value ?? '').replace(/[^A-Za-z0-9_]/g, '_');
|
|
25
|
+
return /^[A-Za-z_]/.test(cleaned) ? cleaned : `_${cleaned}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build a Mermaid `erDiagram`. Entities carry their fields (`Type name`); ref fields become
|
|
30
|
+
* one-to-many relationships (`A ||--o{ B : field`) when the target model is also present.
|
|
31
|
+
*/
|
|
32
|
+
export function buildErDiagram(models: HubModelDescriptor[]): string {
|
|
33
|
+
if (!models || models.length === 0) {
|
|
34
|
+
return 'erDiagram';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const present = new Set(models.map((m) => m.name));
|
|
38
|
+
const lines: string[] = ['erDiagram'];
|
|
39
|
+
|
|
40
|
+
for (const model of models) {
|
|
41
|
+
const entity = sanitizeIdentifier(model.name);
|
|
42
|
+
lines.push(` ${entity} {`);
|
|
43
|
+
for (const field of model.fields) {
|
|
44
|
+
const type = sanitizeIdentifier(field.type || 'Mixed');
|
|
45
|
+
const name = sanitizeIdentifier(field.name);
|
|
46
|
+
lines.push(` ${type} ${name}`);
|
|
47
|
+
}
|
|
48
|
+
lines.push(' }');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const model of models) {
|
|
52
|
+
const from = sanitizeIdentifier(model.name);
|
|
53
|
+
for (const field of model.fields) {
|
|
54
|
+
if (field.ref && present.has(field.ref)) {
|
|
55
|
+
const to = sanitizeIdentifier(field.ref);
|
|
56
|
+
lines.push(` ${from} ||--o{ ${to} : ${sanitizeIdentifier(field.name)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return lines.join('\n');
|
|
62
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildHubCsp,
|
|
5
|
+
buildHubSecurityHeaders,
|
|
6
|
+
escapeHtml,
|
|
7
|
+
escapeJsString,
|
|
8
|
+
generateNonce,
|
|
9
|
+
injectNonce,
|
|
10
|
+
} from './hub-shell.helper';
|
|
11
|
+
|
|
12
|
+
describe('hub-shell.helper', () => {
|
|
13
|
+
describe('escapeHtml', () => {
|
|
14
|
+
it('escapes all five HTML-significant characters', () => {
|
|
15
|
+
expect(escapeHtml(`<img src="x" onerror='y'>&`)).toBe('<img src="x" onerror='y'>&');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('neutralizes a script-tag XSS payload', () => {
|
|
19
|
+
const out = escapeHtml('<script>alert(1)</script>');
|
|
20
|
+
expect(out).not.toContain('<script>');
|
|
21
|
+
expect(out).toContain('<script>');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('handles empty / non-string-ish input', () => {
|
|
25
|
+
expect(escapeHtml('')).toBe('');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('escapeJsString', () => {
|
|
30
|
+
it('escapes characters that would break out of a JS string / script context', () => {
|
|
31
|
+
const out = escapeJsString(`a'b\\c</script>`);
|
|
32
|
+
expect(out).not.toContain('</script>');
|
|
33
|
+
expect(out).toContain("\\'");
|
|
34
|
+
expect(out).toContain('\\\\');
|
|
35
|
+
// The closing angle bracket must be escaped so `</script>` cannot terminate the tag.
|
|
36
|
+
expect(out).toContain('\\u003c');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('escapes CR / LF into their JS escapes', () => {
|
|
40
|
+
const out = escapeJsString('line1\r\nline2');
|
|
41
|
+
expect(out).toBe('line1\\r\\nline2');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('escapes U+2028 / U+2029, which are line terminators inside a JS string literal', () => {
|
|
45
|
+
// Built via fromCharCode so this test file stays pure ASCII (a literal separator would itself
|
|
46
|
+
// terminate the source line). A raw U+2028 in a `'...'` literal is a syntax error at runtime.
|
|
47
|
+
const lineSep = String.fromCharCode(0x2028);
|
|
48
|
+
const paraSep = String.fromCharCode(0x2029);
|
|
49
|
+
const out = escapeJsString(`before${lineSep}mid${paraSep}after`);
|
|
50
|
+
expect(out).toContain('\\u2028');
|
|
51
|
+
expect(out).toContain('\\u2029');
|
|
52
|
+
expect(out).not.toContain(lineSep);
|
|
53
|
+
expect(out).not.toContain(paraSep);
|
|
54
|
+
// The escaped result must be a valid single-line JS string literal.
|
|
55
|
+
expect(() => new Function(`return '${out}';`)()).not.toThrow();
|
|
56
|
+
expect(new Function(`return '${out}';`)()).toBe(`before${lineSep}mid${paraSep}after`);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('generateNonce', () => {
|
|
61
|
+
it('produces a non-empty base64 nonce that differs each call', () => {
|
|
62
|
+
const a = generateNonce();
|
|
63
|
+
const b = generateNonce();
|
|
64
|
+
expect(a.length).toBeGreaterThan(10);
|
|
65
|
+
expect(a).not.toBe(b);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('injectNonce', () => {
|
|
70
|
+
it('replaces every __CSP_NONCE__ placeholder', () => {
|
|
71
|
+
const html = `<style nonce="__CSP_NONCE__"></style><script nonce="__CSP_NONCE__"></script>`;
|
|
72
|
+
const out = injectNonce(html, 'abc123');
|
|
73
|
+
expect(out).not.toContain('__CSP_NONCE__');
|
|
74
|
+
expect(out).toContain('nonce="abc123"');
|
|
75
|
+
expect((out.match(/abc123/g) ?? []).length).toBe(2);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('buildHubCsp', () => {
|
|
80
|
+
it('is a strict, self-contained policy pinned to the nonce', () => {
|
|
81
|
+
const csp = buildHubCsp('nonce123');
|
|
82
|
+
expect(csp).toContain("default-src 'none'");
|
|
83
|
+
expect(csp).toContain("script-src 'nonce-nonce123'");
|
|
84
|
+
expect(csp).toContain("frame-ancestors 'none'");
|
|
85
|
+
expect(csp).not.toContain('unsafe-inline');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('buildHubSecurityHeaders', () => {
|
|
90
|
+
it('returns the strict header set with no-store for HTML by default', () => {
|
|
91
|
+
const headers = buildHubSecurityHeaders('n1');
|
|
92
|
+
expect(headers['Content-Security-Policy']).toContain('nonce-n1');
|
|
93
|
+
expect(headers['X-Frame-Options']).toBe('DENY');
|
|
94
|
+
expect(headers['X-Content-Type-Options']).toBe('nosniff');
|
|
95
|
+
expect(headers['Referrer-Policy']).toBe('no-referrer');
|
|
96
|
+
expect(headers['Cache-Control']).toContain('no-store');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('uses a long-lived immutable cache for the cacheable script bundle', () => {
|
|
100
|
+
const headers = buildHubSecurityHeaders('n1', { cacheable: true });
|
|
101
|
+
expect(headers['Cache-Control']).toContain('max-age');
|
|
102
|
+
expect(headers['Cache-Control']).toContain('immutable');
|
|
103
|
+
expect(headers['Cache-Control']).not.toContain('no-store');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure helpers for the Hub's self-contained HTML shell: escaping, CSP nonce handling and the
|
|
5
|
+
* response security-header set. Kept free of Express/Nest types so they are trivially unit-testable
|
|
6
|
+
* and reusable from both the page controller and the email-preview route.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Placeholder written into the shell template; replaced per-request by {@link injectNonce}. */
|
|
10
|
+
export const HUB_NONCE_PLACEHOLDER = '__CSP_NONCE__';
|
|
11
|
+
|
|
12
|
+
// Unicode line/paragraph separators. Built via fromCharCode so the source file stays pure ASCII —
|
|
13
|
+
// a literal U+2028/U+2029 in a regex literal would itself terminate the line and break parsing.
|
|
14
|
+
const LINE_SEPARATOR = String.fromCharCode(0x2028);
|
|
15
|
+
const PARAGRAPH_SEPARATOR = String.fromCharCode(0x2029);
|
|
16
|
+
|
|
17
|
+
/** Escape the five HTML-significant characters. Every server-provided value in the shell goes through this. */
|
|
18
|
+
export function escapeHtml(value: string): string {
|
|
19
|
+
return String(value ?? '')
|
|
20
|
+
.replace(/&/g, '&')
|
|
21
|
+
.replace(/</g, '<')
|
|
22
|
+
.replace(/>/g, '>')
|
|
23
|
+
.replace(/"/g, '"')
|
|
24
|
+
.replace(/'/g, ''');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Escape a value for embedding inside a `<script>` JS string literal. Escapes backslash, single
|
|
29
|
+
* quote and the `<` character (as `<`) so a `</script>` sequence in the data cannot terminate
|
|
30
|
+
* the surrounding tag. Also escapes U+2028 / U+2029, which are literal line terminators inside a
|
|
31
|
+
* JS string and would otherwise break the script (or enable injection) despite looking like text.
|
|
32
|
+
*/
|
|
33
|
+
export function escapeJsString(value: string): string {
|
|
34
|
+
return String(value ?? '')
|
|
35
|
+
.replace(/\\/g, '\\\\')
|
|
36
|
+
.replace(/'/g, "\\'")
|
|
37
|
+
.replace(/</g, '\\u003c')
|
|
38
|
+
.replace(/\r/g, '\\r')
|
|
39
|
+
.replace(/\n/g, '\\n')
|
|
40
|
+
.split(LINE_SEPARATOR)
|
|
41
|
+
.join('\\u2028')
|
|
42
|
+
.split(PARAGRAPH_SEPARATOR)
|
|
43
|
+
.join('\\u2029');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Generate a fresh per-request CSP nonce. */
|
|
47
|
+
export function generateNonce(): string {
|
|
48
|
+
return randomBytes(16).toString('base64');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Replace every {@link HUB_NONCE_PLACEHOLDER} in a pre-built shell string with the actual nonce. */
|
|
52
|
+
export function injectNonce(html: string, nonce: string): string {
|
|
53
|
+
return html.split(HUB_NONCE_PLACEHOLDER).join(nonce);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build the strict Content-Security-Policy for a Hub HTML response. `default-src 'none'` denies
|
|
58
|
+
* everything by default; scripts and styles are allowed only via the per-request nonce (plus `'self'`
|
|
59
|
+
* for the shared hub.js bundle). No `unsafe-inline`, no external origins.
|
|
60
|
+
*/
|
|
61
|
+
export function buildHubCsp(nonce: string): string {
|
|
62
|
+
return [
|
|
63
|
+
"default-src 'none'",
|
|
64
|
+
`script-src 'nonce-${nonce}' 'self'`,
|
|
65
|
+
`style-src 'nonce-${nonce}'`,
|
|
66
|
+
"img-src 'self' data:",
|
|
67
|
+
"connect-src 'self'",
|
|
68
|
+
"frame-src 'self'",
|
|
69
|
+
"frame-ancestors 'none'",
|
|
70
|
+
"base-uri 'none'",
|
|
71
|
+
"form-action 'self'",
|
|
72
|
+
].join('; ');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The full response header set for Hub responses.
|
|
77
|
+
*
|
|
78
|
+
* @param nonce - the per-request CSP nonce
|
|
79
|
+
* @param opts.cacheable - when true, use a long-lived immutable cache (for the versioned hub.js
|
|
80
|
+
* bundle); otherwise `no-store` (HTML pages and JSON sidecars must never be cached)
|
|
81
|
+
*/
|
|
82
|
+
export function buildHubSecurityHeaders(nonce: string, opts?: { cacheable?: boolean }): Record<string, string> {
|
|
83
|
+
return {
|
|
84
|
+
'Cache-Control': opts?.cacheable ? 'private, max-age=86400, immutable' : 'no-store',
|
|
85
|
+
'Content-Security-Policy': buildHubCsp(nonce),
|
|
86
|
+
'Referrer-Policy': 'no-referrer',
|
|
87
|
+
'X-Content-Type-Options': 'nosniff',
|
|
88
|
+
'X-Frame-Options': 'DENY',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized admin-facing messages for the Hub's mutating actions.
|
|
3
|
+
*
|
|
4
|
+
* The Hub deliberately does NOT route these through the ErrorCode i18n catalog: that module is
|
|
5
|
+
* OPTIONAL (a consumer project may not register it — e.g. `errorCode.autoRegister: false`), so the
|
|
6
|
+
* Hub, which must stay self-contained, cannot hard-depend on it. It is also a single-admin operator
|
|
7
|
+
* tool where a plain English message is the right level of detail — not something an end-user sees.
|
|
8
|
+
*
|
|
9
|
+
* Centralizing the strings here (instead of scattering literals across the action controller and
|
|
10
|
+
* services) keeps the wording consistent and reusable, and gives a single place to review every
|
|
11
|
+
* message the mutating-action surface can emit. See the Hub README ("Action errors").
|
|
12
|
+
*
|
|
13
|
+
* Pure, import-free data — safe to import from anywhere in the module.
|
|
14
|
+
*/
|
|
15
|
+
export const HubActionMessage = {
|
|
16
|
+
/** Generic fallback when an action throws a non-Error value. */
|
|
17
|
+
actionFailed: 'Action failed.',
|
|
18
|
+
/** Delete file: the typed confirmation filename did not match the stored filename. */
|
|
19
|
+
confirmationFilenameMismatch: 'Confirmation filename does not match.',
|
|
20
|
+
/** Delete file: no confirmation filename was supplied. */
|
|
21
|
+
confirmationFilenameRequired: 'Confirmation filename required.',
|
|
22
|
+
/** Test mail: the EmailService is not wired into this app. */
|
|
23
|
+
emailServiceUnavailable: 'EmailService is not available.',
|
|
24
|
+
/** Delete file: no GridFS file with the given id. */
|
|
25
|
+
fileNotFound: 'File not found.',
|
|
26
|
+
/** Delete file: the id is not a valid ObjectId. */
|
|
27
|
+
invalidFileId: 'Invalid file id.',
|
|
28
|
+
/** Migrations action while the migrations panel is disabled. */
|
|
29
|
+
migrationsDisabled: 'The migrations panel is disabled.',
|
|
30
|
+
/** CSRF guard: the mutating request lacked the X-Hub-Request header. */
|
|
31
|
+
missingHubRequestHeader: 'Missing X-Hub-Request header.',
|
|
32
|
+
/** Any action that needs the Mongo connection while it is unavailable. */
|
|
33
|
+
mongoUnavailable: 'No MongoDB connection is available.',
|
|
34
|
+
/** Migrations action while no Mongo URI is configured. */
|
|
35
|
+
mongoUriMissing: 'No MongoDB URI is configured.',
|
|
36
|
+
/** Test mail: no recipient supplied. */
|
|
37
|
+
recipientRequired: 'Recipient (to) required.',
|
|
38
|
+
/** Clear-collector action with an unknown collector name. */
|
|
39
|
+
unknownCollector: 'Unknown collector.',
|
|
40
|
+
/** Cron action with an unknown sub-action (not start/stop/trigger). */
|
|
41
|
+
unknownCronAction: 'Unknown cron action.',
|
|
42
|
+
|
|
43
|
+
/** Confirm-guarded action: the typed keyword did not match the expected one. */
|
|
44
|
+
confirmationKeywordMismatch: (expected: string): string => `Confirmation keyword mismatch (expected "${expected}").`,
|
|
45
|
+
/** Test mail: the requested template is not in the inventory (allowlist). */
|
|
46
|
+
unknownEmailTemplate: (name: string): string => `Unknown email template "${name}".`,
|
|
47
|
+
} as const;
|