@kumori/aurora-backend-handler 1.0.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.
@@ -0,0 +1,1031 @@
1
+ import { Link, MarketplaceItem, MarketplaceService, Notification, Service } from "@hestekumori/aurora-interfaces";
2
+ import { eventHelper } from "../backend-handler";
3
+ import { environment } from "../environment";
4
+
5
+ import {
6
+ getWebSocketStatus,
7
+ initializeGlobalWebSocketClient,
8
+ makeGlobalWebSocketRequest,
9
+ } from "../websocket-manager";
10
+ import { deployServiceHelper } from "./deploy-service-helper";
11
+
12
+ interface DeploymentParameter {
13
+ name: string;
14
+ value: string | number | boolean;
15
+ type: "string" | "number" | "boolean";
16
+ }
17
+ interface DeploymentResourceSpec {
18
+ name: string;
19
+ resource: string;
20
+ }
21
+ interface DeploymentVolatileVolumeSpec {
22
+ name: string;
23
+ volume: {
24
+ kind: string;
25
+ size: number;
26
+ unit: "G";
27
+ };
28
+ }
29
+ interface Secret<VARIANT> {
30
+ secret: VARIANT;
31
+ }
32
+ interface CA<VARIANT> {
33
+ ca: VARIANT;
34
+ }
35
+ interface Volume<VARIANT> {
36
+ volume: VARIANT;
37
+ }
38
+ interface Domain<VARIANT> {
39
+ domain: VARIANT;
40
+ }
41
+ interface Port<VARIANT> {
42
+ port: VARIANT;
43
+ }
44
+ interface Certificate<VARIANT> {
45
+ certificate: VARIANT;
46
+ }
47
+ type DeploymentResource =
48
+ | CA<DeploymentResourceSpec>
49
+ | Secret<DeploymentResourceSpec>
50
+ | Volume<DeploymentResourceSpec>
51
+ | Volume<DeploymentVolatileVolumeSpec>
52
+ | Domain<DeploymentResourceSpec>
53
+ | Port<DeploymentResourceSpec>
54
+ | Certificate<DeploymentResourceSpec>;
55
+ let pendingLinks = new Map<string, Link[]>();
56
+ /**
57
+ * Function to deploy a marketplace item
58
+ * @param item A MarketplaceService object containing the item to deploy
59
+ */
60
+ export const deployMarketplaceItem = async (item: MarketplaceService) => {
61
+ const complexDeployment = item.deploymentData.serverChannels.find(
62
+ (channel) => channel.isPublic
63
+ );
64
+ if (complexDeployment !== undefined) {
65
+ try {
66
+ const formData = await deployServiceHelper(item.deploymentData, item);
67
+
68
+ const url = new URL(
69
+ `${environment.apiServer.baseUrl}/api/${environment.apiServer.apiVersion}/tenant/${item.deploymentData.tenant}/service/${item.deploymentData.name}`
70
+ );
71
+ url.searchParams.append("dryrun", "false");
72
+ url.searchParams.append("accept", "true");
73
+ url.searchParams.append("wait", "30000");
74
+ url.searchParams.append("validate", "true");
75
+ url.searchParams.append("dsl", "true");
76
+
77
+ const response = await fetch(url.toString(), {
78
+ method: "POST",
79
+ body: formData,
80
+ });
81
+
82
+ if (!response.ok) {
83
+ throw new Error(`HTTP error! status: ${response.status}`);
84
+ }
85
+
86
+ const jsonResponse = await response.json();
87
+
88
+ const isTimeout = jsonResponse?.events?.some(
89
+ (event: any) => event.content === "_timeout_"
90
+ );
91
+
92
+ if (isTimeout) {
93
+ console.error("Timeout en la petición:", {
94
+ isOk: false,
95
+ code: "TIMEOUT",
96
+ error: "_timeout_",
97
+ });
98
+ } else {
99
+ if (item.deploymentData.links.length > 0) {
100
+ pendingLinks.set(item.deploymentData.name, item.deploymentData.links);
101
+ linkPendingServices(item.deploymentData, "");
102
+ }
103
+ }
104
+ } catch (err) {
105
+ console.error("Error en la petición de despliegue de servicio:", err);
106
+ }
107
+ } else {
108
+ const deploymentConfigParameters: DeploymentParameter[] = item.parameterList
109
+ .filter(
110
+ (resource) =>
111
+ resource.type === "string" ||
112
+ resource.type === "integer" ||
113
+ resource.type === "number" ||
114
+ resource.type === "bool" ||
115
+ resource.type === "boolean" ||
116
+ resource.type === "fileContent" ||
117
+ resource.type === "array"
118
+ )
119
+ .map((resource) => {
120
+ if (resource.type === "string") {
121
+ return {
122
+ name: resource.name || "",
123
+ value: (resource as any).value as string,
124
+ type: "string",
125
+ };
126
+ }
127
+ if (resource.type === "fileContent") {
128
+ return {
129
+ name: resource.name || "",
130
+ value: (resource as any).value as string,
131
+ type: "string",
132
+ };
133
+ }
134
+ if (resource.type === "bool" || resource.type === "boolean") {
135
+ return {
136
+ name: resource.name || "",
137
+ value:
138
+ (resource as any).value === "true" ||
139
+ (resource as any).value === true,
140
+ type: "boolean",
141
+ };
142
+ }
143
+ if (resource.type === "number" || resource.type === "integer") {
144
+ return {
145
+ name: resource.name || "",
146
+ value: Number((resource as any).value),
147
+ type: "number",
148
+ };
149
+ }
150
+ if (resource.type === "array") {
151
+ return {
152
+ name: resource.name || "",
153
+ value: (resource as any).value || [],
154
+ type: "string",
155
+ };
156
+ }
157
+ return {
158
+ name: resource.name || "",
159
+ value: (resource as any).value as string,
160
+ type: "string",
161
+ };
162
+ });
163
+
164
+ const deploymentData: Service = item.deploymentData as unknown as Service;
165
+ const url = new URL(
166
+ `${environment.apiServer.baseUrl}/api/${environment.apiServer.apiVersion}/tenant/${item.deploymentData.tenant}/service/${deploymentData.name}`
167
+ );
168
+ url.searchParams.append("dsl", "true");
169
+ const parametersObj = deploymentConfigParameters.reduce((acc, param) => {
170
+ const paramMetadata = item.parameterList.find(
171
+ (p) => p.name === param.name
172
+ );
173
+ if (paramMetadata && (paramMetadata as any).parent === "accesspolicies") {
174
+ if (!acc.accesspolicies) {
175
+ acc.accesspolicies = {};
176
+ }
177
+ acc.accesspolicies[param.name] = param.value;
178
+ } else {
179
+ acc[param.name] = param.value;
180
+ }
181
+ return acc;
182
+ }, {} as { [key: string]: any });
183
+
184
+ const resourcesObj = item.resourceList.reduce((acc, resource: any) => {
185
+ if (resource.type === "secret") {
186
+ acc[resource.name] = {
187
+ secret: resource.value as string,
188
+ } as unknown as DeploymentResource;
189
+ } else if (resource.type === "volume") {
190
+ acc[resource.name] = {
191
+ volume: {
192
+ kind: "storage",
193
+ type: resource.kind,
194
+ size: Number(resource.value || 1),
195
+ unit: "G",
196
+ },
197
+ } as unknown as DeploymentResource;
198
+ } else if (resource.type === "domain") {
199
+ acc[resource.name] = {
200
+ domain: resource.value,
201
+ } as unknown as DeploymentResource;
202
+ } else if (resource.type === "certificate") {
203
+ acc[resource.name] = {
204
+ certificate: resource.value,
205
+ } as unknown as DeploymentResource;
206
+ } else if (resource.type === "ca") {
207
+ acc[resource.name] = {
208
+ ca: resource.value,
209
+ } as unknown as DeploymentResource;
210
+ }
211
+ else if (resource.type === "port") {
212
+ acc[resource.name] = {
213
+ port: resource.value,
214
+ } as unknown as DeploymentResource;
215
+ }
216
+
217
+ return acc;
218
+ }, {} as { [key: string]: DeploymentResource });
219
+
220
+ const isInbound = item.module === "builtins/inbound";
221
+
222
+ if (isInbound) {
223
+ const hasCertificate = item.resourceList.some(
224
+ (resource) =>
225
+ resource.type === "certificate" ||
226
+ resource.name.toLowerCase().includes("cert") ||
227
+ resource.name === "servercert"
228
+ );
229
+
230
+ const hasDomain = item.resourceList.some(
231
+ (resource) =>
232
+ resource.type === "domain" ||
233
+ resource.name.toLowerCase().includes("domain") ||
234
+ resource.name === "serverdomain"
235
+ );
236
+ const hasPort = item.resourceList.some(
237
+ (resource) =>
238
+ resource.type === "port" ||
239
+ resource.name.toLowerCase().includes("port")
240
+ );
241
+ let correctType = "tcp";
242
+ let requiredParameters: { [key: string]: any } = {};
243
+
244
+ if (hasCertificate && hasDomain) {
245
+ correctType = "https";
246
+ requiredParameters = {
247
+ type: "https",
248
+ clientcert: false,
249
+ websocket: false,
250
+ remoteaddressheader: parametersObj.remoteaddressheader || "",
251
+ cleanxforwardedfor: false,
252
+ };
253
+ } else if (hasPort && !hasDomain && !hasCertificate) {
254
+ correctType = "tcp";
255
+ requiredParameters = {
256
+ type: "tcp",
257
+ };
258
+ } else {
259
+ console.warn(
260
+ "Inbound configuration doesn't match any valid schema variant:",
261
+ {
262
+ hasCertificate,
263
+ hasDomain,
264
+ hasPort,
265
+ resources: item.resourceList.map((r) => ({
266
+ name: r.name,
267
+ type: r.type,
268
+ })),
269
+ }
270
+ );
271
+ }
272
+ Object.assign(parametersObj, requiredParameters);
273
+ }
274
+
275
+ const serviceName = item.serviceName.startsWith("./")
276
+ ? item.serviceName.split("./")[1]
277
+ : item.serviceName.startsWith(".")
278
+ ? item.serviceName.split(".")[1]
279
+ : item.serviceName;
280
+
281
+ const scale =
282
+ item.type === "component" || item.serviceName === ""
283
+ ? { hsize: 1 }
284
+ : item.schema?.name === "builtins/inbound"
285
+ ? { detail: {} }
286
+ : item.type === "service" && item.roles && Array.isArray(item.roles)
287
+ ? {
288
+ detail: item.roles.reduce(
289
+ (acc, role) => ({
290
+ ...acc,
291
+ [role]: { hsize: 1 },
292
+ }),
293
+ {}
294
+ ),
295
+ }
296
+ : { detail: { [`${item.artifactName}`]: { hsize: 1 } } };
297
+
298
+ const scaling: {
299
+ simple: {
300
+ [key: string]: {
301
+ scale_up: { cpu: number; memory: number };
302
+ scale_down: { cpu: number; memory: number };
303
+ hysteresis: number;
304
+ min_replicas: number;
305
+ max_replicas: number;
306
+ };
307
+ };
308
+ } = {
309
+ simple: {},
310
+ };
311
+
312
+ scaling.simple[item.serviceName] = {
313
+ scale_up: {
314
+ cpu: parseInt(item.scaling?.cpu.up || "") || 0,
315
+ memory: parseInt(item.scaling?.memory.up || "") || 0,
316
+ },
317
+ scale_down: {
318
+ cpu: parseInt(item.scaling?.cpu.down || "") || 0,
319
+ memory: parseInt(item.scaling?.memory.down || "") || 0,
320
+ },
321
+ hysteresis: parseInt(item.scaling?.histeresys || "") || 0,
322
+ min_replicas: item.scaling?.instances.min || 0,
323
+ max_replicas: item.scaling?.instances.max || 0,
324
+ };
325
+ const itemBody = {
326
+ type: "deployment",
327
+ deployment: {
328
+ name: deploymentData.name,
329
+ up: null,
330
+ meta: {},
331
+ config: {
332
+ parameter: parametersObj,
333
+ resource: resourcesObj,
334
+ resilience: 0,
335
+ scale: scale,
336
+ },
337
+ artifact: {
338
+ ref: {
339
+ version: [
340
+ Number(item.version.split(".")[0]),
341
+ Number(item.version.split(".")[1]),
342
+ Number(item.version.split(".")[2]),
343
+ ],
344
+ kind: item.type,
345
+ domain: item.domain,
346
+ module: item.module,
347
+ name: item.artifact,
348
+ package: serviceName,
349
+ },
350
+ },
351
+ },
352
+ comment: " ",
353
+ meta: {
354
+ targetAccount: deploymentData.account,
355
+ targetEnvironment: deploymentData.environment,
356
+ scaling: scaling,
357
+ },
358
+ };
359
+ const response = await fetch(url.toString(), {
360
+ method: "POST",
361
+ headers: {
362
+ "Content-Type": "application/json",
363
+ },
364
+ body: JSON.stringify(itemBody),
365
+ });
366
+
367
+ if (!response.ok) {
368
+ throw new Error(`HTTP error! status: ${response.status}`);
369
+ }
370
+
371
+ const jsonResponse = await response.json();
372
+
373
+ const isTimeout = jsonResponse?.events?.some(
374
+ (event: any) => event.content === "_timeout_"
375
+ );
376
+
377
+ if (isTimeout) {
378
+ console.error("Timeout en la petición:", {
379
+ isOk: false,
380
+ code: "TIMEOUT",
381
+ error: "_timeout_",
382
+ });
383
+ }
384
+
385
+ if (deploymentData.links && deploymentData.links.length > 0) {
386
+ pendingLinks.set(deploymentData.name, deploymentData.links);
387
+
388
+ try {
389
+ await linkPendingServices(deploymentData, "");
390
+ } catch (linkError) {
391
+ console.error("⭐ Error en linkPendingServices:", linkError);
392
+ }
393
+ }
394
+ }
395
+ };
396
+
397
+ export const getMarketplaceItems = async (
398
+ tenants: string[],
399
+ security: string
400
+ ) => {
401
+ await initializeGlobalWebSocketClient(security);
402
+ const promises = tenants.map(async (tenant) => {
403
+ try {
404
+ const response = await makeGlobalWebSocketRequest(
405
+ "marketplace:search_marketplace",
406
+ { tenant },
407
+ 30000,
408
+ "GET",
409
+ tenant
410
+ );
411
+
412
+ const tenantItems: MarketplaceItem[] = [];
413
+
414
+ const itemPromises = Object.entries(response.data.items).map(
415
+ async ([key, value]) => {
416
+ let cpu = 0;
417
+ let memory = 0;
418
+
419
+ (value as any).tags.forEach((tag: string) => {
420
+ if (tag.includes("cpu")) {
421
+ cpu = Number(tag.split("::")[1].split("vCPU")[0]);
422
+ } else if (tag.includes("memory")) {
423
+ const value = tag.split("::")[1]?.trim();
424
+ if (value?.includes("GB")) {
425
+ const memStr = value.split("GB")[0].trim();
426
+ memory = Number(memStr);
427
+ } else if (value?.includes("MB")) {
428
+ const memStr = value.split("MB")[0].trim();
429
+ memory = Number(memStr) / 1024;
430
+ }
431
+ }
432
+ });
433
+ const item: MarketplaceItem = {
434
+ tenant: tenant.toString(),
435
+ name: (value as any).name,
436
+ logo: "https://gitlab.com/uploads/-/system/group/avatar/86481133/Axebow-Logo-2.jpeg?width=96",
437
+ description: (value as any).name,
438
+ version: (value as any).version,
439
+ requirements: { cpu, memory },
440
+ status: "",
441
+ instances: [],
442
+ links: [],
443
+ resources: [],
444
+ domain: (value as any).domain,
445
+ type: (value as any).artifactTypes[0],
446
+ artifact: (value as any).artifact,
447
+ };
448
+
449
+ try {
450
+ const schemaData = await getMarketplaceSchema(
451
+ tenant,
452
+ item,
453
+ security
454
+ );
455
+ item.schema = schemaData;
456
+ } catch (schemaError) {
457
+ console.error(
458
+ `Error cargando schema para ${item.name}:`,
459
+ schemaError
460
+ );
461
+ item.schema = undefined;
462
+ }
463
+
464
+ return item;
465
+ }
466
+ );
467
+
468
+ const processedItems = await Promise.all(itemPromises);
469
+ tenantItems.push(...processedItems);
470
+ return tenantItems;
471
+ } catch (error) {
472
+ console.error(`Error al obtener items para el tenant ${tenant}:`, error);
473
+ return [];
474
+ }
475
+ });
476
+
477
+ const itemsArrays = await Promise.all(promises);
478
+ const items = itemsArrays.flat();
479
+ return { items };
480
+ };
481
+
482
+ export const getMarketplaceSchema = async (
483
+ tenant: string,
484
+ marketplaceItem: MarketplaceItem,
485
+ security: string
486
+ ): Promise<
487
+ | {
488
+ resources: {
489
+ name: string;
490
+ type: string;
491
+ required: boolean;
492
+ protocol?: string;
493
+ defaultValue?: string;
494
+ }[];
495
+ parameters: {
496
+ name: string;
497
+ type: string;
498
+ required: boolean;
499
+ protocol?: string;
500
+ pattern?: string;
501
+ parent?: string;
502
+ defaultValue?: string | number;
503
+ }[];
504
+ channels: {
505
+ name: string;
506
+ type: "client" | "server" | "duplex";
507
+ resource?: { type: "port" | "domain"; value: string };
508
+ protocol?: string;
509
+ }[];
510
+ name?: string;
511
+ roleName?: string;
512
+ roles?: string[];
513
+ hasVariants?: boolean;
514
+ variants?: any[];
515
+ }
516
+ | undefined
517
+ > => {
518
+ await initializeGlobalWebSocketClient(security);
519
+ try {
520
+ const requestPayload = {
521
+ tenant,
522
+ module: marketplaceItem.name,
523
+ version: marketplaceItem.version,
524
+ domain: marketplaceItem.domain,
525
+ artifact: marketplaceItem.artifact,
526
+ };
527
+
528
+ const response = await makeGlobalWebSocketRequest(
529
+ "marketplace:artifact_schema",
530
+ requestPayload,
531
+ 30000,
532
+ "GET",
533
+ tenant
534
+ );
535
+
536
+ const nameKeys = Object.keys(response.data);
537
+ const schemaData = response.data[nameKeys[0]];
538
+
539
+ let roleName = "";
540
+ let roles: string[] = [];
541
+ if (nameKeys[0] !== ".") {
542
+ roles = schemaData.roles || [];
543
+ roleName = roles.join(", ");
544
+ }
545
+
546
+ const schemas = schemaData.schemas || [];
547
+ if (schemas.length > 0 && schemas[0].oneOf) {
548
+ const schemaOptions = schemas[0].oneOf;
549
+ const processedSchemas: any[] = [];
550
+
551
+ for (const schemaRef of schemaOptions) {
552
+ const refKey = schemaRef.$ref?.replace("#/$defs/", "");
553
+ const actualSchema = schemas[0].$defs?.[refKey];
554
+
555
+ if (actualSchema) {
556
+ const schemaResult = processSchema(actualSchema, refKey);
557
+ if (schemaResult) {
558
+ processedSchemas.push({
559
+ name: nameKeys[0],
560
+ roleName,
561
+ roles,
562
+ variant: refKey,
563
+ ...schemaResult,
564
+ });
565
+ }
566
+ }
567
+ }
568
+ return {
569
+ parameters: processedSchemas[0]?.parameters || [],
570
+ resources: processedSchemas[0]?.resources || [],
571
+ channels: processedSchemas[0]?.channels || [],
572
+ name: nameKeys[0],
573
+ roleName,
574
+ roles,
575
+ hasVariants: true,
576
+ variants: processedSchemas,
577
+ } as any;
578
+ }
579
+
580
+ const singleSchema = schemas[0];
581
+ if (singleSchema) {
582
+ const schemaResult = processSchema(singleSchema, "default");
583
+ return {
584
+ parameters: schemaResult?.parameters || [],
585
+ resources: schemaResult?.resources || [],
586
+ channels: schemaResult?.channels || [],
587
+ name: nameKeys[0],
588
+ roleName,
589
+ roles,
590
+ hasVariants: false,
591
+ } as any;
592
+ }
593
+
594
+ return {
595
+ parameters: [],
596
+ resources: [],
597
+ channels: [],
598
+ name: nameKeys[0],
599
+ roleName,
600
+ roles,
601
+ hasVariants: false,
602
+ } as any;
603
+ } catch (error) {
604
+ console.error("Error obteniendo el schema del item:", error);
605
+ throw error;
606
+ }
607
+ };
608
+
609
+ function processSchema(schema: any, schemaType: string) {
610
+ let resources: {
611
+ name: string;
612
+ type: string;
613
+ required: boolean;
614
+ defaultValue?: string;
615
+ }[] = [];
616
+ let parameters: {
617
+ name: string;
618
+ type: string;
619
+ required: boolean;
620
+ defaultValue?: string | number;
621
+ pattern?: string;
622
+ parent?: string;
623
+ }[] = [];
624
+ let channels: {
625
+ name: string;
626
+ type: "client" | "server" | "duplex";
627
+ resource?: {
628
+ type: "port" | "domain";
629
+ value: string;
630
+ };
631
+ }[] = [];
632
+
633
+ const configProps = schema.properties?.config?.properties;
634
+
635
+ if (configProps) {
636
+ Object.entries(configProps).forEach(([key, value]) => {
637
+ const param = value as any;
638
+ parameters.push({
639
+ name: key,
640
+ type: param.type || "string",
641
+ required: schema.properties?.config?.required?.includes(key) || false,
642
+ defaultValue:
643
+ param.default !== undefined
644
+ ? param.default
645
+ : param.enum
646
+ ? param.enum[0]
647
+ : param.const
648
+ ? param.const
649
+ : undefined,
650
+ });
651
+ });
652
+ }
653
+
654
+ const resourceProps = schema.properties?.resource;
655
+ if (resourceProps?.properties) {
656
+ Object.entries(resourceProps.properties).forEach(([key, value]) => {
657
+ if (
658
+ key.toLowerCase() === "data" ||
659
+ key.toLowerCase() === "jetstreamdata" ||
660
+ key.toLowerCase() === "shared"
661
+ ) {
662
+ resources.push({
663
+ name: key,
664
+ type: "volume",
665
+ required: resourceProps.required?.includes(key) || false,
666
+ defaultValue: undefined,
667
+ });
668
+ } else {
669
+ const resourceValue = value as any;
670
+ let resourceType = "";
671
+ let defaultValue: string | undefined = undefined;
672
+
673
+ if (resourceValue.properties?.["$kdsl"]?.const?.NamedType?.Name) {
674
+ const typeName =
675
+ resourceValue.properties["$kdsl"].const.NamedType.Name;
676
+ resourceType = typeName.toLowerCase();
677
+ } else {
678
+ resourceType = Object.keys(resourceValue.properties || {})[0];
679
+ }
680
+
681
+ if (resourceValue.properties) {
682
+ const innerResource =
683
+ resourceValue.properties[resourceType] ||
684
+ resourceValue.properties.inner;
685
+ if (innerResource) {
686
+ if (innerResource.default !== undefined) {
687
+ defaultValue = String(innerResource.default);
688
+ } else if (innerResource.enum && innerResource.enum.length > 0) {
689
+ defaultValue = String(innerResource.enum[0]);
690
+ } else if (resourceType === "volume" && innerResource.oneOf) {
691
+ const firstOption = innerResource.oneOf[0];
692
+ if (firstOption?.properties?.volume?.properties) {
693
+ const sizeDefault =
694
+ firstOption.properties.volume.properties.size?.default;
695
+ const unitDefault =
696
+ firstOption.properties.volume.properties.unit?.enum?.[0];
697
+ if (sizeDefault !== undefined && unitDefault) {
698
+ defaultValue = `${sizeDefault}${unitDefault}`;
699
+ }
700
+ }
701
+ }
702
+ }
703
+ }
704
+
705
+ resources.push({
706
+ name: key,
707
+ type: resourceType || "string",
708
+ required: resourceProps.required?.includes(key) || false,
709
+ defaultValue: defaultValue,
710
+ });
711
+ }
712
+ });
713
+ }
714
+
715
+ const srvProps = schema.properties?.srv?.properties;
716
+ if (srvProps) {
717
+ if (srvProps.client?.properties) {
718
+ Object.entries(srvProps.client.properties).forEach(
719
+ ([channelName, channelData]) => {
720
+ const channelInfo = channelData as any;
721
+ let resource: { type: "port" | "domain"; value: string } | undefined;
722
+
723
+ if (channelInfo.properties) {
724
+ const port =
725
+ channelInfo.properties.port?.const ||
726
+ channelInfo.properties.port?.enum?.[0];
727
+ const protocol =
728
+ channelInfo.properties.protocol?.const ||
729
+ channelInfo.properties.protocol?.enum?.[0];
730
+
731
+ if (port && protocol) {
732
+ resource = {
733
+ type: "port",
734
+ value: `${protocol}:${port}`,
735
+ };
736
+ }
737
+ }
738
+
739
+ channels.push({
740
+ name: channelName,
741
+ type: "client",
742
+ resource,
743
+ });
744
+ }
745
+ );
746
+ }
747
+ if (srvProps.server?.properties) {
748
+ Object.entries(srvProps.server.properties).forEach(
749
+ ([channelName, channelData]) => {
750
+ const channelInfo = channelData as any;
751
+ let resource: { type: "port" | "domain"; value: string } | undefined;
752
+
753
+ if (channelInfo.properties) {
754
+ const port =
755
+ channelInfo.properties.port?.const ||
756
+ channelInfo.properties.port?.enum?.[0];
757
+ const protocol =
758
+ channelInfo.properties.protocol?.const ||
759
+ channelInfo.properties.protocol?.enum?.[0];
760
+
761
+ if (port && protocol) {
762
+ resource = {
763
+ type: "port",
764
+ value: `${protocol}:${port}`,
765
+ };
766
+ }
767
+ }
768
+
769
+ channels.push({
770
+ name: channelName,
771
+ type: "server",
772
+ resource,
773
+ });
774
+ }
775
+ );
776
+ }
777
+ if (srvProps.duplex?.properties) {
778
+ Object.entries(srvProps.duplex.properties).forEach(
779
+ ([channelName, channelData]) => {
780
+ const channelInfo = channelData as any;
781
+ let resource: { type: "port" | "domain"; value: string } | undefined;
782
+
783
+ if (channelInfo.properties) {
784
+ const port =
785
+ channelInfo.properties.port?.const ||
786
+ channelInfo.properties.port?.enum?.[0];
787
+ const protocol =
788
+ channelInfo.properties.protocol?.const ||
789
+ channelInfo.properties.protocol?.enum?.[0];
790
+
791
+ if (port && protocol) {
792
+ resource = {
793
+ type: "port",
794
+ value: `${protocol}:${port}`,
795
+ };
796
+ }
797
+ }
798
+
799
+ channels.push({
800
+ name: channelName,
801
+ type: "duplex",
802
+ resource,
803
+ });
804
+ }
805
+ );
806
+ }
807
+ }
808
+
809
+ return {
810
+ parameters,
811
+ resources,
812
+ channels,
813
+ };
814
+ }
815
+ const generateLinkBody = (data: Service, link: Link) => {
816
+ let linkBody;
817
+
818
+ if (link.origin === data.name) {
819
+ const originInClient = data.clientChannels.find(
820
+ (channel) =>
821
+ channel.name === link.originChannel ||
822
+ channel.from === link.originChannel
823
+ );
824
+ const originInServer = data.serverChannels.find(
825
+ (channel) =>
826
+ channel.name === link.originChannel ||
827
+ channel.from === link.originChannel
828
+ );
829
+ const originInDuplex = data.duplexChannels.find(
830
+ (channel) =>
831
+ channel.name === link.originChannel ||
832
+ channel.from === link.originChannel
833
+ );
834
+
835
+ if (originInClient) {
836
+ linkBody = {
837
+ client_tenant: data.tenant,
838
+ client_service: data.name,
839
+ client_channel: link.originChannel,
840
+ server_tenant: data.tenant,
841
+ server_service: link.target,
842
+ server_channel: link.targetChannel,
843
+ };
844
+ } else if (originInServer || originInDuplex) {
845
+ linkBody = {
846
+ client_tenant: data.tenant,
847
+ client_service: link.target,
848
+ client_channel: link.targetChannel,
849
+ server_tenant: data.tenant,
850
+ server_service: data.name,
851
+ server_channel: link.originChannel,
852
+ };
853
+ }
854
+ } else if (link.target === data.name) {
855
+ const targetInClient = data.clientChannels.find(
856
+ (channel) =>
857
+ channel.name === link.targetChannel ||
858
+ channel.from === link.targetChannel
859
+ );
860
+ const targetInServer = data.serverChannels.find(
861
+ (channel) =>
862
+ channel.name === link.targetChannel ||
863
+ channel.from === link.targetChannel
864
+ );
865
+ const targetInDuplex = data.duplexChannels.find(
866
+ (channel) =>
867
+ channel.name === link.targetChannel ||
868
+ channel.from === link.targetChannel
869
+ );
870
+
871
+ if (targetInClient) {
872
+ linkBody = {
873
+ client_tenant: data.tenant,
874
+ client_service: data.name,
875
+ client_channel: link.targetChannel,
876
+ server_tenant: data.tenant,
877
+ server_service: link.origin,
878
+ server_channel: link.originChannel,
879
+ };
880
+ } else if (targetInServer || targetInDuplex) {
881
+ linkBody = {
882
+ client_tenant: data.tenant,
883
+ client_service: link.origin,
884
+ client_channel: link.originChannel,
885
+ server_tenant: data.tenant,
886
+ server_service: data.name,
887
+ server_channel: link.targetChannel,
888
+ };
889
+ }
890
+ } else {
891
+ console.warn(
892
+ `Servicio actual no involucrado en el enlace: current=${data.name}, origin=${link.origin}, target=${link.target}`
893
+ );
894
+ linkBody = {
895
+ client_tenant: data.tenant,
896
+ client_service: link.origin,
897
+ client_channel: link.originChannel,
898
+ server_tenant: data.tenant,
899
+ server_service: link.target,
900
+ server_channel: link.targetChannel,
901
+ };
902
+ }
903
+ return linkBody;
904
+ };
905
+ const linkPendingServices = async (service: Service, token: string) => {
906
+ const links = pendingLinks.get(service.name);
907
+
908
+ if (links) {
909
+ await Promise.all(
910
+ service.links.map(async (link, index) => {
911
+ try {
912
+ await initializeGlobalWebSocketClient(token);
913
+ const status = getWebSocketStatus();
914
+ const linkBody = generateLinkBody(service, link);
915
+
916
+ const linkResponse = await makeGlobalWebSocketRequest(
917
+ "link:link_service",
918
+ linkBody,
919
+ 30000,
920
+ "LINK",
921
+ service.name
922
+ );
923
+
924
+ const notification: Notification = {
925
+ type: "success",
926
+ subtype: "service-linked",
927
+ date: Date.now().toString(),
928
+ status: "unread",
929
+ callToAction: false,
930
+ data: {
931
+ service: service.name,
932
+ tenant: service.tenant,
933
+ },
934
+ };
935
+
936
+ eventHelper.notification.publish.creation(notification);
937
+ } catch (linkErr) {
938
+ const notification: Notification = {
939
+ type: "error",
940
+ subtype: "error-service-link",
941
+ date: Date.now().toString(),
942
+ info_content: {
943
+ code: (linkErr as any).error.code,
944
+ message: (linkErr as any).error.content,
945
+ },
946
+ status: "unread",
947
+ callToAction: false,
948
+ data: {
949
+ service: service.name,
950
+ tenant: service.tenant,
951
+ },
952
+ };
953
+ eventHelper.notification.publish.creation(notification);
954
+ }
955
+ })
956
+ );
957
+ }
958
+ };
959
+ /**
960
+ * Function to load marketplace items for a specific tenant
961
+ * @param tenant Tenant ID
962
+ * @param security Authorization token
963
+ */
964
+ export const loadMarketplaceItemsForTenant = async (
965
+ tenant: string,
966
+ security: string
967
+ ) => {
968
+ try {
969
+ await initializeGlobalWebSocketClient(security);
970
+
971
+ const response = await makeGlobalWebSocketRequest(
972
+ "marketplace:search_marketplace",
973
+ { tenant },
974
+ 30000,
975
+ "GET",
976
+ tenant
977
+ );
978
+ const items = response.data?.items || [];
979
+ if (!Array.isArray(items) || items.length === 0) {
980
+ return [];
981
+ }
982
+
983
+ const itemPromises = items.map(async (item: any) => {
984
+ const marketplaceItem: MarketplaceItem = {
985
+ tenant: tenant,
986
+ name: item.module,
987
+ logo: "https://gitlab.com/uploads/-/system/group/avatar/86481133/Axebow-Logo-2.jpeg?width=96",
988
+ description: item.description,
989
+ version: item.version,
990
+ requirements: {
991
+ cpu: item.requirements?.cpu || 0,
992
+ memory: item.requirements?.memory || 0,
993
+ },
994
+ status: "",
995
+ instances: [],
996
+ links: [],
997
+ resources: [],
998
+ domain: item.domain,
999
+ type: item.artifactTypes?.[0],
1000
+ artifact: item.artifact,
1001
+ package: item.package,
1002
+ categories: item.categories || [],
1003
+ };
1004
+
1005
+ try {
1006
+ const schemaData = await getMarketplaceSchema(
1007
+ tenant,
1008
+ marketplaceItem,
1009
+ security
1010
+ );
1011
+ marketplaceItem.schema = schemaData;
1012
+ } catch (schemaError) {
1013
+ console.error(
1014
+ `Error loading schema for ${marketplaceItem.name}:`,
1015
+ schemaError
1016
+ );
1017
+ marketplaceItem.schema = undefined;
1018
+ }
1019
+
1020
+ return marketplaceItem;
1021
+ });
1022
+
1023
+ return await Promise.all(itemPromises);
1024
+ } catch (error) {
1025
+ console.error(
1026
+ `Error loading marketplace items for tenant ${tenant}:`,
1027
+ error
1028
+ );
1029
+ return [];
1030
+ }
1031
+ };