@red-hat-developer-hub/plugin-cost-management-backend 2.0.2

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,130 @@
1
+ 'use strict';
2
+
3
+ var checkPermissions = require('../util/checkPermissions.cjs.js');
4
+ var permissions = require('@red-hat-developer-hub/plugin-cost-management-common/permissions');
5
+ var pluginPermissionCommon = require('@backstage/plugin-permission-common');
6
+ var tokenUtil = require('../util/tokenUtil.cjs.js');
7
+ var auditLog = require('../util/auditLog.cjs.js');
8
+
9
+ const COST_CLUSTERS_CACHE_KEY = "cost_clusters";
10
+ const COST_PROJECTS_CACHE_KEY = "cost_projects";
11
+ const CACHE_TTL = 15 * 60 * 1e3;
12
+ const getCostManagementAccess = (options) => async (_, response) => {
13
+ const { logger, permissions: permissions$1, httpAuth, cache, costManagementApi } = options;
14
+ let finalDecision = pluginPermissionCommon.AuthorizeResult.DENY;
15
+ const costPluginDecision = await checkPermissions.authorize(
16
+ _,
17
+ permissions.costPluginPermissions,
18
+ permissions$1,
19
+ httpAuth
20
+ );
21
+ logger.info(`Checking cost.plugin permission:`, costPluginDecision);
22
+ if (costPluginDecision.result === pluginPermissionCommon.AuthorizeResult.ALLOW) {
23
+ finalDecision = pluginPermissionCommon.AuthorizeResult.ALLOW;
24
+ const actor2 = await auditLog.resolveActor(_, options);
25
+ auditLog.emitAuditLog(options, {
26
+ actor: actor2,
27
+ action: "access_check",
28
+ resource: "/access/cost-management",
29
+ decision: "ALLOW"
30
+ });
31
+ const body2 = {
32
+ decision: finalDecision,
33
+ authorizedClusterNames: [],
34
+ authorizeProjects: []
35
+ };
36
+ return response.json(body2);
37
+ }
38
+ let clusterDataMap = {};
39
+ let allProjects = [];
40
+ const clustersFromCache = await cache.get(COST_CLUSTERS_CACHE_KEY);
41
+ const projectsFromCache = await cache.get(COST_PROJECTS_CACHE_KEY);
42
+ if (clustersFromCache && projectsFromCache) {
43
+ clusterDataMap = clustersFromCache;
44
+ allProjects = projectsFromCache;
45
+ logger.info(
46
+ `Using cached data: ${Object.keys(clusterDataMap).length} clusters, ${allProjects.length} projects`
47
+ );
48
+ } else {
49
+ try {
50
+ const token = await tokenUtil.getTokenFromApi(options);
51
+ const [clustersResponse, projectsResponse] = await Promise.all([
52
+ costManagementApi.searchOpenShiftClusters("", { token, limit: 1e3 }),
53
+ costManagementApi.searchOpenShiftProjects("", { token, limit: 1e3 })
54
+ ]);
55
+ const clustersData = await clustersResponse.json();
56
+ const projectsData = await projectsResponse.json();
57
+ clustersData.data?.forEach(
58
+ (cluster) => {
59
+ if (cluster.cluster_alias && cluster.value) {
60
+ clusterDataMap[cluster.cluster_alias] = cluster.value;
61
+ }
62
+ }
63
+ );
64
+ allProjects = [
65
+ ...new Set(
66
+ projectsData.data?.map((project) => project.value)
67
+ )
68
+ ].filter((project) => project !== void 0);
69
+ logger.info(
70
+ `Fetched ${Object.keys(clusterDataMap).length} clusters and ${allProjects.length} projects from Cost Management API`
71
+ );
72
+ await Promise.all([
73
+ cache.set(COST_CLUSTERS_CACHE_KEY, clusterDataMap, {
74
+ ttl: CACHE_TTL
75
+ }),
76
+ cache.set(COST_PROJECTS_CACHE_KEY, allProjects, {
77
+ ttl: CACHE_TTL
78
+ })
79
+ ]);
80
+ } catch (error) {
81
+ logger.error("Error fetching cost management data", error);
82
+ return response.status(500).json({
83
+ decision: pluginPermissionCommon.AuthorizeResult.DENY,
84
+ error: "Failed to fetch cluster data",
85
+ authorizedClusterNames: [],
86
+ authorizeProjects: []
87
+ });
88
+ }
89
+ }
90
+ const { authorizedClusterIds, authorizedClusterProjects } = await checkPermissions.filterAuthorizedClustersAndProjects(
91
+ _,
92
+ permissions$1,
93
+ httpAuth,
94
+ clusterDataMap,
95
+ allProjects,
96
+ "cost"
97
+ );
98
+ const finalAuthorizedClusterNames = [
99
+ .../* @__PURE__ */ new Set([
100
+ ...authorizedClusterIds,
101
+ ...authorizedClusterProjects.map((result) => result.cluster)
102
+ ])
103
+ ];
104
+ const authorizeProjects = authorizedClusterProjects.map(
105
+ (result) => result.project
106
+ );
107
+ if (finalAuthorizedClusterNames.length > 0) {
108
+ finalDecision = pluginPermissionCommon.AuthorizeResult.ALLOW;
109
+ }
110
+ const actor = await auditLog.resolveActor(_, options);
111
+ auditLog.emitAuditLog(options, {
112
+ actor,
113
+ action: "access_check",
114
+ resource: "/access/cost-management",
115
+ decision: finalDecision === pluginPermissionCommon.AuthorizeResult.ALLOW ? "ALLOW" : "DENY",
116
+ filters: {
117
+ clusters: finalAuthorizedClusterNames,
118
+ projects: authorizeProjects
119
+ }
120
+ });
121
+ const body = {
122
+ decision: finalDecision,
123
+ authorizedClusterNames: finalAuthorizedClusterNames,
124
+ authorizeProjects
125
+ };
126
+ return response.json(body);
127
+ };
128
+
129
+ exports.getCostManagementAccess = getCostManagementAccess;
130
+ //# sourceMappingURL=costManagementAccess.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"costManagementAccess.cjs.js","sources":["../../src/routes/costManagementAccess.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { RequestHandler } from 'express';\nimport type { RouterOptions } from '../models/RouterOptions';\nimport {\n authorize,\n filterAuthorizedClustersAndProjects,\n} from '../util/checkPermissions';\nimport { costPluginPermissions } from '@red-hat-developer-hub/plugin-cost-management-common/permissions';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport { getTokenFromApi } from '../util/tokenUtil';\nimport { resolveActor, emitAuditLog } from '../util/auditLog';\n\n// Cache keys for cost management clusters and projects\nconst COST_CLUSTERS_CACHE_KEY = 'cost_clusters';\nconst COST_PROJECTS_CACHE_KEY = 'cost_projects';\nconst CACHE_TTL = 15 * 60 * 1000; // 15 minutes\n\nexport const getCostManagementAccess: (\n options: RouterOptions,\n) => RequestHandler = options => async (_, response) => {\n const { logger, permissions, httpAuth, cache, costManagementApi } = options;\n let finalDecision = AuthorizeResult.DENY;\n\n // Check for cost.plugin permission\n // If user has cost.plugin permission, allow access to all data\n const costPluginDecision = await authorize(\n _,\n costPluginPermissions,\n permissions,\n httpAuth,\n );\n\n logger.info(`Checking cost.plugin permission:`, costPluginDecision);\n\n if (costPluginDecision.result === AuthorizeResult.ALLOW) {\n finalDecision = AuthorizeResult.ALLOW;\n\n const actor = await resolveActor(_, options);\n emitAuditLog(options, {\n actor,\n action: 'access_check',\n resource: '/access/cost-management',\n decision: 'ALLOW',\n });\n\n const body = {\n decision: finalDecision,\n authorizedClusterNames: [],\n authorizeProjects: [],\n };\n return response.json(body);\n }\n\n // RBAC Filtering logic for Cluster & Project using cost/{clusterName} and cost/{clusterName}/{projectName} permissions\n let clusterDataMap: Record<string, string> = {};\n let allProjects: string[] = [];\n\n // Check the cluster & project data in the cache first\n const clustersFromCache = (await cache.get(COST_CLUSTERS_CACHE_KEY)) as\n | Record<string, string>\n | undefined;\n const projectsFromCache = (await cache.get(COST_PROJECTS_CACHE_KEY)) as\n | string[]\n | undefined;\n\n if (clustersFromCache && projectsFromCache) {\n clusterDataMap = clustersFromCache;\n allProjects = projectsFromCache;\n logger.info(\n `Using cached data: ${Object.keys(clusterDataMap).length} clusters, ${\n allProjects.length\n } projects`,\n );\n } else {\n // Fetch clusters and projects from Cost Management API\n try {\n const token = await getTokenFromApi(options);\n\n // Fetch clusters and projects in parallel for better performance\n const [clustersResponse, projectsResponse] = await Promise.all([\n costManagementApi.searchOpenShiftClusters('', { token, limit: 1000 }),\n costManagementApi.searchOpenShiftProjects('', { token, limit: 1000 }),\n ]);\n\n const clustersData = await clustersResponse.json();\n const projectsData = await projectsResponse.json();\n\n // Extract cluster names from response\n clustersData.data?.forEach(\n (cluster: { value: string; cluster_alias: string }) => {\n if (cluster.cluster_alias && cluster.value) {\n clusterDataMap[cluster.cluster_alias] = cluster.value;\n }\n },\n );\n\n // Extract unique project names\n allProjects = [\n ...new Set(\n projectsData.data?.map((project: { value: string }) => project.value),\n ),\n ].filter(project => project !== undefined) as string[];\n\n logger.info(\n `Fetched ${Object.keys(clusterDataMap).length} clusters and ${\n allProjects.length\n } projects from Cost Management API`,\n );\n\n // Store in cache\n await Promise.all([\n cache.set(COST_CLUSTERS_CACHE_KEY, clusterDataMap, {\n ttl: CACHE_TTL,\n }),\n cache.set(COST_PROJECTS_CACHE_KEY, allProjects, {\n ttl: CACHE_TTL,\n }),\n ]);\n } catch (error) {\n logger.error('Error fetching cost management data', error);\n\n // Return unauthorized response on any error\n return response.status(500).json({\n decision: AuthorizeResult.DENY,\n error: 'Failed to fetch cluster data',\n authorizedClusterNames: [],\n authorizeProjects: [],\n });\n }\n }\n\n // RBAC Filtering: Single batch call for both cluster and cluster-project permissions\n\n const { authorizedClusterIds, authorizedClusterProjects } =\n await filterAuthorizedClustersAndProjects(\n _,\n permissions,\n httpAuth,\n clusterDataMap,\n allProjects,\n 'cost',\n );\n\n // Combine cluster names from both cluster-level and project-level permissions\n const finalAuthorizedClusterNames = [\n ...new Set([\n ...authorizedClusterIds,\n ...authorizedClusterProjects.map(result => result.cluster),\n ]),\n ];\n\n const authorizeProjects = authorizedClusterProjects.map(\n result => result.project,\n );\n\n // If user has access to at least one cluster, allow access\n if (finalAuthorizedClusterNames.length > 0) {\n finalDecision = AuthorizeResult.ALLOW;\n }\n\n const actor = await resolveActor(_, options);\n emitAuditLog(options, {\n actor,\n action: 'access_check',\n resource: '/access/cost-management',\n decision: finalDecision === AuthorizeResult.ALLOW ? 'ALLOW' : 'DENY',\n filters: {\n clusters: finalAuthorizedClusterNames,\n projects: authorizeProjects,\n },\n });\n\n const body = {\n decision: finalDecision,\n authorizedClusterNames: finalAuthorizedClusterNames,\n authorizeProjects,\n };\n\n return response.json(body);\n};\n"],"names":["permissions","AuthorizeResult","authorize","costPluginPermissions","actor","resolveActor","emitAuditLog","body","getTokenFromApi","filterAuthorizedClustersAndProjects"],"mappings":"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"}
@@ -0,0 +1,265 @@
1
+ 'use strict';
2
+
3
+ var checkPermissions = require('../util/checkPermissions.cjs.js');
4
+ var permissions = require('@red-hat-developer-hub/plugin-cost-management-common/permissions');
5
+ var pluginPermissionCommon = require('@backstage/plugin-permission-common');
6
+ var tokenUtil = require('../util/tokenUtil.cjs.js');
7
+ var constant = require('../util/constant.cjs.js');
8
+ var auditLog = require('../util/auditLog.cjs.js');
9
+
10
+ const CACHE_TTL = 15 * 60 * 1e3;
11
+ async function resolveAccess(req, proxyPath, options) {
12
+ const isOptimizations = proxyPath.startsWith("recommendations/");
13
+ if (isOptimizations) {
14
+ return resolveOptimizationsAccess(req, options);
15
+ }
16
+ return resolveCostManagementAccess(req, options);
17
+ }
18
+ async function resolveAccessForSection(req, options, pluginPerms, filterStyle, cacheKeys, fetchData) {
19
+ const { permissions, httpAuth, cache } = options;
20
+ const deny = () => ({
21
+ decision: "DENY",
22
+ clusterFilters: [],
23
+ projectFilters: [],
24
+ filterStyle
25
+ });
26
+ const pluginDecision = await checkPermissions.authorize(
27
+ req,
28
+ pluginPerms,
29
+ permissions,
30
+ httpAuth
31
+ );
32
+ if (pluginDecision.result === pluginPermissionCommon.AuthorizeResult.ALLOW) {
33
+ return {
34
+ decision: "ALLOW",
35
+ clusterFilters: [],
36
+ projectFilters: [],
37
+ filterStyle
38
+ };
39
+ }
40
+ let clusterDataMap = {};
41
+ let allProjects = [];
42
+ const cachedClusters = await cache.get(cacheKeys.clusterKey);
43
+ const cachedProjects = await cache.get(cacheKeys.projectKey);
44
+ if (cachedClusters && cachedProjects) {
45
+ clusterDataMap = cachedClusters;
46
+ allProjects = cachedProjects;
47
+ } else {
48
+ const result = await fetchData(options);
49
+ if (!result) return deny();
50
+ clusterDataMap = result.clusters;
51
+ allProjects = result.projects;
52
+ await Promise.all([
53
+ cache.set(cacheKeys.clusterKey, clusterDataMap, { ttl: CACHE_TTL }),
54
+ cache.set(cacheKeys.projectKey, allProjects, { ttl: CACHE_TTL })
55
+ ]);
56
+ }
57
+ const { authorizedClusterIds, authorizedClusterProjects } = await checkPermissions.filterAuthorizedClustersAndProjects(
58
+ req,
59
+ permissions,
60
+ httpAuth,
61
+ clusterDataMap,
62
+ allProjects,
63
+ ...filterStyle === "cost" ? ["cost"] : []
64
+ );
65
+ const finalClusters = [
66
+ .../* @__PURE__ */ new Set([
67
+ ...authorizedClusterIds,
68
+ ...authorizedClusterProjects.map((r) => r.cluster)
69
+ ])
70
+ ];
71
+ const finalProjects = authorizedClusterProjects.map((r) => r.project);
72
+ if (finalClusters.length === 0) return deny();
73
+ return {
74
+ decision: "ALLOW",
75
+ clusterFilters: finalClusters,
76
+ projectFilters: finalProjects,
77
+ filterStyle
78
+ };
79
+ }
80
+ async function resolveOptimizationsAccess(req, options) {
81
+ return resolveAccessForSection(
82
+ req,
83
+ options,
84
+ permissions.rosPluginPermissions,
85
+ "ros",
86
+ { clusterKey: "all_clusters_map", projectKey: "all_projects" },
87
+ async (opts) => {
88
+ const token = await tokenUtil.getTokenFromApi(opts);
89
+ const response = await opts.optimizationApi.getRecommendationList(
90
+ { query: { limit: -1, orderHow: "desc", orderBy: "last_reported" } },
91
+ { token }
92
+ );
93
+ const list = await response.json();
94
+ if (list.errors || !list.data) return null;
95
+ const clusters = {};
96
+ list.data.forEach((r) => {
97
+ if (r.clusterAlias && r.clusterUuid) {
98
+ clusters[r.clusterAlias] = r.clusterUuid;
99
+ }
100
+ });
101
+ const projects = [...new Set(list.data.map((r) => r.project))].filter(
102
+ (p) => p !== void 0
103
+ );
104
+ return { clusters, projects };
105
+ }
106
+ );
107
+ }
108
+ async function resolveCostManagementAccess(req, options) {
109
+ return resolveAccessForSection(
110
+ req,
111
+ options,
112
+ permissions.costPluginPermissions,
113
+ "cost",
114
+ { clusterKey: "cost_clusters", projectKey: "cost_projects" },
115
+ async (opts) => {
116
+ let token;
117
+ try {
118
+ token = await tokenUtil.getTokenFromApi(opts);
119
+ } catch {
120
+ return null;
121
+ }
122
+ try {
123
+ const [clustersResp, projectsResp] = await Promise.all([
124
+ opts.costManagementApi.searchOpenShiftClusters("", {
125
+ token,
126
+ limit: 1e3
127
+ }),
128
+ opts.costManagementApi.searchOpenShiftProjects("", {
129
+ token,
130
+ limit: 1e3
131
+ })
132
+ ]);
133
+ const clustersData = await clustersResp.json();
134
+ const projectsData = await projectsResp.json();
135
+ if (clustersData.errors || projectsData.errors || !clustersData.data || !projectsData.data) {
136
+ return null;
137
+ }
138
+ const clusters = {};
139
+ clustersData.data.forEach(
140
+ (c) => {
141
+ if (c.cluster_alias && c.value) clusters[c.cluster_alias] = c.value;
142
+ }
143
+ );
144
+ const projects = [
145
+ ...new Set(projectsData.data.map((p) => p.value))
146
+ ].filter((p) => p !== void 0);
147
+ return { clusters, projects };
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+ );
153
+ }
154
+ function isPathTraversal(proxyPath) {
155
+ return /(?:^|\/)\.\.(\/|$)/.test(proxyPath) || proxyPath.startsWith("/");
156
+ }
157
+ function parseClientQueryParams(originalUrl, rbacControlledKeys) {
158
+ const rawQuery = originalUrl.split("?")[1] || "";
159
+ const rawParams = rawQuery.split("&").filter((p) => p.length > 0);
160
+ const result = [];
161
+ for (const param of rawParams) {
162
+ const eqIdx = param.indexOf("=");
163
+ const rawKey = eqIdx >= 0 ? param.substring(0, eqIdx) : param;
164
+ const rawVal = eqIdx >= 0 ? param.substring(eqIdx + 1) : "";
165
+ try {
166
+ const decodedKey = decodeURIComponent(rawKey);
167
+ const decodedVal = decodeURIComponent(rawVal);
168
+ if (!rbacControlledKeys.has(decodedKey)) {
169
+ result.push({ key: decodedKey, value: decodedVal });
170
+ }
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+ return result;
176
+ }
177
+ function injectRbacFilters(targetUrl, access) {
178
+ const clusterKey = access.filterStyle === "ros" ? "cluster" : "filter[exact:cluster]";
179
+ const projectKey = access.filterStyle === "ros" ? "project" : "filter[exact:project]";
180
+ access.clusterFilters.forEach(
181
+ (c) => targetUrl.searchParams.append(clusterKey, c)
182
+ );
183
+ access.projectFilters.forEach(
184
+ (p) => targetUrl.searchParams.append(projectKey, p)
185
+ );
186
+ }
187
+ const secureProxy = (options) => async (req, res) => {
188
+ const { config } = options;
189
+ const proxyPath = req.params[0];
190
+ if (!proxyPath) {
191
+ return res.status(400).json({ error: "Missing proxy path" });
192
+ }
193
+ if (isPathTraversal(proxyPath)) {
194
+ return res.status(400).json({ error: "Invalid proxy path: traversal not allowed" });
195
+ }
196
+ try {
197
+ const access = await resolveAccess(req, proxyPath, options);
198
+ const actor = await auditLog.resolveActor(req, options);
199
+ if (access.decision !== "ALLOW") {
200
+ auditLog.emitAuditLog(options, {
201
+ actor,
202
+ action: "data_access",
203
+ resource: proxyPath,
204
+ decision: "DENY",
205
+ filters: {
206
+ clusters: access.clusterFilters,
207
+ projects: access.projectFilters
208
+ }
209
+ });
210
+ return res.status(403).json({ error: "Access denied by RBAC policy" });
211
+ }
212
+ const token = await tokenUtil.getTokenFromApi(options);
213
+ const targetBase = config?.getOptionalString("costManagementProxyBaseUrl") ?? constant.DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL;
214
+ const basePath = `${targetBase}/cost-management/v1/`;
215
+ const targetUrl = new URL(proxyPath, basePath);
216
+ if (!targetUrl.pathname.startsWith(new URL(basePath).pathname)) {
217
+ return res.status(400).json({ error: "Invalid proxy path: traversal not allowed" });
218
+ }
219
+ const rbacControlledKeys = new Set(
220
+ access.filterStyle === "ros" ? ["cluster", "project"] : ["filter[exact:cluster]", "filter[exact:project]"]
221
+ );
222
+ const clientParams = parseClientQueryParams(
223
+ req.originalUrl,
224
+ rbacControlledKeys
225
+ );
226
+ if (!clientParams) {
227
+ return res.status(400).json({ error: "Malformed percent-encoding in query string" });
228
+ }
229
+ for (const { key, value } of clientParams) {
230
+ targetUrl.searchParams.append(key, value);
231
+ }
232
+ injectRbacFilters(targetUrl, access);
233
+ auditLog.emitAuditLog(options, {
234
+ actor,
235
+ action: "data_access",
236
+ resource: proxyPath,
237
+ decision: "ALLOW",
238
+ filters: {
239
+ clusters: access.clusterFilters,
240
+ projects: access.projectFilters
241
+ }
242
+ });
243
+ const upstreamResponse = await fetch(targetUrl.toString(), {
244
+ headers: {
245
+ "Content-Type": "application/json",
246
+ Accept: req.headers.accept || "application/json",
247
+ Authorization: `Bearer ${token}`
248
+ },
249
+ method: "GET"
250
+ });
251
+ const contentType = upstreamResponse.headers.get("content-type") || "";
252
+ res.status(upstreamResponse.status);
253
+ if (contentType.includes("application/json")) {
254
+ return res.json(await upstreamResponse.json());
255
+ }
256
+ res.set("Content-Type", contentType);
257
+ return res.send(await upstreamResponse.text());
258
+ } catch (error) {
259
+ options.logger.error("Secure proxy error", error);
260
+ return res.status(500).json({ error: "Internal proxy error" });
261
+ }
262
+ };
263
+
264
+ exports.secureProxy = secureProxy;
265
+ //# sourceMappingURL=secureProxy.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secureProxy.cjs.js","sources":["../../src/routes/secureProxy.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { RequestHandler, Request } from 'express';\nimport type { RouterOptions } from '../models/RouterOptions';\nimport {\n authorize,\n filterAuthorizedClustersAndProjects,\n} from '../util/checkPermissions';\nimport {\n rosPluginPermissions,\n costPluginPermissions,\n} from '@red-hat-developer-hub/plugin-cost-management-common/permissions';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport { getTokenFromApi } from '../util/tokenUtil';\nimport { DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL } from '../util/constant';\nimport { resolveActor, emitAuditLog } from '../util/auditLog';\n\nconst CACHE_TTL = 15 * 60 * 1000;\n\ninterface AccessResult {\n decision: string;\n clusterFilters: string[];\n projectFilters: string[];\n filterStyle: 'ros' | 'cost';\n}\n\n/**\n * Determines the RBAC scope for a given proxy path and resolves\n * the authorized cluster/project filters server-side.\n */\nasync function resolveAccess(\n req: Request,\n proxyPath: string,\n options: RouterOptions,\n): Promise<AccessResult> {\n const isOptimizations = proxyPath.startsWith('recommendations/');\n\n if (isOptimizations) {\n return resolveOptimizationsAccess(req, options);\n }\n return resolveCostManagementAccess(req, options);\n}\n\ninterface CacheConfig {\n clusterKey: string;\n projectKey: string;\n}\n\ntype DataFetcher = (\n options: RouterOptions,\n) => Promise<{ clusters: Record<string, string>; projects: string[] } | null>;\n\n/**\n * Common RBAC resolution: check plugin-level permission, fetch + cache\n * cluster/project data, filter authorised entries, and return the result.\n */\nasync function resolveAccessForSection(\n req: Request,\n options: RouterOptions,\n pluginPerms: typeof rosPluginPermissions,\n filterStyle: AccessResult['filterStyle'],\n cacheKeys: CacheConfig,\n fetchData: DataFetcher,\n): Promise<AccessResult> {\n const { permissions, httpAuth, cache } = options;\n const deny = (): AccessResult => ({\n decision: 'DENY',\n clusterFilters: [],\n projectFilters: [],\n filterStyle,\n });\n\n const pluginDecision = await authorize(\n req,\n pluginPerms,\n permissions,\n httpAuth,\n );\n if (pluginDecision.result === AuthorizeResult.ALLOW) {\n return {\n decision: 'ALLOW',\n clusterFilters: [],\n projectFilters: [],\n filterStyle,\n };\n }\n\n let clusterDataMap: Record<string, string> = {};\n let allProjects: string[] = [];\n\n const cachedClusters = (await cache.get(cacheKeys.clusterKey)) as\n | Record<string, string>\n | undefined;\n const cachedProjects = (await cache.get(cacheKeys.projectKey)) as\n | string[]\n | undefined;\n\n if (cachedClusters && cachedProjects) {\n clusterDataMap = cachedClusters;\n allProjects = cachedProjects;\n } else {\n const result = await fetchData(options);\n if (!result) return deny();\n\n clusterDataMap = result.clusters;\n allProjects = result.projects;\n\n await Promise.all([\n cache.set(cacheKeys.clusterKey, clusterDataMap, { ttl: CACHE_TTL }),\n cache.set(cacheKeys.projectKey, allProjects, { ttl: CACHE_TTL }),\n ]);\n }\n\n const { authorizedClusterIds, authorizedClusterProjects } =\n await filterAuthorizedClustersAndProjects(\n req,\n permissions,\n httpAuth,\n clusterDataMap,\n allProjects,\n ...(filterStyle === 'cost' ? (['cost'] as const) : []),\n );\n\n const finalClusters = [\n ...new Set([\n ...authorizedClusterIds,\n ...authorizedClusterProjects.map(r => r.cluster),\n ]),\n ];\n const finalProjects = authorizedClusterProjects.map(r => r.project);\n\n if (finalClusters.length === 0) return deny();\n\n return {\n decision: 'ALLOW',\n clusterFilters: finalClusters,\n projectFilters: finalProjects,\n filterStyle,\n };\n}\n\nasync function resolveOptimizationsAccess(\n req: Request,\n options: RouterOptions,\n): Promise<AccessResult> {\n return resolveAccessForSection(\n req,\n options,\n rosPluginPermissions,\n 'ros',\n { clusterKey: 'all_clusters_map', projectKey: 'all_projects' },\n async opts => {\n const token = await getTokenFromApi(opts);\n const response = await opts.optimizationApi.getRecommendationList(\n { query: { limit: -1, orderHow: 'desc', orderBy: 'last_reported' } },\n { token },\n );\n const list = await response.json();\n\n if ((list as any).errors || !list.data) return null;\n\n const clusters: Record<string, string> = {};\n list.data.forEach(r => {\n if (r.clusterAlias && r.clusterUuid) {\n clusters[r.clusterAlias] = r.clusterUuid;\n }\n });\n const projects = [...new Set(list.data.map(r => r.project))].filter(\n (p): p is string => p !== undefined,\n );\n\n return { clusters, projects };\n },\n );\n}\n\nasync function resolveCostManagementAccess(\n req: Request,\n options: RouterOptions,\n): Promise<AccessResult> {\n return resolveAccessForSection(\n req,\n options,\n costPluginPermissions,\n 'cost',\n { clusterKey: 'cost_clusters', projectKey: 'cost_projects' },\n async opts => {\n let token: string;\n try {\n token = await getTokenFromApi(opts);\n } catch {\n return null;\n }\n\n try {\n const [clustersResp, projectsResp] = await Promise.all([\n opts.costManagementApi.searchOpenShiftClusters('', {\n token,\n limit: 1000,\n }),\n opts.costManagementApi.searchOpenShiftProjects('', {\n token,\n limit: 1000,\n }),\n ]);\n\n const clustersData = await clustersResp.json();\n const projectsData = await projectsResp.json();\n\n if (\n (clustersData as any).errors ||\n (projectsData as any).errors ||\n !clustersData.data ||\n !projectsData.data\n ) {\n return null;\n }\n\n const clusters: Record<string, string> = {};\n clustersData.data.forEach(\n (c: { value: string; cluster_alias: string }) => {\n if (c.cluster_alias && c.value) clusters[c.cluster_alias] = c.value;\n },\n );\n const projects = [\n ...new Set(projectsData.data.map((p: { value: string }) => p.value)),\n ].filter((p): p is string => p !== undefined);\n\n return { clusters, projects };\n } catch {\n return null;\n }\n },\n );\n}\n\nfunction isPathTraversal(proxyPath: string): boolean {\n return /(?:^|\\/)\\.\\.(\\/|$)/.test(proxyPath) || proxyPath.startsWith('/');\n}\n\n/**\n * Parses the raw query string, stripping RBAC-controlled keys and\n * decoding percent-encoded parameters. Returns null if encoding is malformed.\n */\nfunction parseClientQueryParams(\n originalUrl: string,\n rbacControlledKeys: Set<string>,\n): { key: string; value: string }[] | null {\n const rawQuery = originalUrl.split('?')[1] || '';\n const rawParams = rawQuery.split('&').filter(p => p.length > 0);\n const result: { key: string; value: string }[] = [];\n\n for (const param of rawParams) {\n const eqIdx = param.indexOf('=');\n const rawKey = eqIdx >= 0 ? param.substring(0, eqIdx) : param;\n const rawVal = eqIdx >= 0 ? param.substring(eqIdx + 1) : '';\n\n try {\n const decodedKey = decodeURIComponent(rawKey);\n const decodedVal = decodeURIComponent(rawVal);\n if (!rbacControlledKeys.has(decodedKey)) {\n result.push({ key: decodedKey, value: decodedVal });\n }\n } catch {\n return null;\n }\n }\n\n return result;\n}\n\n/**\n * Appends server-side RBAC cluster/project filters to the target URL\n * using the appropriate key format for ROS vs Cost Management APIs.\n */\nfunction injectRbacFilters(targetUrl: URL, access: AccessResult): void {\n const clusterKey =\n access.filterStyle === 'ros' ? 'cluster' : 'filter[exact:cluster]';\n const projectKey =\n access.filterStyle === 'ros' ? 'project' : 'filter[exact:project]';\n\n access.clusterFilters.forEach(c =>\n targetUrl.searchParams.append(clusterKey, c),\n );\n access.projectFilters.forEach(p =>\n targetUrl.searchParams.append(projectKey, p),\n );\n}\n\n/**\n * Server-side proxy that keeps the SSO token on the backend and enforces\n * RBAC before forwarding requests to the Cost Management API.\n *\n * Replaces the previous architecture where the frontend received the SSO\n * token via GET /token and called the Backstage proxy directly.\n */\nexport const secureProxy: (options: RouterOptions) => RequestHandler =\n options => async (req, res) => {\n const { config } = options;\n const proxyPath = req.params[0];\n\n if (!proxyPath) {\n return res.status(400).json({ error: 'Missing proxy path' });\n }\n\n if (isPathTraversal(proxyPath)) {\n return res\n .status(400)\n .json({ error: 'Invalid proxy path: traversal not allowed' });\n }\n\n try {\n const access = await resolveAccess(req, proxyPath, options);\n const actor = await resolveActor(req, options);\n\n if (access.decision !== 'ALLOW') {\n emitAuditLog(options, {\n actor,\n action: 'data_access',\n resource: proxyPath,\n decision: 'DENY',\n filters: {\n clusters: access.clusterFilters,\n projects: access.projectFilters,\n },\n });\n return res.status(403).json({ error: 'Access denied by RBAC policy' });\n }\n\n const token = await getTokenFromApi(options);\n\n const targetBase =\n config?.getOptionalString('costManagementProxyBaseUrl') ??\n DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL;\n const basePath = `${targetBase}/cost-management/v1/`;\n const targetUrl = new URL(proxyPath, basePath);\n\n if (!targetUrl.pathname.startsWith(new URL(basePath).pathname)) {\n return res\n .status(400)\n .json({ error: 'Invalid proxy path: traversal not allowed' });\n }\n\n // Express qs parser converts bracket keys like filter[time_scope_value]\n // into nested objects, losing the flat key format the RHCC API expects.\n const rbacControlledKeys = new Set(\n access.filterStyle === 'ros'\n ? ['cluster', 'project']\n : ['filter[exact:cluster]', 'filter[exact:project]'],\n );\n\n const clientParams = parseClientQueryParams(\n req.originalUrl,\n rbacControlledKeys,\n );\n if (!clientParams) {\n return res\n .status(400)\n .json({ error: 'Malformed percent-encoding in query string' });\n }\n\n for (const { key, value } of clientParams) {\n targetUrl.searchParams.append(key, value);\n }\n\n injectRbacFilters(targetUrl, access);\n\n emitAuditLog(options, {\n actor,\n action: 'data_access',\n resource: proxyPath,\n decision: 'ALLOW',\n filters: {\n clusters: access.clusterFilters,\n projects: access.projectFilters,\n },\n });\n\n const upstreamResponse = await fetch(targetUrl.toString(), {\n headers: {\n 'Content-Type': 'application/json',\n Accept: req.headers.accept || 'application/json',\n Authorization: `Bearer ${token}`,\n },\n method: 'GET',\n });\n\n const contentType = upstreamResponse.headers.get('content-type') || '';\n res.status(upstreamResponse.status);\n\n if (contentType.includes('application/json')) {\n return res.json(await upstreamResponse.json());\n }\n\n res.set('Content-Type', contentType);\n return res.send(await upstreamResponse.text());\n } catch (error) {\n options.logger.error('Secure proxy error', error);\n return res.status(500).json({ error: 'Internal proxy error' });\n }\n };\n"],"names":["authorize","AuthorizeResult","filterAuthorizedClustersAndProjects","rosPluginPermissions","getTokenFromApi","costPluginPermissions","resolveActor","emitAuditLog","DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL"],"mappings":";;;;;;;;;AA+BA,MAAM,SAAA,GAAY,KAAK,EAAK,GAAA,GAAA;AAa5B,eAAe,aAAA,CACb,GACA,EAAA,SAAA,EACA,OACuB,EAAA;AACvB,EAAM,MAAA,eAAA,GAAkB,SAAU,CAAA,UAAA,CAAW,kBAAkB,CAAA;AAE/D,EAAA,IAAI,eAAiB,EAAA;AACnB,IAAO,OAAA,0BAAA,CAA2B,KAAK,OAAO,CAAA;AAAA;AAEhD,EAAO,OAAA,2BAAA,CAA4B,KAAK,OAAO,CAAA;AACjD;AAeA,eAAe,wBACb,GACA,EAAA,OAAA,EACA,WACA,EAAA,WAAA,EACA,WACA,SACuB,EAAA;AACvB,EAAA,MAAM,EAAE,WAAA,EAAa,QAAU,EAAA,KAAA,EAAU,GAAA,OAAA;AACzC,EAAA,MAAM,OAAO,OAAqB;AAAA,IAChC,QAAU,EAAA,MAAA;AAAA,IACV,gBAAgB,EAAC;AAAA,IACjB,gBAAgB,EAAC;AAAA,IACjB;AAAA,GACF,CAAA;AAEA,EAAA,MAAM,iBAAiB,MAAMA,0BAAA;AAAA,IAC3B,GAAA;AAAA,IACA,WAAA;AAAA,IACA,WAAA;AAAA,IACA;AAAA,GACF;AACA,EAAI,IAAA,cAAA,CAAe,MAAW,KAAAC,sCAAA,CAAgB,KAAO,EAAA;AACnD,IAAO,OAAA;AAAA,MACL,QAAU,EAAA,OAAA;AAAA,MACV,gBAAgB,EAAC;AAAA,MACjB,gBAAgB,EAAC;AAAA,MACjB;AAAA,KACF;AAAA;AAGF,EAAA,IAAI,iBAAyC,EAAC;AAC9C,EAAA,IAAI,cAAwB,EAAC;AAE7B,EAAA,MAAM,cAAkB,GAAA,MAAM,KAAM,CAAA,GAAA,CAAI,UAAU,UAAU,CAAA;AAG5D,EAAA,MAAM,cAAkB,GAAA,MAAM,KAAM,CAAA,GAAA,CAAI,UAAU,UAAU,CAAA;AAI5D,EAAA,IAAI,kBAAkB,cAAgB,EAAA;AACpC,IAAiB,cAAA,GAAA,cAAA;AACjB,IAAc,WAAA,GAAA,cAAA;AAAA,GACT,MAAA;AACL,IAAM,MAAA,MAAA,GAAS,MAAM,SAAA,CAAU,OAAO,CAAA;AACtC,IAAI,IAAA,CAAC,MAAQ,EAAA,OAAO,IAAK,EAAA;AAEzB,IAAA,cAAA,GAAiB,MAAO,CAAA,QAAA;AACxB,IAAA,WAAA,GAAc,MAAO,CAAA,QAAA;AAErB,IAAA,MAAM,QAAQ,GAAI,CAAA;AAAA,MAChB,KAAA,CAAM,IAAI,SAAU,CAAA,UAAA,EAAY,gBAAgB,EAAE,GAAA,EAAK,WAAW,CAAA;AAAA,MAClE,KAAA,CAAM,IAAI,SAAU,CAAA,UAAA,EAAY,aAAa,EAAE,GAAA,EAAK,WAAW;AAAA,KAChE,CAAA;AAAA;AAGH,EAAA,MAAM,EAAE,oBAAA,EAAsB,yBAA0B,EAAA,GACtD,MAAMC,oDAAA;AAAA,IACJ,GAAA;AAAA,IACA,WAAA;AAAA,IACA,QAAA;AAAA,IACA,cAAA;AAAA,IACA,WAAA;AAAA,IACA,GAAI,WAAgB,KAAA,MAAA,GAAU,CAAC,MAAM,IAAc;AAAC,GACtD;AAEF,EAAA,MAAM,aAAgB,GAAA;AAAA,IACpB,uBAAO,GAAI,CAAA;AAAA,MACT,GAAG,oBAAA;AAAA,MACH,GAAG,yBAAA,CAA0B,GAAI,CAAA,CAAA,CAAA,KAAK,EAAE,OAAO;AAAA,KAChD;AAAA,GACH;AACA,EAAA,MAAM,aAAgB,GAAA,yBAAA,CAA0B,GAAI,CAAA,CAAA,CAAA,KAAK,EAAE,OAAO,CAAA;AAElE,EAAA,IAAI,aAAc,CAAA,MAAA,KAAW,CAAG,EAAA,OAAO,IAAK,EAAA;AAE5C,EAAO,OAAA;AAAA,IACL,QAAU,EAAA,OAAA;AAAA,IACV,cAAgB,EAAA,aAAA;AAAA,IAChB,cAAgB,EAAA,aAAA;AAAA,IAChB;AAAA,GACF;AACF;AAEA,eAAe,0BAAA,CACb,KACA,OACuB,EAAA;AACvB,EAAO,OAAA,uBAAA;AAAA,IACL,GAAA;AAAA,IACA,OAAA;AAAA,IACAC,gCAAA;AAAA,IACA,KAAA;AAAA,IACA,EAAE,UAAA,EAAY,kBAAoB,EAAA,UAAA,EAAY,cAAe,EAAA;AAAA,IAC7D,OAAM,IAAQ,KAAA;AACZ,MAAM,MAAA,KAAA,GAAQ,MAAMC,yBAAA,CAAgB,IAAI,CAAA;AACxC,MAAM,MAAA,QAAA,GAAW,MAAM,IAAA,CAAK,eAAgB,CAAA,qBAAA;AAAA,QAC1C,EAAE,OAAO,EAAE,KAAA,EAAO,IAAI,QAAU,EAAA,MAAA,EAAQ,OAAS,EAAA,eAAA,EAAkB,EAAA;AAAA,QACnE,EAAE,KAAM;AAAA,OACV;AACA,MAAM,MAAA,IAAA,GAAO,MAAM,QAAA,CAAS,IAAK,EAAA;AAEjC,MAAA,IAAK,IAAa,CAAA,MAAA,IAAU,CAAC,IAAA,CAAK,MAAa,OAAA,IAAA;AAE/C,MAAA,MAAM,WAAmC,EAAC;AAC1C,MAAK,IAAA,CAAA,IAAA,CAAK,QAAQ,CAAK,CAAA,KAAA;AACrB,QAAI,IAAA,CAAA,CAAE,YAAgB,IAAA,CAAA,CAAE,WAAa,EAAA;AACnC,UAAS,QAAA,CAAA,CAAA,CAAE,YAAY,CAAA,GAAI,CAAE,CAAA,WAAA;AAAA;AAC/B,OACD,CAAA;AACD,MAAA,MAAM,QAAW,GAAA,CAAC,GAAG,IAAI,GAAI,CAAA,IAAA,CAAK,IAAK,CAAA,GAAA,CAAI,CAAK,CAAA,KAAA,CAAA,CAAE,OAAO,CAAC,CAAC,CAAE,CAAA,MAAA;AAAA,QAC3D,CAAC,MAAmB,CAAM,KAAA;AAAA,OAC5B;AAEA,MAAO,OAAA,EAAE,UAAU,QAAS,EAAA;AAAA;AAC9B,GACF;AACF;AAEA,eAAe,2BAAA,CACb,KACA,OACuB,EAAA;AACvB,EAAO,OAAA,uBAAA;AAAA,IACL,GAAA;AAAA,IACA,OAAA;AAAA,IACAC,iCAAA;AAAA,IACA,MAAA;AAAA,IACA,EAAE,UAAA,EAAY,eAAiB,EAAA,UAAA,EAAY,eAAgB,EAAA;AAAA,IAC3D,OAAM,IAAQ,KAAA;AACZ,MAAI,IAAA,KAAA;AACJ,MAAI,IAAA;AACF,QAAQ,KAAA,GAAA,MAAMD,0BAAgB,IAAI,CAAA;AAAA,OAC5B,CAAA,MAAA;AACN,QAAO,OAAA,IAAA;AAAA;AAGT,MAAI,IAAA;AACF,QAAA,MAAM,CAAC,YAAc,EAAA,YAAY,CAAI,GAAA,MAAM,QAAQ,GAAI,CAAA;AAAA,UACrD,IAAA,CAAK,iBAAkB,CAAA,uBAAA,CAAwB,EAAI,EAAA;AAAA,YACjD,KAAA;AAAA,YACA,KAAO,EAAA;AAAA,WACR,CAAA;AAAA,UACD,IAAA,CAAK,iBAAkB,CAAA,uBAAA,CAAwB,EAAI,EAAA;AAAA,YACjD,KAAA;AAAA,YACA,KAAO,EAAA;AAAA,WACR;AAAA,SACF,CAAA;AAED,QAAM,MAAA,YAAA,GAAe,MAAM,YAAA,CAAa,IAAK,EAAA;AAC7C,QAAM,MAAA,YAAA,GAAe,MAAM,YAAA,CAAa,IAAK,EAAA;AAE7C,QACG,IAAA,YAAA,CAAqB,UACrB,YAAqB,CAAA,MAAA,IACtB,CAAC,YAAa,CAAA,IAAA,IACd,CAAC,YAAA,CAAa,IACd,EAAA;AACA,UAAO,OAAA,IAAA;AAAA;AAGT,QAAA,MAAM,WAAmC,EAAC;AAC1C,QAAA,YAAA,CAAa,IAAK,CAAA,OAAA;AAAA,UAChB,CAAC,CAAgD,KAAA;AAC/C,YAAI,IAAA,CAAA,CAAE,iBAAiB,CAAE,CAAA,KAAA,WAAgB,CAAE,CAAA,aAAa,IAAI,CAAE,CAAA,KAAA;AAAA;AAChE,SACF;AACA,QAAA,MAAM,QAAW,GAAA;AAAA,UACf,GAAG,IAAI,GAAA,CAAI,YAAa,CAAA,IAAA,CAAK,IAAI,CAAC,CAAA,KAAyB,CAAE,CAAA,KAAK,CAAC;AAAA,SACnE,CAAA,MAAA,CAAO,CAAC,CAAA,KAAmB,MAAM,KAAS,CAAA,CAAA;AAE5C,QAAO,OAAA,EAAE,UAAU,QAAS,EAAA;AAAA,OACtB,CAAA,MAAA;AACN,QAAO,OAAA,IAAA;AAAA;AACT;AACF,GACF;AACF;AAEA,SAAS,gBAAgB,SAA4B,EAAA;AACnD,EAAA,OAAO,qBAAqB,IAAK,CAAA,SAAS,CAAK,IAAA,SAAA,CAAU,WAAW,GAAG,CAAA;AACzE;AAMA,SAAS,sBAAA,CACP,aACA,kBACyC,EAAA;AACzC,EAAA,MAAM,WAAW,WAAY,CAAA,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,CAAK,IAAA,EAAA;AAC9C,EAAM,MAAA,SAAA,GAAY,SAAS,KAAM,CAAA,GAAG,EAAE,MAAO,CAAA,CAAA,CAAA,KAAK,CAAE,CAAA,MAAA,GAAS,CAAC,CAAA;AAC9D,EAAA,MAAM,SAA2C,EAAC;AAElD,EAAA,KAAA,MAAW,SAAS,SAAW,EAAA;AAC7B,IAAM,MAAA,KAAA,GAAQ,KAAM,CAAA,OAAA,CAAQ,GAAG,CAAA;AAC/B,IAAA,MAAM,SAAS,KAAS,IAAA,CAAA,GAAI,MAAM,SAAU,CAAA,CAAA,EAAG,KAAK,CAAI,GAAA,KAAA;AACxD,IAAA,MAAM,SAAS,KAAS,IAAA,CAAA,GAAI,MAAM,SAAU,CAAA,KAAA,GAAQ,CAAC,CAAI,GAAA,EAAA;AAEzD,IAAI,IAAA;AACF,MAAM,MAAA,UAAA,GAAa,mBAAmB,MAAM,CAAA;AAC5C,MAAM,MAAA,UAAA,GAAa,mBAAmB,MAAM,CAAA;AAC5C,MAAA,IAAI,CAAC,kBAAA,CAAmB,GAAI,CAAA,UAAU,CAAG,EAAA;AACvC,QAAA,MAAA,CAAO,KAAK,EAAE,GAAA,EAAK,UAAY,EAAA,KAAA,EAAO,YAAY,CAAA;AAAA;AACpD,KACM,CAAA,MAAA;AACN,MAAO,OAAA,IAAA;AAAA;AACT;AAGF,EAAO,OAAA,MAAA;AACT;AAMA,SAAS,iBAAA,CAAkB,WAAgB,MAA4B,EAAA;AACrE,EAAA,MAAM,UACJ,GAAA,MAAA,CAAO,WAAgB,KAAA,KAAA,GAAQ,SAAY,GAAA,uBAAA;AAC7C,EAAA,MAAM,UACJ,GAAA,MAAA,CAAO,WAAgB,KAAA,KAAA,GAAQ,SAAY,GAAA,uBAAA;AAE7C,EAAA,MAAA,CAAO,cAAe,CAAA,OAAA;AAAA,IAAQ,CAC5B,CAAA,KAAA,SAAA,CAAU,YAAa,CAAA,MAAA,CAAO,YAAY,CAAC;AAAA,GAC7C;AACA,EAAA,MAAA,CAAO,cAAe,CAAA,OAAA;AAAA,IAAQ,CAC5B,CAAA,KAAA,SAAA,CAAU,YAAa,CAAA,MAAA,CAAO,YAAY,CAAC;AAAA,GAC7C;AACF;AASO,MAAM,WACX,GAAA,CAAA,OAAA,KAAW,OAAO,GAAA,EAAK,GAAQ,KAAA;AAC7B,EAAM,MAAA,EAAE,QAAW,GAAA,OAAA;AACnB,EAAM,MAAA,SAAA,GAAY,GAAI,CAAA,MAAA,CAAO,CAAC,CAAA;AAE9B,EAAA,IAAI,CAAC,SAAW,EAAA;AACd,IAAO,OAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,sBAAsB,CAAA;AAAA;AAG7D,EAAI,IAAA,eAAA,CAAgB,SAAS,CAAG,EAAA;AAC9B,IAAO,OAAA,GAAA,CACJ,OAAO,GAAG,CAAA,CACV,KAAK,EAAE,KAAA,EAAO,6CAA6C,CAAA;AAAA;AAGhE,EAAI,IAAA;AACF,IAAA,MAAM,MAAS,GAAA,MAAM,aAAc,CAAA,GAAA,EAAK,WAAW,OAAO,CAAA;AAC1D,IAAA,MAAM,KAAQ,GAAA,MAAME,qBAAa,CAAA,GAAA,EAAK,OAAO,CAAA;AAE7C,IAAI,IAAA,MAAA,CAAO,aAAa,OAAS,EAAA;AAC/B,MAAAC,qBAAA,CAAa,OAAS,EAAA;AAAA,QACpB,KAAA;AAAA,QACA,MAAQ,EAAA,aAAA;AAAA,QACR,QAAU,EAAA,SAAA;AAAA,QACV,QAAU,EAAA,MAAA;AAAA,QACV,OAAS,EAAA;AAAA,UACP,UAAU,MAAO,CAAA,cAAA;AAAA,UACjB,UAAU,MAAO,CAAA;AAAA;AACnB,OACD,CAAA;AACD,MAAO,OAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,gCAAgC,CAAA;AAAA;AAGvE,IAAM,MAAA,KAAA,GAAQ,MAAMH,yBAAA,CAAgB,OAAO,CAAA;AAE3C,IAAA,MAAM,UACJ,GAAA,MAAA,EAAQ,iBAAkB,CAAA,4BAA4B,CACtD,IAAAI,+CAAA;AACF,IAAM,MAAA,QAAA,GAAW,GAAG,UAAU,CAAA,oBAAA,CAAA;AAC9B,IAAA,MAAM,SAAY,GAAA,IAAI,GAAI,CAAA,SAAA,EAAW,QAAQ,CAAA;AAE7C,IAAI,IAAA,CAAC,UAAU,QAAS,CAAA,UAAA,CAAW,IAAI,GAAI,CAAA,QAAQ,CAAE,CAAA,QAAQ,CAAG,EAAA;AAC9D,MAAO,OAAA,GAAA,CACJ,OAAO,GAAG,CAAA,CACV,KAAK,EAAE,KAAA,EAAO,6CAA6C,CAAA;AAAA;AAKhE,IAAA,MAAM,qBAAqB,IAAI,GAAA;AAAA,MAC7B,MAAA,CAAO,gBAAgB,KACnB,GAAA,CAAC,WAAW,SAAS,CAAA,GACrB,CAAC,uBAAA,EAAyB,uBAAuB;AAAA,KACvD;AAEA,IAAA,MAAM,YAAe,GAAA,sBAAA;AAAA,MACnB,GAAI,CAAA,WAAA;AAAA,MACJ;AAAA,KACF;AACA,IAAA,IAAI,CAAC,YAAc,EAAA;AACjB,MAAO,OAAA,GAAA,CACJ,OAAO,GAAG,CAAA,CACV,KAAK,EAAE,KAAA,EAAO,8CAA8C,CAAA;AAAA;AAGjE,IAAA,KAAA,MAAW,EAAE,GAAA,EAAK,KAAM,EAAA,IAAK,YAAc,EAAA;AACzC,MAAU,SAAA,CAAA,YAAA,CAAa,MAAO,CAAA,GAAA,EAAK,KAAK,CAAA;AAAA;AAG1C,IAAA,iBAAA,CAAkB,WAAW,MAAM,CAAA;AAEnC,IAAAD,qBAAA,CAAa,OAAS,EAAA;AAAA,MACpB,KAAA;AAAA,MACA,MAAQ,EAAA,aAAA;AAAA,MACR,QAAU,EAAA,SAAA;AAAA,MACV,QAAU,EAAA,OAAA;AAAA,MACV,OAAS,EAAA;AAAA,QACP,UAAU,MAAO,CAAA,cAAA;AAAA,QACjB,UAAU,MAAO,CAAA;AAAA;AACnB,KACD,CAAA;AAED,IAAA,MAAM,gBAAmB,GAAA,MAAM,KAAM,CAAA,SAAA,CAAU,UAAY,EAAA;AAAA,MACzD,OAAS,EAAA;AAAA,QACP,cAAgB,EAAA,kBAAA;AAAA,QAChB,MAAA,EAAQ,GAAI,CAAA,OAAA,CAAQ,MAAU,IAAA,kBAAA;AAAA,QAC9B,aAAA,EAAe,UAAU,KAAK,CAAA;AAAA,OAChC;AAAA,MACA,MAAQ,EAAA;AAAA,KACT,CAAA;AAED,IAAA,MAAM,WAAc,GAAA,gBAAA,CAAiB,OAAQ,CAAA,GAAA,CAAI,cAAc,CAAK,IAAA,EAAA;AACpE,IAAI,GAAA,CAAA,MAAA,CAAO,iBAAiB,MAAM,CAAA;AAElC,IAAI,IAAA,WAAA,CAAY,QAAS,CAAA,kBAAkB,CAAG,EAAA;AAC5C,MAAA,OAAO,GAAI,CAAA,IAAA,CAAK,MAAM,gBAAA,CAAiB,MAAM,CAAA;AAAA;AAG/C,IAAI,GAAA,CAAA,GAAA,CAAI,gBAAgB,WAAW,CAAA;AACnC,IAAA,OAAO,GAAI,CAAA,IAAA,CAAK,MAAM,gBAAA,CAAiB,MAAM,CAAA;AAAA,WACtC,KAAO,EAAA;AACd,IAAQ,OAAA,CAAA,MAAA,CAAO,KAAM,CAAA,oBAAA,EAAsB,KAAK,CAAA;AAChD,IAAO,OAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,wBAAwB,CAAA;AAAA;AAEjE;;;;"}
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var clients = require('@red-hat-developer-hub/plugin-cost-management-common/clients');
5
+ var constant = require('../util/constant.cjs.js');
6
+
7
+ const costManagementServiceRef = backendPluginApi.createServiceRef(
8
+ {
9
+ id: "cost-management-client",
10
+ defaultFactory: async (service) => backendPluginApi.createServiceFactory({
11
+ service,
12
+ deps: {
13
+ configApi: backendPluginApi.coreServices.rootConfig
14
+ },
15
+ async factory({ configApi }) {
16
+ const baseUrl = configApi.getOptionalString("costManagementProxyBaseUrl") ?? constant.DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL;
17
+ return new clients.CostManagementSlimClient({
18
+ discoveryApi: {
19
+ async getBaseUrl(_pluginId) {
20
+ return baseUrl;
21
+ }
22
+ }
23
+ });
24
+ }
25
+ })
26
+ }
27
+ );
28
+
29
+ exports.costManagementServiceRef = costManagementServiceRef;
30
+ //# sourceMappingURL=costManagementService.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"costManagementService.cjs.js","sources":["../../src/service/costManagementService.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createServiceFactory,\n createServiceRef,\n} from '@backstage/backend-plugin-api';\nimport type { CostManagementSlimApi } from '@red-hat-developer-hub/plugin-cost-management-common/clients';\nimport { CostManagementSlimClient } from '@red-hat-developer-hub/plugin-cost-management-common/clients';\nimport { DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL } from '../util/constant';\n\nexport const costManagementServiceRef = createServiceRef<CostManagementSlimApi>(\n {\n id: 'cost-management-client',\n defaultFactory: async service =>\n createServiceFactory({\n service,\n deps: {\n configApi: coreServices.rootConfig,\n },\n async factory({ configApi }): Promise<CostManagementSlimApi> {\n // Note: The client appends /cost-management/v1/... to this base URL\n const baseUrl =\n configApi.getOptionalString('costManagementProxyBaseUrl') ??\n DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL;\n\n return new CostManagementSlimClient({\n discoveryApi: {\n async getBaseUrl(_pluginId?: string) {\n return baseUrl;\n },\n },\n }) as CostManagementSlimApi;\n },\n }),\n },\n);\n"],"names":["createServiceRef","createServiceFactory","coreServices","DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL","CostManagementSlimClient"],"mappings":";;;;;;AAyBO,MAAM,wBAA2B,GAAAA,iCAAA;AAAA,EACtC;AAAA,IACE,EAAI,EAAA,wBAAA;AAAA,IACJ,cAAA,EAAgB,OAAM,OAAA,KACpBC,qCAAqB,CAAA;AAAA,MACnB,OAAA;AAAA,MACA,IAAM,EAAA;AAAA,QACJ,WAAWC,6BAAa,CAAA;AAAA,OAC1B;AAAA,MACA,MAAM,OAAA,CAAQ,EAAE,SAAA,EAA6C,EAAA;AAE3D,QAAA,MAAM,OACJ,GAAA,SAAA,CAAU,iBAAkB,CAAA,4BAA4B,CACxD,IAAAC,+CAAA;AAEF,QAAA,OAAO,IAAIC,gCAAyB,CAAA;AAAA,UAClC,YAAc,EAAA;AAAA,YACZ,MAAM,WAAW,SAAoB,EAAA;AACnC,cAAO,OAAA,OAAA;AAAA;AACT;AACF,SACD,CAAA;AAAA;AACH,KACD;AAAA;AAEP;;;;"}
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var clients = require('@red-hat-developer-hub/plugin-cost-management-common/clients');
5
+ var constant = require('../util/constant.cjs.js');
6
+
7
+ const optimizationServiceRef = backendPluginApi.createServiceRef({
8
+ id: "optimization-client",
9
+ defaultFactory: async (service) => backendPluginApi.createServiceFactory({
10
+ service,
11
+ deps: {
12
+ configApi: backendPluginApi.coreServices.rootConfig
13
+ },
14
+ async factory({ configApi }) {
15
+ const baseUrl = configApi.getOptionalString("optimizationsBaseUrl") ?? constant.DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL;
16
+ return new clients.OptimizationsClient({
17
+ discoveryApi: {
18
+ async getBaseUrl(_pluginId) {
19
+ return baseUrl;
20
+ }
21
+ }
22
+ });
23
+ }
24
+ })
25
+ });
26
+
27
+ exports.optimizationServiceRef = optimizationServiceRef;
28
+ //# sourceMappingURL=optimizationsService.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optimizationsService.cjs.js","sources":["../../src/service/optimizationsService.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createServiceFactory,\n createServiceRef,\n} from '@backstage/backend-plugin-api';\nimport type { OptimizationsApi } from '@red-hat-developer-hub/plugin-cost-management-common/clients';\nimport { OptimizationsClient } from '@red-hat-developer-hub/plugin-cost-management-common/clients';\nimport { DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL } from '../util/constant';\n\nexport const optimizationServiceRef = createServiceRef<OptimizationsApi>({\n id: 'optimization-client',\n defaultFactory: async service =>\n createServiceFactory({\n service,\n deps: {\n configApi: coreServices.rootConfig,\n },\n async factory({ configApi }): Promise<OptimizationsApi> {\n // Base URL without /cost-management/v1 since OptimizationsClient appends it\n const baseUrl =\n configApi.getOptionalString('optimizationsBaseUrl') ??\n DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL;\n\n return new OptimizationsClient({\n discoveryApi: {\n async getBaseUrl(_pluginId?: string) {\n return baseUrl;\n },\n },\n }) as OptimizationsApi;\n },\n }),\n});\n"],"names":["createServiceRef","createServiceFactory","coreServices","DEFAULT_COST_MANAGEMENT_PROXY_BASE_URL","OptimizationsClient"],"mappings":";;;;;;AAyBO,MAAM,yBAAyBA,iCAAmC,CAAA;AAAA,EACvE,EAAI,EAAA,qBAAA;AAAA,EACJ,cAAA,EAAgB,OAAM,OAAA,KACpBC,qCAAqB,CAAA;AAAA,IACnB,OAAA;AAAA,IACA,IAAM,EAAA;AAAA,MACJ,WAAWC,6BAAa,CAAA;AAAA,KAC1B;AAAA,IACA,MAAM,OAAA,CAAQ,EAAE,SAAA,EAAwC,EAAA;AAEtD,MAAA,MAAM,OACJ,GAAA,SAAA,CAAU,iBAAkB,CAAA,sBAAsB,CAClD,IAAAC,+CAAA;AAEF,MAAA,OAAO,IAAIC,2BAAoB,CAAA;AAAA,QAC7B,YAAc,EAAA;AAAA,UACZ,MAAM,WAAW,SAAoB,EAAA;AACnC,YAAO,OAAA,OAAA;AAAA;AACT;AACF,OACD,CAAA;AAAA;AACH,GACD;AACL,CAAC;;;;"}
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ var express = require('express');
4
+ var Router = require('express-promise-router');
5
+ var pluginPermissionNode = require('@backstage/plugin-permission-node');
6
+ var permissions = require('@red-hat-developer-hub/plugin-cost-management-common/permissions');
7
+ var access = require('../routes/access.cjs.js');
8
+ var costManagementAccess = require('../routes/costManagementAccess.cjs.js');
9
+ var secureProxy = require('../routes/secureProxy.cjs.js');
10
+ var applyRecommendation = require('../routes/applyRecommendation.cjs.js');
11
+
12
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
13
+
14
+ var express__default = /*#__PURE__*/_interopDefaultCompat(express);
15
+ var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
16
+
17
+ async function createRouter(options) {
18
+ const router = Router__default.default();
19
+ const permissionsIntegrationRouter = pluginPermissionNode.createPermissionIntegrationRouter({
20
+ permissions: [
21
+ ...permissions.rosPluginPermissions,
22
+ ...permissions.rosApplyPermissions,
23
+ ...permissions.costPluginPermissions
24
+ ]
25
+ });
26
+ router.use(express__default.default.json());
27
+ router.use(permissionsIntegrationRouter);
28
+ router.get("/health", (_req, res) => {
29
+ res.json({ status: "ok" });
30
+ });
31
+ router.get("/access", access.getAccess(options));
32
+ router.get("/access/cost-management", costManagementAccess.getCostManagementAccess(options));
33
+ router.post("/apply-recommendation", applyRecommendation.applyRecommendation(options));
34
+ router.get("/proxy/*", secureProxy.secureProxy(options));
35
+ return router;
36
+ }
37
+
38
+ exports.createRouter = createRouter;
39
+ //# sourceMappingURL=router.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport type { RouterOptions } from '../models/RouterOptions';\nimport { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';\nimport {\n rosPluginPermissions,\n rosApplyPermissions,\n costPluginPermissions,\n} from '@red-hat-developer-hub/plugin-cost-management-common/permissions';\nimport { getAccess } from '../routes/access';\nimport { getCostManagementAccess } from '../routes/costManagementAccess';\nimport { secureProxy } from '../routes/secureProxy';\nimport { applyRecommendation } from '../routes/applyRecommendation';\n\n/** @public */\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const router = Router();\n const permissionsIntegrationRouter = createPermissionIntegrationRouter({\n permissions: [\n ...rosPluginPermissions,\n ...rosApplyPermissions,\n ...costPluginPermissions,\n ],\n });\n\n router.use(express.json());\n router.use(permissionsIntegrationRouter);\n\n router.get('/health', (_req, res) => {\n res.json({ status: 'ok' });\n });\n\n router.get('/access', getAccess(options));\n\n router.get('/access/cost-management', getCostManagementAccess(options));\n\n router.post('/apply-recommendation', applyRecommendation(options));\n\n router.get('/proxy/*', secureProxy(options));\n\n return router;\n}\n"],"names":["Router","createPermissionIntegrationRouter","rosPluginPermissions","rosApplyPermissions","costPluginPermissions","express","getAccess","getCostManagementAccess","applyRecommendation","secureProxy"],"mappings":";;;;;;;;;;;;;;;;AA+BA,eAAsB,aACpB,OACyB,EAAA;AACzB,EAAA,MAAM,SAASA,uBAAO,EAAA;AACtB,EAAA,MAAM,+BAA+BC,sDAAkC,CAAA;AAAA,IACrE,WAAa,EAAA;AAAA,MACX,GAAGC,gCAAA;AAAA,MACH,GAAGC,+BAAA;AAAA,MACH,GAAGC;AAAA;AACL,GACD,CAAA;AAED,EAAO,MAAA,CAAA,GAAA,CAAIC,wBAAQ,CAAA,IAAA,EAAM,CAAA;AACzB,EAAA,MAAA,CAAO,IAAI,4BAA4B,CAAA;AAEvC,EAAA,MAAA,CAAO,GAAI,CAAA,SAAA,EAAW,CAAC,IAAA,EAAM,GAAQ,KAAA;AACnC,IAAA,GAAA,CAAI,IAAK,CAAA,EAAE,MAAQ,EAAA,IAAA,EAAM,CAAA;AAAA,GAC1B,CAAA;AAED,EAAA,MAAA,CAAO,GAAI,CAAA,SAAA,EAAWC,gBAAU,CAAA,OAAO,CAAC,CAAA;AAExC,EAAA,MAAA,CAAO,GAAI,CAAA,yBAAA,EAA2BC,4CAAwB,CAAA,OAAO,CAAC,CAAA;AAEtE,EAAA,MAAA,CAAO,IAAK,CAAA,uBAAA,EAAyBC,uCAAoB,CAAA,OAAO,CAAC,CAAA;AAEjE,EAAA,MAAA,CAAO,GAAI,CAAA,UAAA,EAAYC,uBAAY,CAAA,OAAO,CAAC,CAAA;AAE3C,EAAO,OAAA,MAAA;AACT;;;;"}
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ async function resolveActor(req, options) {
4
+ try {
5
+ const credentials = await options.httpAuth.credentials(req);
6
+ const info = await options.userInfo.getUserInfo(credentials);
7
+ return info.userEntityRef;
8
+ } catch (error) {
9
+ options.logger.warn("Failed to resolve actor identity", error);
10
+ return "unknown";
11
+ }
12
+ }
13
+ function emitAuditLog(options, entry) {
14
+ options.logger.info(JSON.stringify({ audit: true, ...entry }));
15
+ }
16
+
17
+ exports.emitAuditLog = emitAuditLog;
18
+ exports.resolveActor = resolveActor;
19
+ //# sourceMappingURL=auditLog.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auditLog.cjs.js","sources":["../../src/util/auditLog.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { Request } from 'express';\nimport type { RouterOptions } from '../models/RouterOptions';\n\nexport interface AuditEntry {\n actor: string;\n action: string;\n resource: string;\n decision: string;\n filters?: {\n clusters: string[];\n projects: string[];\n };\n meta?: Record<string, unknown>;\n}\n\n/**\n * Resolves the user entity ref from a request using the Backstage auth chain.\n * Falls back to 'unknown' if user identity cannot be determined.\n */\nexport async function resolveActor(\n req: Request,\n options: RouterOptions,\n): Promise<string> {\n try {\n const credentials = await options.httpAuth.credentials(req);\n const info = await options.userInfo.getUserInfo(credentials);\n return info.userEntityRef;\n } catch (error) {\n options.logger.warn('Failed to resolve actor identity', error);\n return 'unknown';\n }\n}\n\nexport function emitAuditLog(options: RouterOptions, entry: AuditEntry): void {\n options.logger.info(JSON.stringify({ audit: true, ...entry }));\n}\n"],"names":[],"mappings":";;AAmCsB,eAAA,YAAA,CACpB,KACA,OACiB,EAAA;AACjB,EAAI,IAAA;AACF,IAAA,MAAM,WAAc,GAAA,MAAM,OAAQ,CAAA,QAAA,CAAS,YAAY,GAAG,CAAA;AAC1D,IAAA,MAAM,IAAO,GAAA,MAAM,OAAQ,CAAA,QAAA,CAAS,YAAY,WAAW,CAAA;AAC3D,IAAA,OAAO,IAAK,CAAA,aAAA;AAAA,WACL,KAAO,EAAA;AACd,IAAQ,OAAA,CAAA,MAAA,CAAO,IAAK,CAAA,kCAAA,EAAoC,KAAK,CAAA;AAC7D,IAAO,OAAA,SAAA;AAAA;AAEX;AAEgB,SAAA,YAAA,CAAa,SAAwB,KAAyB,EAAA;AAC5E,EAAQ,OAAA,CAAA,MAAA,CAAO,IAAK,CAAA,IAAA,CAAK,SAAU,CAAA,EAAE,OAAO,IAAM,EAAA,GAAG,KAAM,EAAC,CAAC,CAAA;AAC/D;;;;;"}