@futdevpro/nts-dynamo 1.15.34 → 1.15.37

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 (32) hide show
  1. package/.dynamo/logs/cicd-pipeline/output.log +2630 -0
  2. package/.dynamo/logs/cicd-pipeline/status.json +321 -0
  3. package/build/_models/interfaces/cors-settings.interface.d.ts +52 -0
  4. package/build/_models/interfaces/cors-settings.interface.d.ts.map +1 -0
  5. package/build/_models/interfaces/cors-settings.interface.js +3 -0
  6. package/build/_models/interfaces/cors-settings.interface.js.map +1 -0
  7. package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.d.ts +24 -2
  8. package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.d.ts.map +1 -1
  9. package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.js +124 -0
  10. package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.js.map +1 -1
  11. package/build/_modules/ai/_services/ai-llm.service-base.d.ts +56 -1
  12. package/build/_modules/ai/_services/ai-llm.service-base.d.ts.map +1 -1
  13. package/build/_modules/ai/_services/ai-llm.service-base.js +122 -0
  14. package/build/_modules/ai/_services/ai-llm.service-base.js.map +1 -1
  15. package/build/_services/server/app.server.d.ts +25 -0
  16. package/build/_services/server/app.server.d.ts.map +1 -1
  17. package/build/_services/server/app.server.js +64 -0
  18. package/build/_services/server/app.server.js.map +1 -1
  19. package/build/index.d.ts +1 -0
  20. package/build/index.d.ts.map +1 -1
  21. package/build/index.js +1 -0
  22. package/build/index.js.map +1 -1
  23. package/package.json +2 -2
  24. package/pnpm-workspace.yaml +3 -0
  25. package/src/_models/interfaces/cors-settings.interface.spec.ts +52 -0
  26. package/src/_models/interfaces/cors-settings.interface.ts +56 -0
  27. package/src/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.tools.spec.ts +106 -0
  28. package/src/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.ts +154 -5
  29. package/src/_modules/ai/_services/ai-llm.service-base.ts +180 -2
  30. package/src/_services/server/app.server.ts +82 -3
  31. package/src/index.ts +1 -0
  32. /package/{pipeline.cicd.config.json → .dynamo/pipeline.cicd.config.json} +0 -0
@@ -1,20 +1,23 @@
1
1
  import { OpenAI } from 'openai';
2
2
 
3
- import { DyFM_OAI_Settings, DyFM_OAI_Model, DyFM_OAI_CallSettings } from '@futdevpro/fsm-dynamo/ai/open-ai';
3
+ import { DyFM_OAI_Settings, DyFM_OAI_Model, DyFM_OAI_CallSettings, DyFM_OAI_Models } from '@futdevpro/fsm-dynamo/ai/open-ai';
4
4
  import { DyFM_AnyError, DyFM_Error, DyFM_Error_Settings, DyFM_Log, DyFM_notNull, DyFM_Object } from '@futdevpro/fsm-dynamo';
5
- import {
6
- DyFM_AI_Message,
5
+ import {
6
+ DyFM_AI_Message,
7
7
  DyFM_AI_MessageRole,
8
8
  DyFM_AI_Provider,
9
9
  DyFM_AI_ProviderCapabilities,
10
10
  DyFM_AI_CallSettings,
11
11
  DyFM_AI_Config,
12
- DyFM_AI_LLM_Response
12
+ DyFM_AI_LLM_Response,
13
+ DyFM_AI_Tool,
14
+ DyFM_AI_ToolCall,
15
+ DyFM_AI_ModelInfo
13
16
  } from '@futdevpro/fsm-dynamo/ai';
14
17
 
15
18
  import { DyNTS_global_settings } from '../../../../../_collections/global-settings.const';
16
19
  import { ChatCompletion } from 'openai/resources';
17
- import { ChatCompletionCreateParamsBase, ChatCompletionMessageParam } from 'openai/resources/chat/completions';
20
+ import { ChatCompletionCreateParamsBase, ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources/chat/completions';
18
21
  import { DyNTS_AI_CostEventCallback } from '../../../_models/interfaces/dynts-ai-cost-event-callback.interface';
19
22
  import { DyNTS_OAI_LLM_Predefined_Requests } from '../_models/interfaces/oai-llm-predefined-requests.interface';
20
23
  import { DyNTS_OAI_global_settings } from '../_collections/oai-global-settings.const';
@@ -567,6 +570,152 @@ export class DyNTS_OAI_LLM_ServiceBase extends DyNTS_AI_LLM_ServiceBase {
567
570
  }
568
571
  }
569
572
 
573
+ //#region Function calling (tool use) — FR-047
574
+
575
+ /** OpenAI tool-kepes modell-registry (a base capability-precheck-jehez). */
576
+ protected override getModelRegistry(): DyFM_AI_ModelInfo[] {
577
+ return DyFM_OAI_Models;
578
+ }
579
+
580
+ /**
581
+ * Egy OpenAI-kor a tool-loop-ban: felepiti a natív request-et (system + a teljes beszelgetes
582
+ * tool-uzenetekkel + tools), meghivja az API-t, es normalizalt DyFM_AI_LLM_Response-t ad vissza
583
+ * (toolCalls feltoltve, ha a model tool-t akar hivni). FR-002 cost-event: 'llm-tool-use'.
584
+ */
585
+ protected override async callModelWithTools(
586
+ set: {
587
+ conversation: DyFM_AI_Message[];
588
+ tools: DyFM_AI_Tool[];
589
+ settings?: DyFM_OAI_CallSettings;
590
+ issuer: string;
591
+ }
592
+ ): Promise<DyFM_AI_LLM_Response> {
593
+ try {
594
+ const settings: DyFM_OAI_CallSettings = set.settings ?? this.defaultSettings;
595
+
596
+ const messages: ChatCompletionMessageParam[] = [
597
+ this.toOpenAIMessage(this.getDefaultSystemMessage(settings)),
598
+ ...set.conversation.map((message: DyFM_AI_Message) => this.toOpenAIMessage(message)),
599
+ ];
600
+
601
+ const input: ChatCompletionCreateParamsBase = {
602
+ model: settings?.useModel ?? this.defaultModel,
603
+ messages: messages,
604
+ temperature: DyFM_notNull(settings?.temperature) ? settings.temperature : this.defaultSettings.temperature,
605
+ max_completion_tokens: DyFM_notNull(settings?.maxTokens) ? settings.maxTokens : this.defaultSettings.maxTokens,
606
+ top_p: DyFM_notNull(settings?.topP) ? settings.topP : this.defaultSettings.topP,
607
+ frequency_penalty: DyFM_notNull(settings?.frequencyPenalty) ? settings.frequencyPenalty : this.defaultSettings.frequencyPenalty,
608
+ presence_penalty: DyFM_notNull(settings?.presencePenalty) ? settings.presencePenalty : this.defaultSettings.presencePenalty,
609
+ tools: set.tools.map((tool: DyFM_AI_Tool) => this.toOpenAITool(tool)),
610
+ tool_choice: 'auto',
611
+ };
612
+
613
+ const start: number = Date.now();
614
+ const result: ChatCompletion = await this.openai.chat.completions.create(input) as ChatCompletion;
615
+ const durationMs: number = Date.now() - start;
616
+
617
+ const message = result.choices[0].message;
618
+
619
+ const toolCalls: DyFM_AI_ToolCall[] = (message.tool_calls ?? [])
620
+ .filter((toolCall) => toolCall.type === 'function')
621
+ .map((toolCall) => ({
622
+ id: toolCall.id,
623
+ name: toolCall.function.name,
624
+ arguments: this.parseToolArguments(toolCall.function.arguments),
625
+ }));
626
+
627
+ this.emitCostEvent({
628
+ callType: 'llm-tool-use',
629
+ provider: this.aiProvider,
630
+ model: String(input.model ?? this.defaultModel),
631
+ tokensUsed: {
632
+ input: result.usage?.prompt_tokens ?? 0,
633
+ output: result.usage?.completion_tokens ?? 0,
634
+ total: result.usage?.total_tokens ?? 0,
635
+ },
636
+ durationMs: durationMs,
637
+ issuer: set.issuer,
638
+ timestamp: new Date(),
639
+ });
640
+
641
+ return {
642
+ content: message.content ?? '',
643
+ toolCalls: toolCalls.length ? toolCalls : undefined,
644
+ usage: result.usage ? {
645
+ promptTokens: result.usage.prompt_tokens,
646
+ completionTokens: result.usage.completion_tokens,
647
+ totalTokens: result.usage.total_tokens,
648
+ } : undefined,
649
+ model: result.model,
650
+ finishReason: result.choices[0].finish_reason,
651
+ stopReason: result.choices[0].finish_reason,
652
+ rawResponse: result,
653
+ };
654
+ } catch (error) {
655
+ throw new DyFM_Error({
656
+ ...this.getDefaultErrorSettings('callModelWithTools', error, set.issuer),
657
+ errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OLSB-TC0`,
658
+ });
659
+ }
660
+ }
661
+
662
+ /** Agnosztikus tool-definicio → OpenAI ChatCompletionTool. */
663
+ protected toOpenAITool(tool: DyFM_AI_Tool): ChatCompletionTool {
664
+ return {
665
+ type: 'function',
666
+ function: {
667
+ name: tool.name,
668
+ description: tool.description,
669
+ parameters: tool.parameters as Record<string, unknown>,
670
+ },
671
+ };
672
+ }
673
+
674
+ /**
675
+ * Agnosztikus DyFM_AI_Message → OpenAI ChatCompletionMessageParam.
676
+ * Kezeli a tool-result (role:'tool') es a tool-hivo assistant (tool_calls) uzeneteket is.
677
+ */
678
+ protected toOpenAIMessage(message: DyFM_AI_Message): ChatCompletionMessageParam {
679
+ if (message.role === DyFM_AI_MessageRole.tool) {
680
+ return {
681
+ role: 'tool',
682
+ tool_call_id: message.toolCallId ?? '',
683
+ content: message.content,
684
+ };
685
+ }
686
+
687
+ if (message.role === DyFM_AI_MessageRole.assistant && message.toolCalls?.length) {
688
+ return {
689
+ role: 'assistant',
690
+ content: message.content || null,
691
+ tool_calls: message.toolCalls.map((toolCall: DyFM_AI_ToolCall) => ({
692
+ id: toolCall.id,
693
+ type: 'function' as const,
694
+ function: {
695
+ name: toolCall.name,
696
+ arguments: JSON.stringify(toolCall.arguments),
697
+ },
698
+ })),
699
+ };
700
+ }
701
+
702
+ return {
703
+ role: message.role as 'system' | 'user' | 'assistant',
704
+ content: message.content,
705
+ };
706
+ }
707
+
708
+ /** Tool-argumentumok parse-olasa (OpenAI JSON-string → object); hibas JSON eseten ures object. */
709
+ protected parseToolArguments(raw: string): Record<string, unknown> {
710
+ try {
711
+ return JSON.parse(raw || '{}');
712
+ } catch {
713
+ return {};
714
+ }
715
+ }
716
+
717
+ //#endregion
718
+
570
719
  // async askJSONListQuestion(
571
720
  // question: string,
572
721
  // issuer: string,
@@ -1,5 +1,16 @@
1
1
  import { DyNTS_AI_Provider_ServiceBase } from './ai-provider.service-base';
2
- import { DyFM_AI_CallSettings, DyFM_AI_Message, DyFM_AI_LLM_Response, DyFM_AI_MessageRole } from '@futdevpro/fsm-dynamo/ai';
2
+ import {
3
+ DyFM_AI_CallSettings,
4
+ DyFM_AI_Message,
5
+ DyFM_AI_LLM_Response,
6
+ DyFM_AI_MessageRole,
7
+ DyFM_AI_Tool,
8
+ DyFM_AI_ToolCall,
9
+ DyFM_AI_ToolResult,
10
+ DyFM_AI_ToolHandlers,
11
+ DyFM_AI_ModelInfo,
12
+ DyFM_AI_ModelRegistry_Util,
13
+ } from '@futdevpro/fsm-dynamo/ai';
3
14
  import { DyFM_Error, DyFM_Error_Settings, DyFM_getLocalStackLocation, DyFM_Log, DyFM_Object } from '@futdevpro/fsm-dynamo';
4
15
  import {
5
16
  DyFM_AI_GenericSelect_Input,
@@ -40,7 +51,174 @@ export abstract class DyNTS_AI_LLM_ServiceBase<
40
51
  }
41
52
 
42
53
  defaultLogReplacer: string = '...long-context...';
43
-
54
+
55
+ //////////////////////////////////////////////////////////////////////////////////////////
56
+ // FUNCTION CALLING (TOOL USE) — FR-047 //
57
+ //////////////////////////////////////////////////////////////////////////////////////////
58
+ // Provider-agnostic agent-loop. The loop logic lives here ONCE (ported from the legacy
59
+ // FDPNTS_GPT_ControlService.getAnswerWithTools); each provider only overrides
60
+ // `callModelWithTools` (one provider turn) and `getModelRegistry` (capability honesty).
61
+ // Tools are a REQUEST parameter (not on settings), per the FR-047 design.
62
+
63
+ /**
64
+ * A provider tool-kepes modell-registry-je (override providerenkent — pl. DyFM_OAI_Models).
65
+ * Ures default → a modell tool-kepessege ismeretlen → a precheck elutasitja (honesty).
66
+ */
67
+ protected getModelRegistry(): DyFM_AI_ModelInfo[] {
68
+ return [];
69
+ }
70
+
71
+ /**
72
+ * Ellenorzi, hogy a feloldott modell tamogatja-e a function calling-ot; ha nem, beszedes
73
+ * hibaval elhasal MIELOTT barmilyen API-hivas tortenne (kritikus pl. a Local providernel).
74
+ */
75
+ protected assertToolsSupported(modelId: string): void {
76
+ if (!DyFM_AI_ModelRegistry_Util.modelSupportsTools(this.getModelRegistry(), modelId)) {
77
+ throw new DyFM_Error({
78
+ message: `Model '${modelId}' does not support function calling (provider: ${this.aiProvider})`,
79
+ userMessage: `The selected AI model does not support tools.`,
80
+ errorCode: 'DyNTS-AILSB-TLC0',
81
+ });
82
+ }
83
+ }
84
+
85
+ /**
86
+ * GPT valasz Function Calling Agent Tool-okkal — provider-agnosztikus agent-loop.
87
+ * @description A `tools` definiciok + `toolHandlers` alapjan tool-loop-ot futtat: hivja a
88
+ * modellt (callModelWithTools) → ha a valaszban tool-call van, lefuttatja a regisztralt
89
+ * handler-t (runToolCall, never-throw) es az eredmenyt visszafuzi → ismetli, amig a model
90
+ * vegso (tool-call nelkuli) valaszt ad, vagy a maxIterations limitet eleri. A tool-ok a
91
+ * REQUEST-parameterben jonnek, nem a settings-ben.
92
+ */
93
+ async requestWithTools(
94
+ set: {
95
+ conversation: DyFM_AI_Message[];
96
+ tools: DyFM_AI_Tool[];
97
+ toolHandlers: DyFM_AI_ToolHandlers;
98
+ settings?: T_AISettings;
99
+ issuer: string;
100
+ maxIterations?: number;
101
+ }
102
+ ): Promise<DyFM_AI_LLM_Response> {
103
+ const modelId: string = (set.settings?.useModel ?? this.defaultModel) as string;
104
+ this.assertToolsSupported(modelId);
105
+
106
+ return this.runToolLoop(set);
107
+ }
108
+
109
+ /**
110
+ * Egy provider-kor a tool-loop-ban: elkuldi a beszelgetest + tool-okat, normalizalt valaszt ad.
111
+ * @description Override-olando providerenkent (OpenAI / Anthropic / Google / …). A default
112
+ * elhasal — egy provider, ami nem implementalja, tool-use-t nem tud kiszolgalni.
113
+ */
114
+ protected async callModelWithTools(
115
+ _set: {
116
+ conversation: DyFM_AI_Message[];
117
+ tools: DyFM_AI_Tool[];
118
+ settings?: T_AISettings;
119
+ issuer: string;
120
+ }
121
+ ): Promise<DyFM_AI_LLM_Response> {
122
+ throw new DyFM_Error({
123
+ message: `callModelWithTools is not implemented for provider '${this.aiProvider}'`,
124
+ userMessage: `Function calling is not available for this AI provider yet.`,
125
+ errorCode: 'DyNTS-AILSB-TLN0',
126
+ });
127
+ }
128
+
129
+ /**
130
+ * A provider-agnosztikus agent-loop torzse. A bemeno `conversation` ele a provider teszi a
131
+ * system-message-et (callModelWithTools); a loop a tool-call/tool-result uzeneteket fuzi hozza.
132
+ */
133
+ protected async runToolLoop(
134
+ set: {
135
+ conversation: DyFM_AI_Message[];
136
+ tools: DyFM_AI_Tool[];
137
+ toolHandlers: DyFM_AI_ToolHandlers;
138
+ settings?: T_AISettings;
139
+ issuer: string;
140
+ maxIterations?: number;
141
+ }
142
+ ): Promise<DyFM_AI_LLM_Response> {
143
+ const maxIterations: number = set.maxIterations ?? 8;
144
+ const conversation: DyFM_AI_Message[] = [...set.conversation];
145
+
146
+ for (let iteration: number = 0; iteration < maxIterations; iteration++) {
147
+ const response: DyFM_AI_LLM_Response = await this.callModelWithTools({
148
+ conversation: conversation,
149
+ tools: set.tools,
150
+ settings: set.settings,
151
+ issuer: set.issuer,
152
+ });
153
+
154
+ // nincs tool-hivas → ez a vegso valasz
155
+ if (!response.toolCalls?.length) {
156
+ return response;
157
+ }
158
+
159
+ // a model tool-hivo (assistant) uzenetet visszatesszuk a kontextusba
160
+ conversation.push({
161
+ role: DyFM_AI_MessageRole.assistant,
162
+ content: response.content ?? '',
163
+ toolCalls: response.toolCalls,
164
+ });
165
+
166
+ // minden tool-hivast lefuttatunk (never-throw), es az eredmenyt visszaadjuk a modellnek
167
+ const results: DyFM_AI_ToolResult[] = await Promise.all(
168
+ response.toolCalls.map((call: DyFM_AI_ToolCall) => this.runToolCall(call, set.toolHandlers))
169
+ );
170
+
171
+ results.forEach((result: DyFM_AI_ToolResult) => {
172
+ conversation.push({
173
+ role: DyFM_AI_MessageRole.tool,
174
+ content: result.content,
175
+ toolCallId: result.toolCallId,
176
+ });
177
+ });
178
+ }
179
+
180
+ // a tool-loop nem konvergalt a limiten belul
181
+ throw new DyFM_Error({
182
+ message: `Tool loop did not converge within ${maxIterations} iterations`,
183
+ userMessage: `We encountered an error while running AI tools, please contact the responsible development team.`,
184
+ errorCode: 'DyNTS-AILSB-TL0',
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Egy tool-hivas lefuttatasa a regisztralt handler-rel. SOHA nem dob — a tool-hiba string-kent
190
+ * megy vissza a modellnek (hogy korrigalhasson), hianyzo handler eseten is informativ uzenet
191
+ * (soha nem [object Object]).
192
+ */
193
+ protected async runToolCall(
194
+ call: DyFM_AI_ToolCall,
195
+ toolHandlers: DyFM_AI_ToolHandlers
196
+ ): Promise<DyFM_AI_ToolResult> {
197
+ const handler = toolHandlers[call.name];
198
+
199
+ if (!handler) {
200
+ return {
201
+ toolCallId: call.id,
202
+ content: `ERROR: no handler registered for tool '${call.name}'`,
203
+ isError: true,
204
+ };
205
+ }
206
+
207
+ try {
208
+ return {
209
+ toolCallId: call.id,
210
+ content: await handler(call.arguments),
211
+ };
212
+ } catch (error) {
213
+ return {
214
+ toolCallId: call.id,
215
+ content: `ERROR executing tool '${call.name}': ` +
216
+ `${error instanceof Error ? error.message : String(error)}`,
217
+ isError: true,
218
+ };
219
+ }
220
+ }
221
+
44
222
  // Core abstract methods
45
223
  /**
46
224
  * Call LLM with system and user messages
@@ -47,6 +47,9 @@ import {
47
47
  import {
48
48
  DyNTS_StaticClient_Settings
49
49
  } from '../../_models/interfaces/static-client-settings.interface';
50
+ import {
51
+ DyNTS_Cors_Settings
52
+ } from '../../_models/interfaces/cors-settings.interface';
50
53
  import { DyNTS_SingletonService } from '../base/singleton.service';
51
54
  import { DyNTS_GlobalService } from '../core/global.service';
52
55
  import { DyNTS_RoutingModule } from '../route/routing-module.service';
@@ -1074,20 +1077,24 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1074
1077
  */
1075
1078
  protected async initOpenExpress(): Promise<void> {
1076
1079
  if (this.fnLogs) DyFM_Log.log('\nfn:. initOpenExpress');
1077
- this.openExpress = Express();
1080
+ this.openExpress = Express();
1078
1081
  this.openExpress.set('maxHeaderSize', 10 * megabyte); // 1024 * 1024 * 10, // 10MB
1079
1082
  this.openExpress.use(BodyParser.urlencoded(this._portSettings.httpUrlencoded));
1080
1083
  this.openExpress.use(BodyParser.json(this._portSettings.httpJson));
1084
+ // FR-041 — CORS allowlist enforcement. No-op if getCorsSettings() is not overridden.
1085
+ this.mountCors(this.openExpress);
1081
1086
  }
1082
1087
 
1083
1088
  /**
1084
- *
1089
+ *
1085
1090
  */
1086
1091
  protected async initSecureExpress(): Promise<void> {
1087
1092
  if (this.fnLogs) DyFM_Log.log('\nfn:. initSecureExpress');
1088
- this.secureExpress = Express();
1093
+ this.secureExpress = Express();
1089
1094
  this.secureExpress.use(BodyParser.urlencoded(this._portSettings.httpsUrlencoded));
1090
1095
  this.secureExpress.use(BodyParser.json(this._portSettings.httpsJson));
1096
+ // FR-041 — CORS allowlist enforcement (same as open express).
1097
+ this.mountCors(this.secureExpress);
1091
1098
 
1092
1099
  const options = {
1093
1100
  key: FileSystem.readFileSync(this._cert.keyPath),
@@ -1412,6 +1419,69 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1412
1419
  }
1413
1420
  }
1414
1421
 
1422
+ /**
1423
+ * FR-041 — CORS middleware. Echoes a matching `Access-Control-Allow-Origin`
1424
+ * for any request whose Origin header is in `getCorsSettings().allowedOrigins`,
1425
+ * adds the canonical `Access-Control-*` companion headers, and short-circuits
1426
+ * the OPTIONS preflight with a 204.
1427
+ *
1428
+ * No-op when the subclass does NOT override `getCorsSettings()` — preserves
1429
+ * back-compat for apps that have always been same-origin and never needed
1430
+ * CORS (the typical pre-FR-041 case).
1431
+ *
1432
+ * Why hand-rolled instead of the `cors` npm package:
1433
+ * - ~30 lines, zero new dependency.
1434
+ * - Full control over Vary/credentials/preflight semantics.
1435
+ * - Aligns with FDP "no unnecessary deps" philosophy.
1436
+ */
1437
+ protected mountCors(express: Express.Application): void {
1438
+ const settings: DyNTS_Cors_Settings | undefined = this.getCorsSettings?.();
1439
+ if (!settings) {
1440
+ return;
1441
+ }
1442
+
1443
+ const allowCreds: boolean = settings.allowCredentials !== false;
1444
+ const methods: string[] = settings.allowedMethods ?? [
1445
+ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS',
1446
+ ];
1447
+ const headers: string[] = settings.allowedHeaders ?? [
1448
+ 'Authorization', 'Content-Type', 'X-Admin-Key', 'X-Requested-With',
1449
+ ];
1450
+ const exposed: string[] = settings.exposedHeaders ?? [
1451
+ 'Content-Length', 'Content-Type',
1452
+ ];
1453
+ const maxAge: number = settings.maxAgeSeconds ?? 86400;
1454
+
1455
+ const isAllowed: (origin: string) => boolean = (origin: string): boolean => {
1456
+ if (settings.allowedOrigins === '*') return true;
1457
+ if (typeof settings.allowedOrigins === 'function') {
1458
+ return settings.allowedOrigins(origin);
1459
+ }
1460
+ return settings.allowedOrigins.includes(origin);
1461
+ };
1462
+
1463
+ express.use((req: Express.Request, res: Express.Response, next: Express.NextFunction): void => {
1464
+ const origin: string | undefined = req.headers.origin;
1465
+ if (origin && isAllowed(origin)) {
1466
+ // Echo the matching origin (NOT wildcard) because credentials require it.
1467
+ res.setHeader('Access-Control-Allow-Origin', origin);
1468
+ if (allowCreds) {
1469
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
1470
+ }
1471
+ res.setHeader('Access-Control-Allow-Methods', methods.join(', '));
1472
+ res.setHeader('Access-Control-Allow-Headers', headers.join(', '));
1473
+ res.setHeader('Access-Control-Expose-Headers', exposed.join(', '));
1474
+ res.setHeader('Access-Control-Max-Age', String(maxAge));
1475
+ res.setHeader('Vary', 'Origin');
1476
+ }
1477
+ if (req.method === 'OPTIONS') {
1478
+ res.status(204).end();
1479
+ return;
1480
+ }
1481
+ next();
1482
+ });
1483
+ }
1484
+
1415
1485
  /**
1416
1486
  * Ha getStaticClientSettings() visszaad beállításokat, a client static fájlok a '/' alatt
1417
1487
  * lesznek kiszolgálva (API route-ok után). SPA fallback opcionális (fallbackPath).
@@ -1580,6 +1650,15 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1580
1650
  */
1581
1651
  getStaticClientSettings?(): DyNTS_StaticClient_Settings | undefined;
1582
1652
 
1653
+ /**
1654
+ * FR-041 — Ha megadva, CORS middleware mount-olódik mindkét Express instance-ra
1655
+ * (open + secure). Az `allowedOrigins` listán szereplő Origin-eket echo-zza vissza,
1656
+ * `Access-Control-Allow-Credentials: true`-val (default) együtt a Bearer JWT
1657
+ * cross-domain auth flow miatt. OPTIONS preflight 204-gyel rövidre záródik.
1658
+ * Ha undefined → no CORS headers, no preflight handling — back-compat.
1659
+ */
1660
+ getCorsSettings?(): DyNTS_Cors_Settings | undefined;
1661
+
1583
1662
  /**
1584
1663
  * MISSING Description (TODO)
1585
1664
  */
package/src/index.ts CHANGED
@@ -50,6 +50,7 @@ export * from './_models/interfaces/global-service-settings.interface';
50
50
  export * from './_models/interfaces/global-settings.interface';
51
51
  export * from './_models/interfaces/routing-module-settings.interface';
52
52
  export * from './_models/interfaces/static-client-settings.interface';
53
+ export * from './_models/interfaces/cors-settings.interface';
53
54
 
54
55
  // models/CONTROL MODELS
55
56
  export * from './_models/control-models/api-call-params.control-model';