@futdevpro/nts-dynamo 1.15.131 → 1.15.134

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 (38) hide show
  1. package/.dynamo/logs/cicd-pipeline/output.log +1713 -1595
  2. package/.dynamo/logs/cicd-pipeline/status.json +28 -28
  3. package/__documentations/2026-08-08-fr292-app-version-header.md +38 -0
  4. package/build/_collections/app-version-header.util.d.ts +42 -0
  5. package/build/_collections/app-version-header.util.d.ts.map +1 -0
  6. package/build/_collections/app-version-header.util.js +67 -0
  7. package/build/_collections/app-version-header.util.js.map +1 -0
  8. package/build/_models/interfaces/app-version-header-settings.interface.d.ts +17 -0
  9. package/build/_models/interfaces/app-version-header-settings.interface.d.ts.map +1 -0
  10. package/build/_models/interfaces/app-version-header-settings.interface.js +3 -0
  11. package/build/_models/interfaces/app-version-header-settings.interface.js.map +1 -0
  12. package/build/_models/interfaces/global-settings.interface.d.ts +7 -0
  13. package/build/_models/interfaces/global-settings.interface.d.ts.map +1 -1
  14. package/build/_modules/usage/usage.controller.d.ts.map +1 -1
  15. package/build/_modules/usage/usage.controller.js +14 -4
  16. package/build/_modules/usage/usage.controller.js.map +1 -1
  17. package/build/_services/base/data.service.d.ts.map +1 -1
  18. package/build/_services/base/data.service.js +4 -1
  19. package/build/_services/base/data.service.js.map +1 -1
  20. package/build/_services/server/app.server.d.ts +9 -0
  21. package/build/_services/server/app.server.d.ts.map +1 -1
  22. package/build/_services/server/app.server.js +21 -0
  23. package/build/_services/server/app.server.js.map +1 -1
  24. package/build/index.d.ts +2 -0
  25. package/build/index.d.ts.map +1 -1
  26. package/build/index.js +2 -0
  27. package/build/index.js.map +1 -1
  28. package/package.json +2 -2
  29. package/src/_collections/app-version-header.util.spec.ts +108 -0
  30. package/src/_collections/app-version-header.util.ts +74 -0
  31. package/src/_models/interfaces/app-version-header-settings.interface.ts +17 -0
  32. package/src/_models/interfaces/global-settings.interface.ts +8 -0
  33. package/src/_modules/usage/usage.controller.spec.ts +25 -0
  34. package/src/_modules/usage/usage.controller.ts +37 -23
  35. package/src/_services/base/data.service.spec.ts +55 -0
  36. package/src/_services/base/data.service.ts +4 -1
  37. package/src/_services/server/app.server.ts +26 -0
  38. package/src/index.ts +2 -0
package/build/index.js CHANGED
@@ -48,9 +48,11 @@ tslib_1.__exportStar(require("./_models/interfaces/signed-url-verify-result.inte
48
48
  tslib_1.__exportStar(require("./_models/interfaces/static-client-settings.interface"), exports);
49
49
  tslib_1.__exportStar(require("./_models/interfaces/cors-settings.interface"), exports);
50
50
  tslib_1.__exportStar(require("./_models/interfaces/security-headers-settings.interface"), exports);
51
+ tslib_1.__exportStar(require("./_models/interfaces/app-version-header-settings.interface"), exports);
51
52
  tslib_1.__exportStar(require("./_models/interfaces/db-query-options.interface"), exports);
52
53
  tslib_1.__exportStar(require("./_models/interfaces/migration-entry.interface"), exports);
53
54
  tslib_1.__exportStar(require("./_collections/security-headers.util"), exports);
55
+ tslib_1.__exportStar(require("./_collections/app-version-header.util"), exports);
54
56
  tslib_1.__exportStar(require("./_collections/signed-url.util"), exports);
55
57
  tslib_1.__exportStar(require("./_collections/client-safe-error.util"), exports);
56
58
  tslib_1.__exportStar(require("./_collections/load-shed.util"), exports);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;;;;;IAsBI;;;AAEJ,cAAc;AACd,sEAA4C;AAC5C,wFAA8D;AAC9D,8FAAoE;AACpE,sFAA4D;AAC5D,mFAAyD;AACzD,uFAA6D;AAC7D,+EAAqD;AAGrD,QAAQ;AACR,wEAA8C;AAC9C,8EAAoD;AACpD,8EAAoD;AACpD,uEAA6C;AAG7C,6CAA6C;AAE7C,mEAAmE;AACnE,gGAAsE;AACtE,8FAAoE;AACpE,iGAAuE;AACvE,yFAA+D;AAC/D,iGAAuE;AACvE,kGAAwE;AACxE,gGAAsE;AACtE,uFAA6D;AAC7D,mGAAyE;AACzE,0FAAgE;AAChE,yFAA+D;AAC/D,+EAAqD;AACrD,yEAA+C;AAC/C,gFAAsD;AACtD,wEAA8C;AAC9C,mFAAyD;AACzD,kFAAwD;AACxD,4EAAkD;AAClD,+EAAqD;AAErD,wBAAwB;AACxB,iGAAuE;AACvE,yGAA8E;AAC9E,4FAAkE;AAClE,qGAA2E;AAC3E,iGAAuE;AACvE,+FAAqE;AACrE,gGAAsE;AAEtE,eAAe;AACf,yEAA+C;AAE/C,qBAAqB;AACrB,4FAAkE;AAGlE,WAAW;AACX,gBAAgB;AAChB,uEAA6C;AAC7C,wEAA8C;AAE9C,yEAA+C;AAC/C,6FAAmE;AACnE,0EAAgD;AAChD,gFAAsD;AAEtD,sFAA4D;AAC5D,oFAA0D;AAE1D,gBAAgB;AAChB,4EAAkD;AAClD,gFAAsD;AACtD,wEAA8C;AAC9C,sEAA4C;AAC5C,kFAAwD;AACxD,6EAAmD;AAEnD,kBAAkB;AAClB,wEAA8C;AAE9C,iBAAiB;AACjB,+EAAqD;AACrD,mFAAyD;AAEzD,kBAAkB;AAClB,4EAAkD"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;;;;;IAsBI;;;AAEJ,cAAc;AACd,sEAA4C;AAC5C,wFAA8D;AAC9D,8FAAoE;AACpE,sFAA4D;AAC5D,mFAAyD;AACzD,uFAA6D;AAC7D,+EAAqD;AAGrD,QAAQ;AACR,wEAA8C;AAC9C,8EAAoD;AACpD,8EAAoD;AACpD,uEAA6C;AAG7C,6CAA6C;AAE7C,mEAAmE;AACnE,gGAAsE;AACtE,8FAAoE;AACpE,iGAAuE;AACvE,yFAA+D;AAC/D,iGAAuE;AACvE,kGAAwE;AACxE,gGAAsE;AACtE,uFAA6D;AAC7D,mGAAyE;AACzE,qGAA2E;AAC3E,0FAAgE;AAChE,yFAA+D;AAC/D,+EAAqD;AACrD,iFAAuD;AACvD,yEAA+C;AAC/C,gFAAsD;AACtD,wEAA8C;AAC9C,mFAAyD;AACzD,kFAAwD;AACxD,4EAAkD;AAClD,+EAAqD;AAErD,wBAAwB;AACxB,iGAAuE;AACvE,yGAA8E;AAC9E,4FAAkE;AAClE,qGAA2E;AAC3E,iGAAuE;AACvE,+FAAqE;AACrE,gGAAsE;AAEtE,eAAe;AACf,yEAA+C;AAE/C,qBAAqB;AACrB,4FAAkE;AAGlE,WAAW;AACX,gBAAgB;AAChB,uEAA6C;AAC7C,wEAA8C;AAE9C,yEAA+C;AAC/C,6FAAmE;AACnE,0EAAgD;AAChD,gFAAsD;AAEtD,sFAA4D;AAC5D,oFAA0D;AAE1D,gBAAgB;AAChB,4EAAkD;AAClD,gFAAsD;AACtD,wEAA8C;AAC9C,sEAA4C;AAC5C,kFAAwD;AACxD,6EAAmD;AAEnD,kBAAkB;AAClB,wEAA8C;AAE9C,iBAAiB;AACjB,+EAAqD;AACrD,mFAAyD;AAEzD,kBAAkB;AAClB,4EAAkD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@futdevpro/nts-dynamo",
3
- "version": "01.15.131",
3
+ "version": "01.15.134",
4
4
  "description": "Dynamic NodeTS (NodeJS-Typescript), MongoDB Backend System Framework by Future Development Program Ltd.",
5
5
  "DyBu_settings": {
6
6
  "packageType": "server-package",
@@ -378,7 +378,7 @@
378
378
  "@anthropic-ai/sdk": "~0.39.0",
379
379
  "@modelcontextprotocol/sdk": "~1.29.0",
380
380
  "better-sqlite3": "~12.4.1",
381
- "@futdevpro/fsm-dynamo": "1.16.34",
381
+ "@futdevpro/fsm-dynamo": "1.16.37",
382
382
  "@types/express": "4.17.21",
383
383
  "@types/geoip-lite": "~1.4.1",
384
384
  "@types/node": "~24.1.0",
@@ -0,0 +1,108 @@
1
+ import {
2
+ DYNTS_APP_VERSION_HEADER,
3
+ DyNTS_createAppVersionHeaderMiddleware,
4
+ DyNTS_resolveExposeHeaders,
5
+ } from './app-version-header.util';
6
+
7
+ /**
8
+ * FR-292 — a deployolt app-verzió minden válaszon.
9
+ *
10
+ * A garanciák, amiket ez a spec ŐRIZ:
11
+ * - ISMERETLEN verziónál NINCS header (a `unknown-version` hamis „új verzió"-riasztást adna),
12
+ * - a verzió a hívás pillanatában olvasódik (az app-params KÉSŐBB tölti be — egy induláskor
13
+ * befagyasztott érték örökre `unknown` maradna),
14
+ * - a CORS expose-lista BŐVÜL, nem felülíródik (különben más headerek olvashatatlanná válnának),
15
+ * - a bővítés IDEMPOTENS (kétszeri felszerelés nem duplikál).
16
+ */
17
+ describe('| FR-292 app-version response header', () => {
18
+
19
+ /** Minimal Express-válasz stub — csak a header-tárolás kell hozzá. */
20
+ function makeRes(): { headers: Record<string, string>; setHeader: Function; getHeader: Function } {
21
+ const headers: Record<string, string> = {};
22
+ return {
23
+ headers: headers,
24
+ setHeader: (name: string, value: string): void => { headers[name] = value; },
25
+ getHeader: (name: string): string | undefined => headers[name],
26
+ };
27
+ }
28
+
29
+ describe('| DyNTS_resolveExposeHeaders', () => {
30
+
31
+ it('| ures listabol a header neve lesz', () => {
32
+ expect(DyNTS_resolveExposeHeaders(undefined, 'X-App-Version')).toBe('X-App-Version');
33
+ expect(DyNTS_resolveExposeHeaders('', 'X-App-Version')).toBe('X-App-Version');
34
+ });
35
+
36
+
37
+ it('| a MEGLEVO listat megtartja es hozzafuz (nem felulir)', () => {
38
+ expect(DyNTS_resolveExposeHeaders('Content-Length, Content-Type', 'X-App-Version'))
39
+ .toBe('Content-Length, Content-Type, X-App-Version');
40
+ });
41
+
42
+
43
+ it('| idempotens — ha mar benne van, nem duplikal (kis-nagybetu fuggetlenul)', () => {
44
+ expect(DyNTS_resolveExposeHeaders('Content-Type, X-App-Version', 'X-App-Version'))
45
+ .toBe('Content-Type, X-App-Version');
46
+ expect(DyNTS_resolveExposeHeaders('content-type, x-app-version', 'X-App-Version'))
47
+ .toBe('content-type, x-app-version');
48
+ });
49
+ });
50
+
51
+
52
+ describe('| DyNTS_createAppVersionHeaderMiddleware', () => {
53
+
54
+ it('| kiirja a verziot ES exposeolja a CORS-nak', () => {
55
+ const res = makeRes();
56
+ let nextCalled: boolean = false;
57
+ DyNTS_createAppVersionHeaderMiddleware((): string => '01.15.460')(
58
+ {} as never, res as never, ((): void => { nextCalled = true; }) as never,
59
+ );
60
+ expect(res.headers[DYNTS_APP_VERSION_HEADER]).toBe('01.15.460');
61
+ expect(res.headers['Access-Control-Expose-Headers']).toContain(DYNTS_APP_VERSION_HEADER);
62
+ expect(nextCalled).toBe(true);
63
+ });
64
+
65
+
66
+ it('| ISMERETLEN verzional nem ir headert — de a lancot TOVABBENGEDI', () => {
67
+ for (const version of [undefined, '', 'unknown-version']) {
68
+ const res = makeRes();
69
+ let nextCalled: boolean = false;
70
+ DyNTS_createAppVersionHeaderMiddleware((): string | undefined => version)(
71
+ {} as never, res as never, ((): void => { nextCalled = true; }) as never,
72
+ );
73
+ expect(res.headers[DYNTS_APP_VERSION_HEADER]).withContext(String(version)).toBeUndefined();
74
+ expect(nextCalled).withContext(String(version)).toBe(true);
75
+ }
76
+ });
77
+
78
+
79
+ /**
80
+ * A verziót a HÍVÁSKOR kell olvasni: a `systemVersion` az app-params betöltésekor áll be,
81
+ * ami a middleware felszerelése UTÁN történik. Ha a middleware befagyasztaná az értéket,
82
+ * a fejléc örökre hiányozna — pont a friss deployon.
83
+ */
84
+ it('| a verziot HIVASKOR olvassa, nem felszereleskor', () => {
85
+ let current: string = 'unknown-version';
86
+ const middleware = DyNTS_createAppVersionHeaderMiddleware((): string => current);
87
+
88
+ const firstRes = makeRes();
89
+ middleware({} as never, firstRes as never, ((): void => undefined) as never);
90
+ expect(firstRes.headers[DYNTS_APP_VERSION_HEADER]).toBeUndefined();
91
+
92
+ current = '01.15.461'; // app-params betoltodott
93
+ const secondRes = makeRes();
94
+ middleware({} as never, secondRes as never, ((): void => undefined) as never);
95
+ expect(secondRes.headers[DYNTS_APP_VERSION_HEADER]).toBe('01.15.461');
96
+ });
97
+
98
+
99
+ it('| egyedi header-nev is hasznalhato', () => {
100
+ const res = makeRes();
101
+ DyNTS_createAppVersionHeaderMiddleware((): string => '1.2.3', 'X-Custom-Version')(
102
+ {} as never, res as never, ((): void => undefined) as never,
103
+ );
104
+ expect(res.headers['X-Custom-Version']).toBe('1.2.3');
105
+ expect(res.headers['Access-Control-Expose-Headers']).toBe('X-Custom-Version');
106
+ });
107
+ });
108
+ });
@@ -0,0 +1,74 @@
1
+ import * as Express from 'express';
2
+
3
+ /**
4
+ * FR-292 (2026-08-08) — a DEPLOYOLT app-verzió kiírása MINDEN válasz headerébe.
5
+ *
6
+ * **Miért kell:** a böngészőben nyitva hagyott SPA a memóriájában tartja a bundle-t, és a
7
+ * deploy után is a RÉGI kódot futtatja, amíg a felhasználó újra nem tölt. Ez nem elméleti:
8
+ * egy órákig nyitva hagyott dashboard-fülön hiányzó gombokat / régi viselkedést mutat, és
9
+ * a felhasználó (jogosan) azt hiszi, hogy a funkció nincs kész.
10
+ *
11
+ * Az itteni megoldás **nem polling**: a szerver minden válaszra ráteszi a saját deployolt
12
+ * app-verzióját, a kliens pedig a MÁR MEGLÉVŐ hívásaiból olvassa ki (ngx oldalon a
13
+ * `DyNX_AppVersion_Interceptor`). Nulla extra kérés, nulla extra terhelés — a jelzés
14
+ * „ingyen" utazik azon, ami úgyis megy.
15
+ *
16
+ * **CORS:** cross-origin kliens csak akkor OLVASHATJA a headert, ha az szerepel az
17
+ * `Access-Control-Expose-Headers`-ben. Ezért a middleware a meglévő értékhez HOZZÁFŰZ
18
+ * (nem felülír) — ezért kell a CORS-mount UTÁN felszerelni.
19
+ */
20
+
21
+ /** A deployolt app-verziót hordozó response-header neve (a kliens ezt olvassa). */
22
+ export const DYNTS_APP_VERSION_HEADER: string = 'X-App-Version';
23
+
24
+ /** Az `Access-Control-Expose-Headers` header neve — hogy ne szórjuk el string-literálként. */
25
+ const EXPOSE_HEADERS_HEADER: string = 'Access-Control-Expose-Headers';
26
+
27
+ /**
28
+ * Kiszámolja az `Access-Control-Expose-Headers` ÚJ értékét úgy, hogy a meglévő listát
29
+ * megtartja és a verzió-headert csak akkor fűzi hozzá, ha még nincs benne (idempotens).
30
+ *
31
+ * Tiszta függvény: Express nélkül unit-tesztelhető.
32
+ *
33
+ * @param currentValue a válaszon MÁR beállított érték (a CORS-middleware-től), ha van
34
+ * @param headerName a hozzáfűzendő header neve
35
+ */
36
+ export function DyNTS_resolveExposeHeaders(currentValue: string | undefined, headerName: string): string {
37
+ if (!currentValue) {
38
+ return headerName;
39
+ }
40
+ const alreadyExposed: boolean = currentValue
41
+ .split(',')
42
+ .some((entry: string): boolean => entry.trim().toLowerCase() === headerName.toLowerCase());
43
+
44
+ return alreadyExposed ? currentValue : `${currentValue}, ${headerName}`;
45
+ }
46
+
47
+ /**
48
+ * Express-middleware, ami minden válaszra ráteszi a deployolt app-verziót.
49
+ *
50
+ * A verziót FÜGGVÉNYKÉNT kapja (nem értékként), mert a `DyNTS_global_settings.systemVersion`
51
+ * az app-params betöltésekor áll be — egy induláskor kiolvasott érték `unknown-version`
52
+ * maradhatna örökre.
53
+ *
54
+ * @param getAppVersion a deployolt app-verziót visszaadó getter
55
+ * @param headerName felülírható header-név (default: `X-App-Version`)
56
+ */
57
+ export function DyNTS_createAppVersionHeaderMiddleware(
58
+ getAppVersion: () => string | undefined,
59
+ headerName: string = DYNTS_APP_VERSION_HEADER,
60
+ ): Express.RequestHandler {
61
+ return (_req: Express.Request, res: Express.Response, next: Express.NextFunction): void => {
62
+ const appVersion: string | undefined = getAppVersion();
63
+ // Ismeretlen verziónál NEM írunk headert: egy `unknown-version` jelzés rosszabb a
64
+ // semminél — a kliens „új verzió van" hamis riasztást adna belőle.
65
+ if (appVersion && appVersion !== 'unknown-version') {
66
+ res.setHeader(headerName, appVersion);
67
+ res.setHeader(
68
+ EXPOSE_HEADERS_HEADER,
69
+ DyNTS_resolveExposeHeaders(res.getHeader(EXPOSE_HEADERS_HEADER)?.toString(), headerName),
70
+ );
71
+ }
72
+ next();
73
+ };
74
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * FR-292 (2026-08-08) — a deployolt app-verziót hordozó response-header beállításai.
3
+ *
4
+ * A szerver minden válaszra ráteszi a saját app-verzióját, a kliens (ngx oldalon a
5
+ * `DyNX_AppVersion_Interceptor`) pedig ebből veszi észre, hogy a betöltött bundle elavult.
6
+ * Nulla extra kérés: a jelzés a már meglévő API-forgalmon utazik.
7
+ */
8
+ export interface DyNTS_AppVersionHeader_Settings {
9
+ /**
10
+ * Ki-/bekapcsolja a header-middleware-t. **Default: `true`** — informatív header, se PII,
11
+ * se belső részlet nincs benne, viszont nélküle minden app külön építené ugyanezt.
12
+ */
13
+ enabled?: boolean;
14
+
15
+ /** Felülírható header-név. Default: `X-App-Version`. */
16
+ headerName?: string;
17
+ }
@@ -1,6 +1,7 @@
1
1
  import { DyFM_EnvironmentFlag } from '@futdevpro/fsm-dynamo';
2
2
  import { DyNTS_RouteSecurity } from '../../_enums/route-security.enum';
3
3
  import { DyNTS_SecurityHeaders_Settings } from './security-headers-settings.interface';
4
+ import { DyNTS_AppVersionHeader_Settings } from './app-version-header-settings.interface';
4
5
  import { DyNTS_SocketSecurity } from '../../_modules/socket/_enums/socket-security.enum';
5
6
  import { DyNTS_GlobalLog_Settings } from './global-log-settings.interface';
6
7
  import { DyNTS_Environment_Settings } from './environment-settings.interface';
@@ -170,6 +171,13 @@ export interface DyNTS_Global_Settings {
170
171
  */
171
172
  securityHeaders?: DyNTS_SecurityHeaders_Settings;
172
173
 
174
+ /**
175
+ * FR-292 — a deployolt app-verzio kiirasa minden valasz headerebe (X-App-Version), hogy
176
+ * a bongeszoben NYITVA HAGYOTT SPA eszrevegye, ha kozben ujabb kliens-build ment ki.
177
+ * Default: enabled. Lasd {@link DyNTS_AppVersionHeader_Settings}.
178
+ */
179
+ appVersionHeader?: DyNTS_AppVersionHeader_Settings;
180
+
173
181
  /**
174
182
  * FR-193 — bedrock OOM korai-figyelmeztető heap-watchdog ({@link DyNTS_MemoryGuard}).
175
183
  * Ha `enabled`, a base `App` startup automatikusan feltelepíti. Periodikusan figyeli a
@@ -77,5 +77,30 @@ describe('| DyNTS_Usage_Controller', () => {
77
77
  expect((getUsageDataEndpoint as any)?.tasks).toBeDefined();
78
78
  expect((getUsageDataEndpoint as any)?.tasks.length).toBeGreaterThan(0);
79
79
  });
80
+
81
+ /**
82
+ * BFR-FDPPAL-001 — optional-auth mód (`auth_settings.optional: true`): a `getAuthService()`
83
+ * `undefined`-ot ad; a setupEndpoints NEM halhat meg TypeError-ral (boot-crash / restart-loop),
84
+ * a védett getUsageData kimarad, a session-endpointok élnek.
85
+ */
86
+ describe('| optional-auth mód — nincs auth service (BFR-FDPPAL-001)', () => {
87
+ beforeEach(() => {
88
+ (DyNTS_GlobalService.getAuthService as jasmine.Spy).and.returnValue(undefined);
89
+ });
90
+
91
+ it('| setupEndpoints nem dob, a session-endpointok megvannak', () => {
92
+ expect(() => controller.setupEndpoints()).not.toThrow();
93
+
94
+ expect(controller.endpoints.find(ep => ep.name === 'newSession')).toBeDefined();
95
+ expect(controller.endpoints.find(ep => ep.name === 'updateUsage')).toBeDefined();
96
+ expect(controller.endpoints.find(ep => ep.name === 'closeSession')).toBeDefined();
97
+ });
98
+
99
+ it('| a védett getUsageData endpoint KIMARAD (auth nélkül nem szolgálható ki)', () => {
100
+ controller.setupEndpoints();
101
+
102
+ expect(controller.endpoints.find(ep => ep.name === 'getUsageData')).toBeUndefined();
103
+ });
104
+ });
80
105
  });
81
106
 
@@ -1,7 +1,7 @@
1
1
 
2
2
  import { Request, Response } from 'express';
3
3
 
4
- import { DyFM_HttpCallType, DyFM_RelativeDate } from '@futdevpro/fsm-dynamo';
4
+ import { DyFM_HttpCallType, DyFM_Log, DyFM_RelativeDate } from '@futdevpro/fsm-dynamo';
5
5
  import {
6
6
  DyFM_usageModule_settings, DyFM_UsageSession
7
7
  } from '@futdevpro/fsm-dynamo/usage';
@@ -36,7 +36,8 @@ export class DyNTS_Usage_Controller extends DyNTS_Controller {
36
36
  let username: string;
37
37
 
38
38
  try {
39
- username = this.authService.getUsernameFromRequest(req);
39
+ // BFR-FDPPAL-001: optional-auth módban nincs auth service — a session issuer ilyenkor 'unknown'
40
+ username = this.authService?.getUsernameFromRequest(req) ?? 'unknown';
40
41
  } catch {
41
42
  username = 'unknown';
42
43
  }
@@ -99,28 +100,41 @@ export class DyNTS_Usage_Controller extends DyNTS_Controller {
99
100
  ],
100
101
  }),
101
102
 
102
- new DyNTS_Endpoint_Params({
103
- name: 'getUsageData',
104
- type: DyFM_HttpCallType.get,
105
- endpoint: DyFM_usageModule_settings.endPoints.getUsageData,
106
- preProcesses: [ this.authService.authenticate_tokenPerm_accUsageData ],
107
- tasks: [
108
- async (req: Request, res: Response, issuer: string): Promise<void> => {
109
- const range: DyFM_RelativeDate = req.params.range as DyFM_RelativeDate;
110
-
111
- const usageService = new DyNTS_Usage_DataService({
112
- issuer: issuer,
113
- });
114
-
115
- await usageService.getUsage(range);
116
-
117
- res.send(
118
- usageService.simplifiedDailyUsage
119
- );
120
- },
121
- ],
122
- }),
123
103
  ];
104
+
105
+ // BFR-FDPPAL-001: optional-auth módban (`auth_settings.optional: true`) a `getAuthService()`
106
+ // `undefined`-ot ad — a védett usage-lekérdezést ilyenkor KIHAGYJUK (az usage-adat auth nélkül
107
+ // nem szolgálható ki), de a boot nem halhat meg TypeError-ral; a session-endpointok élnek.
108
+ if (this.authService) {
109
+ this.endpoints.push(
110
+ new DyNTS_Endpoint_Params({
111
+ name: 'getUsageData',
112
+ type: DyFM_HttpCallType.get,
113
+ endpoint: DyFM_usageModule_settings.endPoints.getUsageData,
114
+ preProcesses: [ this.authService.authenticate_tokenPerm_accUsageData ],
115
+ tasks: [
116
+ async (req: Request, res: Response, issuer: string): Promise<void> => {
117
+ const range: DyFM_RelativeDate = req.params.range as DyFM_RelativeDate;
118
+
119
+ const usageService = new DyNTS_Usage_DataService({
120
+ issuer: issuer,
121
+ });
122
+
123
+ await usageService.getUsage(range);
124
+
125
+ res.send(
126
+ usageService.simplifiedDailyUsage
127
+ );
128
+ },
129
+ ],
130
+ }),
131
+ );
132
+ } else {
133
+ DyFM_Log.warn(
134
+ 'DyNTS_Usage_Controller: no auth service (optional-auth mode) - ' +
135
+ 'the protected "getUsageData" endpoint is SKIPPED; session endpoints remain available.'
136
+ );
137
+ }
124
138
  }
125
139
  }
126
140
 
@@ -725,5 +725,60 @@ describe('| DyNTS_DataService', () => {
725
725
  expect(r.changedFields).toEqual(['email']);
726
726
  });
727
727
  });
728
+
729
+ /**
730
+ * BFR-FDPTOKENSERVICE-011 — a validátor tömb-ága: egy OPCIONÁLIS `X[]`+`subObjectParams` mező
731
+ * `undefined` értéke (a normál „még nincs elem" eset) NEM szállhat el `forEach`-TypeError-ral.
732
+ * A required-hiányt a VP8-ág fogja — az `Array.isArray` őr nem gyengít semmit.
733
+ */
734
+ describe('| validateForSave — opcionális tömb-mező subObjectParams-szal (BFR-FDPTOKENSERVICE-011)', (): void => {
735
+ class WithOptArr extends DyFM_Metadata {
736
+ name?: string;
737
+ invoices?: { num?: string }[];
738
+ }
739
+
740
+ const buildParams = (required: boolean): DyFM_DataModel_Params<WithOptArr> =>
741
+ new DyFM_DataModel_Params<WithOptArr>({
742
+ dataName: 'with_opt_arr',
743
+ properties: {
744
+ invoices: {
745
+ key: 'invoices',
746
+ type: DyFM_BasicProperty_Type.array,
747
+ required: required,
748
+ subObjectParams: {
749
+ num: { key: 'num', type: DyFM_BasicProperty_Type.string, required: true },
750
+ },
751
+ },
752
+ },
753
+ });
754
+
755
+ it('| undefined opcionális tömb → NEM dob (a mentés nem bukhat a hiányzó elemeken)', async (): Promise<void> => {
756
+ const optSvc = new DyNTS_DataService<WithOptArr>(
757
+ Object.assign(new WithOptArr(), { name: 'x' }), buildParams(false), 'test-issuer',
758
+ );
759
+ await expectAsync(optSvc.validateForSave()).toBeResolved();
760
+ });
761
+
762
+ it('| tényleges tömbnél a sub-validáció VÁLTOZATLANUL fut (required sub-mező hiánya → 400)', async (): Promise<void> => {
763
+ const optSvc = new DyNTS_DataService<WithOptArr>(
764
+ Object.assign(new WithOptArr(), { name: 'x', invoices: [ {} ] }), buildParams(false), 'test-issuer',
765
+ );
766
+ let thrown: any = null;
767
+ try { await optSvc.validateForSave(); } catch (e) { thrown = e; }
768
+ expect(thrown).not.toBeNull();
769
+ expect(DyFM_Error.getErrorStatus(thrown)).toBe(400);
770
+ });
771
+
772
+ it('| required tömb hiánya továbbra is validációs 400 (az őr nem gyengít; hiányra a VP1 required-ág fut)', async (): Promise<void> => {
773
+ const reqSvc = new DyNTS_DataService<WithOptArr>(
774
+ Object.assign(new WithOptArr(), { name: 'x' }), buildParams(true), 'test-issuer',
775
+ );
776
+ let thrown: any = null;
777
+ try { await reqSvc.validateForSave(); } catch (e) { thrown = e; }
778
+ expect(thrown).not.toBeNull();
779
+ expect(DyFM_Error.getErrorStatus(thrown)).toBe(400);
780
+ expect(DyFM_Error.getErrorCode(thrown)).toContain('DyNTS-DS0-VP');
781
+ });
782
+ });
728
783
  });
729
784
 
@@ -1695,7 +1695,10 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
1695
1695
  });
1696
1696
  }
1697
1697
 
1698
- if (propertyParams.subObjectParams) {
1698
+ // BFR-FDPTOKENSERVICE-011: csak TÉNYLEGES tömbnél szállunk le a sub-objektumokba — egy
1699
+ // OPCIONÁLIS `X[]` mező `undefined` értéke (a normál „még nincs elem" eset) nem dönthet
1700
+ // `forEach`-TypeError-ba; a required-hiányt a fenti VP8-ág fogja, ez az őr nem gyengít.
1701
+ if (propertyParams.subObjectParams && Array.isArray(propertyValue)) {
1699
1702
  propertyValue.forEach((item: any): void => {
1700
1703
  this.validateDataByPropertyParams(item, propertyParams.subObjectParams, propertyParams);
1701
1704
  });
@@ -34,6 +34,8 @@ import { DyNTS_ClientSafeError_Util } from '../../_collections/client-safe-error
34
34
  import { DyNTS_getEnvironment_settings } from '../../_collections/get-environment-settings.util';
35
35
  import { DyNTS_createSecurityHeadersMiddleware } from '../../_collections/security-headers.util';
36
36
  import { DyNTS_createLoadShedMiddleware } from '../../_collections/load-shed.util';
37
+ import { DyNTS_createAppVersionHeaderMiddleware } from '../../_collections/app-version-header.util';
38
+ import { DyNTS_AppVersionHeader_Settings } from '../../_models/interfaces/app-version-header-settings.interface';
37
39
  import { DyNTS_SecurityHeaders_Settings } from '../../_models/interfaces/security-headers-settings.interface';
38
40
  import { startMongoReconnectGuard } from '../../_collections/mongo-reconnect-guard.util';
39
41
  import { DyNTS_resolveStaticCacheControlHeader } from '../../_collections/static-cache-control.util';
@@ -1296,6 +1298,9 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1296
1298
  this.mountSecurityHeaders(this.openExpress);
1297
1299
  // FR-041 — CORS allowlist enforcement. No-op if getCorsSettings() is not overridden.
1298
1300
  this.mountCors(this.openExpress);
1301
+ // FR-292 — a deployolt app-verzio minden valaszon. A CORS UTAN kell, mert az
1302
+ // `Access-Control-Expose-Headers`-hoz HOZZAFUZ (kulonben a CORS felulirna).
1303
+ this.mountAppVersionHeader(this.openExpress);
1299
1304
  // BFR-OVERSEER-009 — opt-in auto-load-shed 503 CRITICAL memória-nyomásnál (esszenciális utak kivételével).
1300
1305
  if (DyNTS_global_settings.memoryGuard?.loadShedEnabled) {
1301
1306
  this.openExpress.use(DyNTS_createLoadShedMiddleware());
@@ -1314,6 +1319,8 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1314
1319
  this.mountSecurityHeaders(this.secureExpress);
1315
1320
  // FR-041 — CORS allowlist enforcement (same as open express).
1316
1321
  this.mountCors(this.secureExpress);
1322
+ // FR-292 — a deployolt app-verzio minden valaszon (mint az open expressen).
1323
+ this.mountAppVersionHeader(this.secureExpress);
1317
1324
  // BFR-OVERSEER-009 — opt-in auto-load-shed 503 (mint az open expressen).
1318
1325
  if (DyNTS_global_settings.memoryGuard?.loadShedEnabled) {
1319
1326
  this.secureExpress.use(DyNTS_createLoadShedMiddleware());
@@ -1682,6 +1689,25 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1682
1689
  ));
1683
1690
  }
1684
1691
 
1692
+ /**
1693
+ * FR-292 — a deployolt app-verziot minden valaszra rateszi (`X-App-Version`), hogy a
1694
+ * bongeszoben NYITVA HAGYOTT SPA eszrevegye, ha kozben ujabb kliens-build ment ki.
1695
+ *
1696
+ * Nem polling: a jelzes a MAR MENO API-hivasokon utazik. Az `enabled` default TRUE — egy
1697
+ * informativ response-header, ami se PII-t, se belso reszletet nem szivarogtat, viszont
1698
+ * enelkul minden app kulon-kulon epitene ugyanezt.
1699
+ */
1700
+ protected mountAppVersionHeader(express: Express.Application): void {
1701
+ const settings: DyNTS_AppVersionHeader_Settings | undefined = DyNTS_global_settings.appVersionHeader;
1702
+ if (settings?.enabled === false) {
1703
+ return;
1704
+ }
1705
+ express.use(DyNTS_createAppVersionHeaderMiddleware(
1706
+ (): string => DyNTS_global_settings.systemVersion,
1707
+ settings?.headerName,
1708
+ ));
1709
+ }
1710
+
1685
1711
  protected mountCors(express: Express.Application): void {
1686
1712
  const settings: DyNTS_Cors_Settings | undefined = this.getCorsSettings?.();
1687
1713
  if (!settings) {
package/src/index.ts CHANGED
@@ -53,9 +53,11 @@ export * from './_models/interfaces/signed-url-verify-result.interface';
53
53
  export * from './_models/interfaces/static-client-settings.interface';
54
54
  export * from './_models/interfaces/cors-settings.interface';
55
55
  export * from './_models/interfaces/security-headers-settings.interface';
56
+ export * from './_models/interfaces/app-version-header-settings.interface';
56
57
  export * from './_models/interfaces/db-query-options.interface';
57
58
  export * from './_models/interfaces/migration-entry.interface';
58
59
  export * from './_collections/security-headers.util';
60
+ export * from './_collections/app-version-header.util';
59
61
  export * from './_collections/signed-url.util';
60
62
  export * from './_collections/client-safe-error.util';
61
63
  export * from './_collections/load-shed.util';