@futdevpro/nts-dynamo 1.15.153 → 1.15.155

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.
Files changed (52) hide show
  1. package/README.md +25 -6
  2. package/__documentations/2026-08-22-content-free-process-error-boundary.md +91 -0
  3. package/build/_models/control-models/app-params.control-model.d.ts.map +1 -1
  4. package/build/_models/control-models/app-params.control-model.js +1 -2
  5. package/build/_models/control-models/app-params.control-model.js.map +1 -1
  6. package/build/_modules/server/index.d.ts +4 -0
  7. package/build/_modules/server/index.d.ts.map +1 -1
  8. package/build/_modules/server/index.js +5 -0
  9. package/build/_modules/server/index.js.map +1 -1
  10. package/build/_modules/server/safe-diagnostic/safe-diagnostic-cause.type-enum.d.ts +9 -0
  11. package/build/_modules/server/safe-diagnostic/safe-diagnostic-cause.type-enum.d.ts.map +1 -0
  12. package/build/_modules/server/safe-diagnostic/safe-diagnostic-cause.type-enum.js +13 -0
  13. package/build/_modules/server/safe-diagnostic/safe-diagnostic-cause.type-enum.js.map +1 -0
  14. package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.d.ts +20 -0
  15. package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.d.ts.map +1 -0
  16. package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.js +24 -0
  17. package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.js.map +1 -0
  18. package/build/_modules/server/safe-diagnostic/safe-diagnostic.control-service.d.ts +19 -0
  19. package/build/_modules/server/safe-diagnostic/safe-diagnostic.control-service.d.ts.map +1 -0
  20. package/build/_modules/server/safe-diagnostic/safe-diagnostic.control-service.js +83 -0
  21. package/build/_modules/server/safe-diagnostic/safe-diagnostic.control-service.js.map +1 -0
  22. package/build/_modules/server/safe-diagnostic/safe-diagnostic.interface.d.ts +19 -0
  23. package/build/_modules/server/safe-diagnostic/safe-diagnostic.interface.d.ts.map +1 -0
  24. package/build/_modules/server/safe-diagnostic/safe-diagnostic.interface.js +3 -0
  25. package/build/_modules/server/safe-diagnostic/safe-diagnostic.interface.js.map +1 -0
  26. package/build/_services/core/global.service.d.ts +3 -3
  27. package/build/_services/core/global.service.d.ts.map +1 -1
  28. package/build/_services/core/global.service.js +24 -18
  29. package/build/_services/core/global.service.js.map +1 -1
  30. package/build/_services/server/app.server.d.ts +2 -0
  31. package/build/_services/server/app.server.d.ts.map +1 -1
  32. package/build/_services/server/app.server.js +71 -199
  33. package/build/_services/server/app.server.js.map +1 -1
  34. package/build/_services/shared.static-service.d.ts +11 -4
  35. package/build/_services/shared.static-service.d.ts.map +1 -1
  36. package/build/_services/shared.static-service.js +26 -6
  37. package/build/_services/shared.static-service.js.map +1 -1
  38. package/package.json +15 -9
  39. package/src/_models/control-models/app-params.control-model.spec.ts +9 -5
  40. package/src/_models/control-models/app-params.control-model.ts +4 -3
  41. package/src/_modules/server/index.ts +6 -0
  42. package/src/_modules/server/safe-diagnostic/safe-diagnostic-cause.type-enum.ts +8 -0
  43. package/src/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.ts +19 -0
  44. package/src/_modules/server/safe-diagnostic/safe-diagnostic.control-service.spec.ts +124 -0
  45. package/src/_modules/server/safe-diagnostic/safe-diagnostic.control-service.ts +99 -0
  46. package/src/_modules/server/safe-diagnostic/safe-diagnostic.interface.ts +20 -0
  47. package/src/_modules/server/server-status/db-readiness-probe.control-service.spec.ts +2 -2
  48. package/src/_services/core/global.service.ts +34 -33
  49. package/src/_services/server/app-safe-diagnostic.spec.ts +124 -0
  50. package/src/_services/server/app.server.ts +209 -262
  51. package/src/_services/shared.static-service.spec.ts +57 -23
  52. package/src/_services/shared.static-service.ts +48 -19
@@ -0,0 +1,99 @@
1
+ import { createHash } from 'crypto';
2
+
3
+ import { DyFM_Error, DyFM_Log } from '@futdevpro/fsm-dynamo';
4
+
5
+ import { DyNTS_SafeDiagnosticCause_Type } from './safe-diagnostic-cause.type-enum';
6
+ import {
7
+ DyNTS_SafeDiagnosticInput_Interface,
8
+ DyNTS_SafeDiagnostic_Interface
9
+ } from './safe-diagnostic.interface';
10
+
11
+ /** Projects arbitrary failures to a versioned, content-free public diagnostic. */
12
+ export class DyNTS_SafeDiagnostic_ControlService {
13
+ private static readonly contractVersion: 'dynts-safe-diagnostic/1' = 'dynts-safe-diagnostic/1';
14
+ private static readonly defaultCode: string = 'DYNTS_SAFE_DIAGNOSTIC_INVALID_CODE';
15
+ private static readonly defaultStatus: number = 500;
16
+
17
+ /** Returns an immutable exact allowlist and never reflects raw failure content. */
18
+ static project(input: DyNTS_SafeDiagnosticInput_Interface): DyNTS_SafeDiagnostic_Interface {
19
+ const code: string = this.getCode(input.code);
20
+ const causeType: DyNTS_SafeDiagnosticCause_Type = this.getCauseType(input.error);
21
+ const status: number = this.getStatus(input.status);
22
+ const fingerprint: string = this.getFingerprint([
23
+ this.contractVersion,
24
+ code,
25
+ input.stage,
26
+ causeType,
27
+ status.toString(),
28
+ ]);
29
+
30
+ return Object.freeze({
31
+ contractVersion: this.contractVersion,
32
+ code: code,
33
+ stage: input.stage,
34
+ causeType: causeType,
35
+ fingerprint: fingerprint,
36
+ status: status,
37
+ });
38
+ }
39
+
40
+ /** Logs only the projected allowlist and returns the same immutable diagnostic. */
41
+ static report(input: DyNTS_SafeDiagnosticInput_Interface): DyNTS_SafeDiagnostic_Interface {
42
+ const diagnostic: DyNTS_SafeDiagnostic_Interface = this.project(input);
43
+
44
+ DyFM_Log.error('[DyNTS safe diagnostic]', diagnostic);
45
+
46
+ return diagnostic;
47
+ }
48
+
49
+ /** Creates a framework error that carries only the safe diagnostic allowlist. */
50
+ static toError(diagnostic: DyNTS_SafeDiagnostic_Interface): DyFM_Error {
51
+ return new DyFM_Error({
52
+ errorCode: diagnostic.code,
53
+ message: `Framework failure at ${diagnostic.stage}. Correlation: ${diagnostic.fingerprint}.`,
54
+ userMessage: 'A server operation failed. Contact the responsible team with the correlation fingerprint.',
55
+ additionalContent: { diagnostic: diagnostic },
56
+ status: diagnostic.status,
57
+ });
58
+ }
59
+
60
+ private static getCode(code: string): string {
61
+ return /^[A-Z][A-Z0-9_|-]{2,127}$/.test(code) ? code : this.defaultCode;
62
+ }
63
+
64
+ private static getCauseType(error: unknown): DyNTS_SafeDiagnosticCause_Type {
65
+ try {
66
+ if (error instanceof DyFM_Error) {
67
+ return DyNTS_SafeDiagnosticCause_Type.dynamoError;
68
+ }
69
+
70
+ if (error instanceof Error) {
71
+ return DyNTS_SafeDiagnosticCause_Type.nativeError;
72
+ }
73
+
74
+ if (error !== null && typeof error === 'object') {
75
+ return DyNTS_SafeDiagnosticCause_Type.object;
76
+ }
77
+
78
+ if (error !== undefined) {
79
+ return DyNTS_SafeDiagnosticCause_Type.primitive;
80
+ }
81
+ } catch {
82
+ return DyNTS_SafeDiagnosticCause_Type.unknown;
83
+ }
84
+
85
+ return DyNTS_SafeDiagnosticCause_Type.unknown;
86
+ }
87
+
88
+ private static getStatus(status?: number): number {
89
+ if (typeof status !== 'number' || !Number.isInteger(status) || status < 100 || status > 599) {
90
+ return this.defaultStatus;
91
+ }
92
+
93
+ return status;
94
+ }
95
+
96
+ private static getFingerprint(subject: string[]): string {
97
+ return createHash('sha256').update(subject.join('\u0000'), 'utf8').digest('hex');
98
+ }
99
+ }
@@ -0,0 +1,20 @@
1
+ import { DyNTS_SafeDiagnosticCause_Type } from './safe-diagnostic-cause.type-enum';
2
+ import { DyNTS_SafeDiagnosticStage_Type } from './safe-diagnostic-stage.type-enum';
3
+
4
+ /** Caller-owned input whose raw failure content is never returned or logged. */
5
+ export interface DyNTS_SafeDiagnosticInput_Interface {
6
+ code: string;
7
+ stage: DyNTS_SafeDiagnosticStage_Type;
8
+ error?: unknown;
9
+ status?: number;
10
+ }
11
+
12
+ /** Exact public allowlist emitted by the safe diagnostic boundary. */
13
+ export interface DyNTS_SafeDiagnostic_Interface {
14
+ contractVersion: 'dynts-safe-diagnostic/1';
15
+ code: string;
16
+ stage: DyNTS_SafeDiagnosticStage_Type;
17
+ causeType: DyNTS_SafeDiagnosticCause_Type;
18
+ fingerprint: string;
19
+ status: number;
20
+ }
@@ -189,7 +189,7 @@ describe('| DyNTS_DbReadinessProbe_ControlService', (): void => {
189
189
  failure.self = failure;
190
190
  const result: DyNTS_DbReadinessProbeResult_Interface = await probe(createConnection({
191
191
  readyStateGetter: (): number => {
192
- throw failure;
192
+ throw failure;
193
193
  },
194
194
  }));
195
195
 
@@ -203,7 +203,7 @@ describe('| DyNTS_DbReadinessProbe_ControlService', (): void => {
203
203
  failure.self = failure;
204
204
  const result: DyNTS_DbReadinessProbeResult_Interface = await probe(createConnection({
205
205
  databaseGetter: (): mongoose.Connection['db'] => {
206
- throw failure;
206
+ throw failure;
207
207
  },
208
208
  }));
209
209
 
@@ -1,6 +1,3 @@
1
-
2
-
3
-
4
1
  import {
5
2
  DyFM_AnyError,
6
3
  DyFM_Array,
@@ -11,23 +8,26 @@ import {
11
8
  DyFM_Log,
12
9
  DyFM_Metadata
13
10
  } from '@futdevpro/fsm-dynamo';
14
- import { DyNTS_App_Params } from '../../_models/control-models/app-params.control-model';
15
-
16
- import {
17
- DyNTS_GlobalErrorHandlerFn,
18
- DyNTS_GlobalService_Settings
19
- } from '../../_models/interfaces/global-service-settings.interface';
20
11
 
21
12
  import { DyNTS_AuthService } from './auth.service';
22
- /* import { DyNTS_DBServiceCollection } from './db-service-collection.service'; */
23
- import { DyNTS_DBService } from '../base/db.service';
24
13
  import { DyNTS_EmailService } from './email.service';
25
14
  import { DyNTS_Service_Collection } from './service-collection.service';
15
+ /* import { DyNTS_DBServiceCollection } from './db-service-collection.service'; */
26
16
  /* import { DyNTS_EmailServiceCollection } from './email-service-collection.service'; */
27
- import { DyNTS_global_settings } from '../../_collections/global-settings.const';
17
+ import { DyNTS_DBService } from '../base/db.service';
28
18
  import { DyNTS_SingletonService } from '../base/singleton.service';
29
19
  /* import { DyNTS_archiveFlag } from '../../_collections/archive-flag.const'; */
30
20
  import { DyNTS_getArchivedDBName } from '../../_collections/archive.util';
21
+ import { DyNTS_global_settings } from '../../_collections/global-settings.const';
22
+ import { DyNTS_App_Params } from '../../_models/control-models/app-params.control-model';
23
+ import {
24
+ DyNTS_GlobalErrorHandlerFn,
25
+ DyNTS_GlobalService_Settings
26
+ } from '../../_models/interfaces/global-service-settings.interface';
27
+ import { DyNTS_SafeDiagnosticStage_Type } from '../../_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum';
28
+ import {
29
+ DyNTS_SafeDiagnostic_ControlService
30
+ } from '../../_modules/server/safe-diagnostic/safe-diagnostic.control-service';
31
31
 
32
32
  /**
33
33
  * This is the main Global/Core Service Collection used by DynamoNTS,
@@ -76,8 +76,12 @@ export class DyNTS_GlobalService extends DyNTS_SingletonService {
76
76
 
77
77
  static globalErrorHandler: DyNTS_GlobalErrorHandlerFn =
78
78
  async (error: DyFM_AnyError): Promise<void> => {
79
- DyFM_Log.error(`Global Error Handler not set!\nError:`, error);
80
- }
79
+ DyNTS_SafeDiagnostic_ControlService.report({
80
+ code: 'DYNTS|GLOBAL|HANDLER_NOT_SET',
81
+ stage: DyNTS_SafeDiagnosticStage_Type.globalErrorHandler,
82
+ error: error,
83
+ });
84
+ };
81
85
 
82
86
  /**
83
87
  * You need to setup global Services through this function
@@ -182,11 +186,11 @@ export class DyNTS_GlobalService extends DyNTS_SingletonService {
182
186
  try {
183
187
  this.instance.dbServiceCollection[dbModel.dataName] = new DyNTS_DBService(dbModel);
184
188
  } catch (error) {
185
- if (error instanceof DyFM_Error && !DyFM_global_settings.log_settings.highDetailedLogs) {
186
- error.logSimple(`Failed to create DyNTS_DBService (${dbModel.dataName}).`);
187
- } else {
188
- DyFM_Log.error(`Failed to create DyNTS_DBService (${dbModel.dataName}).`, error);
189
- }
189
+ DyNTS_SafeDiagnostic_ControlService.report({
190
+ code: 'DYNTS|GLOBAL|DB_SERVICE_CREATE_FAILED',
191
+ stage: DyNTS_SafeDiagnosticStage_Type.bootstrap,
192
+ error: error,
193
+ });
190
194
 
191
195
  throw new DyFM_Error({
192
196
  ...this.getDefaultErrorSettings('setDBServices', error, 'DyNTS_GlobalService'),
@@ -265,7 +269,7 @@ export class DyNTS_GlobalService extends DyNTS_SingletonService {
265
269
 
266
270
  private static async setErrorHandler(errorHandler?: DyNTS_GlobalErrorHandlerFn): Promise<void> {
267
271
  try {
268
- DyNTS_GlobalService.globalErrorHandler = (async (error: any): Promise<void> => {
272
+ DyNTS_GlobalService.globalErrorHandler = (async (error: unknown): Promise<void> => {
269
273
  try {
270
274
  if (errorHandler) {
271
275
  await errorHandler(error);
@@ -280,20 +284,17 @@ export class DyNTS_GlobalService extends DyNTS_SingletonService {
280
284
  'please check if it is using the same node_modules as the app)'
281
285
  );
282
286
 
283
- DyFM_Error.logSimple(`Error caught by globalErrorHandler.`, error);
287
+ DyNTS_SafeDiagnostic_ControlService.report({
288
+ code: 'DYNTS|GLOBAL|UNHANDLED_ERROR',
289
+ stage: DyNTS_SafeDiagnosticStage_Type.globalErrorHandler,
290
+ error: error,
291
+ });
284
292
  } catch (error) {
285
- if (error instanceof DyFM_Error) {
286
- error.logSimple(
287
- `NTS GLOBAL MULTILEVEL ERROR: ` +
288
- `\nFailed to handle error on globalErrorHandler.`,
289
- );
290
- } else {
291
- DyFM_Log.error(
292
- `NTS GLOBAL MULTILEVEL ERROR: ` +
293
- `\nFailed to handle error on globalErrorHandler.`,
294
- (error as DyFM_Error)?.getErrorSimplified?.() ?? error
295
- );
296
- }
293
+ DyNTS_SafeDiagnostic_ControlService.report({
294
+ code: 'DYNTS|GLOBAL|HANDLER_FAILED',
295
+ stage: DyNTS_SafeDiagnosticStage_Type.globalErrorHandler,
296
+ error: error,
297
+ });
297
298
  }
298
299
  });
299
300
  } catch (error) {
@@ -0,0 +1,124 @@
1
+ import { DyFM_Log } from '@futdevpro/fsm-dynamo';
2
+
3
+ import { DyNTS_App_Params } from '../../_models/control-models/app-params.control-model';
4
+ import { DyNTS_Http_Settings } from '../../_models/control-models/http-settings.control-model';
5
+ import { DyNTS_GlobalService_Settings } from '../../_models/interfaces/global-service-settings.interface';
6
+ import { DyNTS_GlobalService } from '../core/global.service';
7
+ import { DyNTS_App } from './app.server';
8
+
9
+ class DyNTS_SafeDiagnosticTestApp extends DyNTS_App {
10
+ protected override asyncConstruct(): Promise<void> {
11
+ return Promise.resolve();
12
+ }
13
+
14
+ /** Supplies the minimal valid application parameters for the boundary test. */
15
+ getAppParams(): DyNTS_App_Params {
16
+ return new DyNTS_App_Params({
17
+ name: 'SafeDiagnosticTestApp',
18
+ version: '1.0.0',
19
+ dbName: 'safe_diagnostic_test',
20
+ });
21
+ }
22
+
23
+ /** Keeps the test application independent from concrete global services. */
24
+ getGlobalServiceCollection(): DyNTS_GlobalService_Settings {
25
+ return {};
26
+ }
27
+
28
+ /** Keeps the test application independent from a listening HTTP server. */
29
+ getPortSettings(): DyNTS_Http_Settings {
30
+ return new DyNTS_Http_Settings();
31
+ }
32
+ }
33
+
34
+ class DyNTS_FailingBootstrapTestApp extends DyNTS_SafeDiagnosticTestApp {
35
+ protected override asyncConstruct(): Promise<void> {
36
+ return Promise.reject(new Error(DyNTS_FailingBootstrapTestApp.canary));
37
+ }
38
+
39
+ static readonly canary: string = 'mongodb://bootstrap-user:bootstrap-secret@private-host.example/database';
40
+ }
41
+
42
+ describe('| DyNTS_App safe process boundary', (): void => {
43
+ const rejectionCanary: string = 'mongodb://rejection-user:rejection-secret@private-host.example/database';
44
+ let originalHandler: typeof DyNTS_GlobalService.globalErrorHandler;
45
+ let initialListeners: NodeJS.UnhandledRejectionListener[];
46
+
47
+ beforeEach((): void => {
48
+ originalHandler = DyNTS_GlobalService.globalErrorHandler;
49
+ initialListeners = process.listeners('unhandledRejection');
50
+ });
51
+
52
+ afterEach((): void => {
53
+ DyNTS_GlobalService.globalErrorHandler = originalHandler;
54
+ process.listeners('unhandledRejection').forEach((listener: NodeJS.UnhandledRejectionListener): void => {
55
+ if (!initialListeners.includes(listener)) {
56
+ process.removeListener('unhandledRejection', listener);
57
+ }
58
+ });
59
+ });
60
+
61
+ it('| projects an unhandled rejection before logging or forwarding it', async (): Promise<void> => {
62
+ const logSpy: jasmine.Spy = spyOn(DyFM_Log, 'error');
63
+ const handlerSpy: jasmine.Spy = jasmine.createSpy('globalErrorHandler').and.resolveTo();
64
+
65
+ DyNTS_GlobalService.globalErrorHandler = handlerSpy;
66
+ new DyNTS_SafeDiagnosticTestApp();
67
+
68
+ const addedListeners: NodeJS.UnhandledRejectionListener[] = process
69
+ .listeners('unhandledRejection')
70
+ .filter((listener: NodeJS.UnhandledRejectionListener): boolean => !initialListeners.includes(listener));
71
+
72
+ expect(addedListeners.length).toBe(1);
73
+ addedListeners[0](new Error(rejectionCanary), Promise.resolve());
74
+ await Promise.resolve();
75
+
76
+ const serializedLogs: string = JSON.stringify(logSpy.calls.allArgs());
77
+ const serializedForwardedError: string = JSON.stringify(handlerSpy.calls.mostRecent().args[0]);
78
+
79
+ expect(serializedLogs).toContain('dynts-safe-diagnostic/1');
80
+ expect(serializedForwardedError).toContain('dynts-safe-diagnostic/1');
81
+ expect(serializedForwardedError).toContain('DYNTS-AS0-BASE-UR');
82
+ expect(handlerSpy.calls.mostRecent().args.length).toBe(1);
83
+ expect(serializedLogs).not.toContain(rejectionCanary);
84
+ expect(serializedLogs).not.toContain('rejection-secret');
85
+ expect(serializedForwardedError).not.toContain(rejectionCanary);
86
+ expect(serializedForwardedError).not.toContain('rejection-secret');
87
+ });
88
+
89
+ it('| projects a bootstrap rejection before logging, forwarding and exiting', async (): Promise<void> => {
90
+ const logSpy: jasmine.Spy = spyOn(DyFM_Log, 'error');
91
+ const handlerSpy: jasmine.Spy = jasmine.createSpy('globalErrorHandler').and.resolveTo();
92
+ const exitSpy: jasmine.Spy = spyOn(process, 'exit');
93
+
94
+ DyNTS_GlobalService.globalErrorHandler = handlerSpy;
95
+ new DyNTS_FailingBootstrapTestApp();
96
+ await new Promise<void>((resolve: () => void): NodeJS.Immediate => setImmediate(resolve));
97
+
98
+ const serializedLogs: string = JSON.stringify(logSpy.calls.allArgs());
99
+ const serializedForwardedError: string = JSON.stringify(handlerSpy.calls.mostRecent().args[0]);
100
+
101
+ expect(exitSpy).toHaveBeenCalledOnceWith(1);
102
+ expect(serializedLogs).toContain('dynts-safe-diagnostic/1');
103
+ expect(serializedForwardedError).toContain('dynts-safe-diagnostic/1');
104
+ expect(serializedForwardedError).toContain('DYNTS-AS0-BOOTSTRAP-FAILED');
105
+ expect(handlerSpy.calls.mostRecent().args.length).toBe(1);
106
+ expect(serializedLogs).not.toContain(DyNTS_FailingBootstrapTestApp.canary);
107
+ expect(serializedLogs).not.toContain('bootstrap-secret');
108
+ expect(serializedForwardedError).not.toContain(DyNTS_FailingBootstrapTestApp.canary);
109
+ expect(serializedForwardedError).not.toContain('bootstrap-secret');
110
+ });
111
+
112
+ it('| projects raw failures in the default global error handler', async (): Promise<void> => {
113
+ const logSpy: jasmine.Spy = spyOn(DyFM_Log, 'error');
114
+
115
+ await DyNTS_GlobalService.setServices({});
116
+ await DyNTS_GlobalService.globalErrorHandler(new Error(rejectionCanary));
117
+
118
+ const serializedLogs: string = JSON.stringify(logSpy.calls.allArgs());
119
+
120
+ expect(serializedLogs).toContain('dynts-safe-diagnostic/1');
121
+ expect(serializedLogs).not.toContain(rejectionCanary);
122
+ expect(serializedLogs).not.toContain('rejection-secret');
123
+ });
124
+ });