@red-hat-developer-hub/backstage-plugin-adoption-insights-backend 0.9.0 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @red-hat-developer-hub/backstage-plugin-adoption-insights-backend
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8a42ace: Backstage version bump to v1.54.6
8
+
9
+ ### Patch Changes
10
+
11
+ - b7d3535: Stop forwarding the caller's raw Authorization header when resolving TechDocs titles; mint a plugin-to-plugin token on behalf of the calling user, and reject unsafe entity path segments.
12
+ - Updated dependencies [8a42ace]
13
+ - @red-hat-developer-hub/backstage-plugin-adoption-insights-common@1.0.0
14
+
15
+ ## 0.9.1
16
+
17
+ ### Patch Changes
18
+
19
+ - d623d36: Refreshed `yarn.lock` to align resolved dependency versions.
20
+ - Updated dependencies [d623d36]
21
+ - @red-hat-developer-hub/backstage-plugin-adoption-insights-common@0.9.1
22
+
3
23
  ## 0.9.0
4
24
 
5
25
  ### Minor Changes
package/README.md CHANGED
@@ -104,6 +104,8 @@ If you want to run the entire project, including the frontend, run `yarn start`
104
104
 
105
105
  `GET /api/adoption-insights/events`
106
106
 
107
+ When `type=top_techdocs`, the backend may enrich each row with a TechDocs `site_name`. That lookup uses Backstage plugin-to-plugin authentication (`getPluginRequestToken`) on behalf of the calling user — it does not forward the caller's raw `Authorization` header. TechDocs therefore evaluates `catalog.entity.read` for that user; entities they cannot read fall back to the entity name, as do failed lookups and invalid entity paths. Rows with empty `namespace` / `kind` / `name` keep an empty `site_name` (TechDocs root).
108
+
107
109
  ## Query Parameters
108
110
 
109
111
  | Parameter | Type | Required | Description |
@@ -9,20 +9,33 @@ var event = require('../validation/event.cjs.js');
9
9
  var eventRequest = require('../validation/event-request.cjs.js');
10
10
  var ValidationError = require('../validation/ValidationError.cjs.js');
11
11
  var config = require('../utils/config.cjs.js');
12
+ var techdocsMetadata = require('../utils/techdocsMetadata.cjs.js');
12
13
 
14
+ function readTechDocsSiteName(payload) {
15
+ if (typeof payload !== "object" || payload === null || !("site_name" in payload)) {
16
+ return void 0;
17
+ }
18
+ const siteName = payload.site_name;
19
+ return typeof siteName === "string" ? siteName : void 0;
20
+ }
13
21
  class EventApiController {
14
22
  database;
15
23
  config;
16
24
  processor;
17
25
  auditor;
18
- constructor(eventDatabase, processor, config, auditor) {
26
+ auth;
27
+ httpAuth;
28
+ discovery;
29
+ logger;
30
+ constructor(eventDatabase, processor, config, auditor, auth, httpAuth, discovery, logger) {
19
31
  this.database = eventDatabase;
20
32
  this.processor = processor;
21
33
  this.config = config;
22
34
  this.auditor = auditor;
23
- }
24
- async getBaseUrl(pluginId) {
25
- return `${this.config.getString("backend.baseUrl")}/api/${pluginId}`;
35
+ this.auth = auth;
36
+ this.httpAuth = httpAuth;
37
+ this.discovery = discovery;
38
+ this.logger = logger;
26
39
  }
27
40
  processIncomingEvents(events, auditEvent) {
28
41
  const proccessedEvents = events.filter((e) => !!e.context?.userId).map((event) => new Event.Event(event, this.database.isJsonSupported()));
@@ -124,32 +137,120 @@ class EventApiController {
124
137
  }
125
138
  }
126
139
  async getTechdocsMetadata(req, result) {
127
- const promises = [];
128
- const baseUrl = await this.getBaseUrl("techdocs");
129
- result.data.forEach((row) => {
140
+ const rowsToFetch = [];
141
+ for (const row of result.data) {
130
142
  if (!row.namespace || !row.kind || !row.name) {
131
143
  row.site_name = "";
132
- } else {
133
- promises.push(
134
- fetch(
135
- `${baseUrl}/metadata/techdocs/${row.namespace}/${row.kind}/${row.name}`,
136
- {
137
- headers: {
138
- Accept: "application/json",
139
- Authorization: req.headers.authorization
140
- }
141
- }
142
- ).then(async (response) => {
143
- const data = await response.json();
144
- row.site_name = data.site_name ?? row.name;
145
- }).catch((e) => {
146
- console.warn(e);
147
- row.site_name = row.name;
148
- })
144
+ continue;
145
+ }
146
+ const parts = {
147
+ namespace: row.namespace,
148
+ kind: row.kind,
149
+ name: row.name
150
+ };
151
+ if (!techdocsMetadata.isSafeTechDocsEntitySegment(parts.namespace) || !techdocsMetadata.isSafeTechDocsEntitySegment(parts.kind) || !techdocsMetadata.isSafeTechDocsEntitySegment(parts.name)) {
152
+ this.logger.warn(
153
+ "Skipping TechDocs metadata fetch for unsafe entity path",
154
+ {
155
+ namespace: parts.namespace,
156
+ kind: parts.kind,
157
+ name: parts.name
158
+ }
149
159
  );
160
+ row.site_name = row.name;
161
+ continue;
150
162
  }
151
- });
152
- await Promise.all(promises);
163
+ rowsToFetch.push({ row, parts });
164
+ }
165
+ if (rowsToFetch.length === 0) {
166
+ return;
167
+ }
168
+ let baseUrl;
169
+ let token;
170
+ try {
171
+ baseUrl = await this.discovery.getBaseUrl("techdocs");
172
+ const credentials = await this.httpAuth.credentials(req, {
173
+ allow: ["user"]
174
+ });
175
+ ({ token } = await this.auth.getPluginRequestToken({
176
+ onBehalfOf: credentials,
177
+ targetPluginId: "techdocs"
178
+ }));
179
+ } catch (error) {
180
+ this.logger.warn(
181
+ "Failed to prepare TechDocs metadata lookup; falling back to entity names",
182
+ {
183
+ error: error instanceof Error ? error.message : String(error)
184
+ }
185
+ );
186
+ for (const { row } of rowsToFetch) {
187
+ row.site_name = row.name;
188
+ }
189
+ return;
190
+ }
191
+ const fetchTargets = [];
192
+ for (const { row, parts } of rowsToFetch) {
193
+ const url = techdocsMetadata.buildTechDocsMetadataUrl(baseUrl, parts);
194
+ if (!url) {
195
+ this.logger.warn(
196
+ "Skipping TechDocs metadata fetch for unsafe entity path",
197
+ {
198
+ namespace: parts.namespace,
199
+ kind: parts.kind,
200
+ name: parts.name
201
+ }
202
+ );
203
+ row.site_name = row.name;
204
+ continue;
205
+ }
206
+ fetchTargets.push({ row, url });
207
+ }
208
+ await Promise.all(
209
+ fetchTargets.map(
210
+ ({ row, url }) => fetch(url, {
211
+ headers: {
212
+ Accept: "application/json",
213
+ Authorization: `Bearer ${token}`
214
+ }
215
+ }).then(async (response) => {
216
+ if (!response.ok) {
217
+ if (response.status === 404) {
218
+ this.logger.debug(
219
+ "TechDocs metadata not found; falling back to entity name",
220
+ {
221
+ kind: row.kind,
222
+ name: row.name,
223
+ namespace: row.namespace,
224
+ status: response.status
225
+ }
226
+ );
227
+ } else {
228
+ this.logger.warn(
229
+ "TechDocs metadata request failed; falling back to entity name",
230
+ {
231
+ kind: row.kind,
232
+ name: row.name,
233
+ namespace: row.namespace,
234
+ status: response.status
235
+ }
236
+ );
237
+ }
238
+ row.site_name = row.name;
239
+ return;
240
+ }
241
+ const data = await response.json();
242
+ row.site_name = readTechDocsSiteName(data) ?? row.name;
243
+ }).catch((error) => {
244
+ this.logger.warn("Failed to fetch TechDocs metadata", {
245
+ kind: row.kind,
246
+ name: row.name,
247
+ namespace: row.namespace,
248
+ error: error instanceof Error ? error.message : String(error)
249
+ });
250
+ row.site_name = row.name;
251
+ })
252
+ )
253
+ );
153
254
  }
154
255
  }
155
256
 
@@ -1 +1 @@
1
- {"version":3,"file":"EventApiController.cjs.js","sources":["../../src/controllers/EventApiController.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 */\nimport { Request, Response } from 'express';\nimport { json2csv as Parser } from 'json-2-csv';\nimport { Knex } from 'knex';\nimport { AnalyticsEvent } from '@backstage/core-plugin-api';\nimport { EventDatabase, Filters } from '../database/event-database';\nimport { EventBatchProcessor } from '../domain/EventBatchProcessor';\nimport { Event } from '../models/Event';\nimport { QueryParams, QueryType } from '../types/event-request';\nimport { toEndOfDayUTC, toStartOfDayUTC } from '../utils/date';\nimport { EventSchema } from '../validation/event';\nimport { EventRequestSchema } from '../validation/event-request';\nimport { ValidationError } from '../validation/ValidationError';\nimport {\n AuditorService,\n AuditorServiceEvent,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport { getLicensedUsersCount } from '../utils/config';\nimport { TechDocsCount, TopTechDocsCount } from '../types/event';\n\nclass EventApiController {\n private readonly database: EventDatabase;\n private readonly config: RootConfigService;\n private readonly processor: EventBatchProcessor;\n private readonly auditor: AuditorService;\n\n constructor(\n eventDatabase: EventDatabase,\n processor: EventBatchProcessor,\n config: RootConfigService,\n auditor: AuditorService,\n ) {\n this.database = eventDatabase;\n this.processor = processor;\n this.config = config;\n this.auditor = auditor;\n }\n\n async getBaseUrl(pluginId: string): Promise<string> {\n return `${this.config.getString('backend.baseUrl')}/api/${pluginId}`;\n }\n\n private processIncomingEvents(\n events: AnalyticsEvent[],\n auditEvent: AuditorServiceEvent,\n ): void {\n const proccessedEvents = events\n .filter(e => !!e.context?.userId)\n .map(event => new Event(event, this.database.isJsonSupported()));\n\n proccessedEvents.forEach(event => {\n const result = EventSchema.safeParse(event);\n\n if (!result.success) {\n auditEvent.fail({\n error: new Error(JSON.stringify(result.error.flatten())),\n });\n throw new ValidationError('Invalid event data', result.error.flatten());\n }\n auditEvent.success({ meta: { eventId: event.id } });\n return this.processor.addEvent(event);\n });\n }\n\n async trackEvents(\n req: Request<{}, {}, AnalyticsEvent[]>,\n res: Response,\n ): Promise<void> {\n const events = req.body;\n\n const auditEvent = await this.auditor.createEvent({\n eventId: 'capture-event',\n request: req,\n });\n\n try {\n this.processIncomingEvents(events, auditEvent);\n res.status(200).json({ success: true, message: 'Event received' });\n } catch (error) {\n auditEvent.fail({\n error: error instanceof Error ? error : new Error(String(error)),\n });\n res\n .status(400)\n .json({ message: error.message, errors: error.details.fieldErrors });\n }\n }\n\n // Get insights based on the type of data requested\n async getInsights(\n req: Request<{}, {}, {}, QueryParams>,\n res: Response,\n ): Promise<void> {\n const auditEvent = await this.auditor.createEvent({\n eventId: 'get-insights',\n request: req,\n });\n\n const parsed = EventRequestSchema.safeParse(req.query);\n if (!parsed.success) {\n auditEvent.fail({\n error: new Error(\n JSON.stringify({\n message: 'Invalid query',\n errors: parsed.error.flatten().fieldErrors,\n }),\n ),\n });\n res.status(400).json({\n message: 'Invalid query',\n errors: parsed.error.flatten().fieldErrors,\n });\n return;\n }\n const { type, format, ...params } = parsed.data;\n const licensedUsers = getLicensedUsersCount(this.config);\n const filters: Filters = {\n ...params,\n end_date: toEndOfDayUTC(params.end_date, params.timezone) as string,\n start_date: toStartOfDayUTC(params.start_date, params.timezone) as string,\n };\n const db = this.database;\n\n db.setFilters(filters);\n db.setConfig({ licensedUsers });\n const queryHandlers: Record<QueryType, () => Promise<Knex.QueryBuilder>> = {\n total_users: () => db.getUsers(),\n active_users: () => db.getDailyUsers(),\n top_searches: () => db.getTopSearches(),\n top_plugins: () => db.getTopPluginViews(),\n top_techdocs: () => db.getTopTechDocsViews(),\n top_templates: () => db.getTopTemplateViews(),\n top_catalog_entities: () => db.getTopCatalogEntitiesViews(),\n };\n\n try {\n const result = await queryHandlers[type as QueryType]();\n\n if (type === 'top_techdocs') {\n await this.getTechdocsMetadata(req, result);\n }\n\n auditEvent.success({\n meta: {\n queryType: type,\n format: format,\n resultsCount: result.data?.length || 0,\n },\n });\n\n if (format === 'csv' && result.data) {\n const csv = Parser(result.data);\n res.header('Content-Type', 'text/csv');\n res.attachment(`adoption_insights_${type}.csv`);\n res.send(csv);\n return;\n }\n res.json(result);\n } catch (error) {\n auditEvent.fail({\n error: error instanceof Error ? error : new Error(String(error)),\n });\n res.status(500).json({ error: 'Internal Server Error' });\n return;\n }\n }\n\n async getTechdocsMetadata(\n req: Request<{}, {}, {}, QueryParams>,\n result: TopTechDocsCount,\n ) {\n const promises: Promise<void>[] = [];\n const baseUrl = await this.getBaseUrl('techdocs');\n\n result.data.forEach((row: TechDocsCount) => {\n if (!row.namespace || !row.kind || !row.name) {\n row.site_name = '';\n } else {\n promises.push(\n fetch(\n `${baseUrl}/metadata/techdocs/${row.namespace}/${row.kind}/${row.name}`,\n {\n headers: {\n Accept: 'application/json',\n Authorization: req.headers.authorization as string,\n },\n },\n )\n .then(async response => {\n const data = await response.json();\n row.site_name = data.site_name ?? row.name;\n })\n .catch(e => {\n console.warn(e);\n row.site_name = row.name;\n }),\n );\n }\n });\n\n await Promise.all(promises);\n }\n}\n\nexport default EventApiController;\n"],"names":["Event","event","EventSchema","ValidationError","EventRequestSchema","getLicensedUsersCount","toEndOfDayUTC","toStartOfDayUTC","Parser"],"mappings":"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"}
1
+ {"version":3,"file":"EventApiController.cjs.js","sources":["../../src/controllers/EventApiController.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 */\nimport { Request, Response } from 'express';\nimport { json2csv as Parser } from 'json-2-csv';\nimport { Knex } from 'knex';\nimport { AnalyticsEvent } from '@backstage/core-plugin-api';\nimport { EventDatabase, Filters } from '../database/event-database';\nimport { EventBatchProcessor } from '../domain/EventBatchProcessor';\nimport { Event } from '../models/Event';\nimport { QueryParams, QueryType } from '../types/event-request';\nimport { toEndOfDayUTC, toStartOfDayUTC } from '../utils/date';\nimport { EventSchema } from '../validation/event';\nimport { EventRequestSchema } from '../validation/event-request';\nimport { ValidationError } from '../validation/ValidationError';\nimport {\n AuditorService,\n AuditorServiceEvent,\n AuthService,\n DiscoveryService,\n HttpAuthService,\n LoggerService,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport { getLicensedUsersCount } from '../utils/config';\nimport { TechDocsCount, TopTechDocsCount } from '../types/event';\nimport {\n buildTechDocsMetadataUrl,\n isSafeTechDocsEntitySegment,\n TechDocsEntityParts,\n} from '../utils/techdocsMetadata';\n\nfunction readTechDocsSiteName(payload: unknown): string | undefined {\n if (\n typeof payload !== 'object' ||\n payload === null ||\n !('site_name' in payload)\n ) {\n return undefined;\n }\n const siteName = (payload as { site_name: unknown }).site_name;\n return typeof siteName === 'string' ? siteName : undefined;\n}\n\nclass EventApiController {\n private readonly database: EventDatabase;\n private readonly config: RootConfigService;\n private readonly processor: EventBatchProcessor;\n private readonly auditor: AuditorService;\n private readonly auth: AuthService;\n private readonly httpAuth: HttpAuthService;\n private readonly discovery: DiscoveryService;\n private readonly logger: LoggerService;\n\n constructor(\n eventDatabase: EventDatabase,\n processor: EventBatchProcessor,\n config: RootConfigService,\n auditor: AuditorService,\n auth: AuthService,\n httpAuth: HttpAuthService,\n discovery: DiscoveryService,\n logger: LoggerService,\n ) {\n this.database = eventDatabase;\n this.processor = processor;\n this.config = config;\n this.auditor = auditor;\n this.auth = auth;\n this.httpAuth = httpAuth;\n this.discovery = discovery;\n this.logger = logger;\n }\n\n private processIncomingEvents(\n events: AnalyticsEvent[],\n auditEvent: AuditorServiceEvent,\n ): void {\n const proccessedEvents = events\n .filter(e => !!e.context?.userId)\n .map(event => new Event(event, this.database.isJsonSupported()));\n\n proccessedEvents.forEach(event => {\n const result = EventSchema.safeParse(event);\n\n if (!result.success) {\n auditEvent.fail({\n error: new Error(JSON.stringify(result.error.flatten())),\n });\n throw new ValidationError('Invalid event data', result.error.flatten());\n }\n auditEvent.success({ meta: { eventId: event.id } });\n return this.processor.addEvent(event);\n });\n }\n\n async trackEvents(\n req: Request<{}, {}, AnalyticsEvent[]>,\n res: Response,\n ): Promise<void> {\n const events = req.body;\n\n const auditEvent = await this.auditor.createEvent({\n eventId: 'capture-event',\n request: req,\n });\n\n try {\n this.processIncomingEvents(events, auditEvent);\n res.status(200).json({ success: true, message: 'Event received' });\n } catch (error) {\n auditEvent.fail({\n error: error instanceof Error ? error : new Error(String(error)),\n });\n res\n .status(400)\n .json({ message: error.message, errors: error.details.fieldErrors });\n }\n }\n\n // Get insights based on the type of data requested\n async getInsights(\n req: Request<{}, {}, {}, QueryParams>,\n res: Response,\n ): Promise<void> {\n const auditEvent = await this.auditor.createEvent({\n eventId: 'get-insights',\n request: req,\n });\n\n const parsed = EventRequestSchema.safeParse(req.query);\n if (!parsed.success) {\n auditEvent.fail({\n error: new Error(\n JSON.stringify({\n message: 'Invalid query',\n errors: parsed.error.flatten().fieldErrors,\n }),\n ),\n });\n res.status(400).json({\n message: 'Invalid query',\n errors: parsed.error.flatten().fieldErrors,\n });\n return;\n }\n const { type, format, ...params } = parsed.data;\n const licensedUsers = getLicensedUsersCount(this.config);\n const filters: Filters = {\n ...params,\n end_date: toEndOfDayUTC(params.end_date, params.timezone) as string,\n start_date: toStartOfDayUTC(params.start_date, params.timezone) as string,\n };\n const db = this.database;\n\n db.setFilters(filters);\n db.setConfig({ licensedUsers });\n const queryHandlers: Record<QueryType, () => Promise<Knex.QueryBuilder>> = {\n total_users: () => db.getUsers(),\n active_users: () => db.getDailyUsers(),\n top_searches: () => db.getTopSearches(),\n top_plugins: () => db.getTopPluginViews(),\n top_techdocs: () => db.getTopTechDocsViews(),\n top_templates: () => db.getTopTemplateViews(),\n top_catalog_entities: () => db.getTopCatalogEntitiesViews(),\n };\n\n try {\n const result = await queryHandlers[type as QueryType]();\n\n if (type === 'top_techdocs') {\n await this.getTechdocsMetadata(req, result);\n }\n\n auditEvent.success({\n meta: {\n queryType: type,\n format: format,\n resultsCount: result.data?.length || 0,\n },\n });\n\n if (format === 'csv' && result.data) {\n const csv = Parser(result.data);\n res.header('Content-Type', 'text/csv');\n res.attachment(`adoption_insights_${type}.csv`);\n res.send(csv);\n return;\n }\n res.json(result);\n } catch (error) {\n auditEvent.fail({\n error: error instanceof Error ? error : new Error(String(error)),\n });\n res.status(500).json({ error: 'Internal Server Error' });\n return;\n }\n }\n\n async getTechdocsMetadata(\n req: Request<{}, {}, {}, QueryParams>,\n result: TopTechDocsCount,\n ) {\n const rowsToFetch: { row: TechDocsCount; parts: TechDocsEntityParts }[] =\n [];\n\n for (const row of result.data) {\n if (!row.namespace || !row.kind || !row.name) {\n row.site_name = '';\n continue;\n }\n\n const parts: TechDocsEntityParts = {\n namespace: row.namespace,\n kind: row.kind,\n name: row.name,\n };\n if (\n !isSafeTechDocsEntitySegment(parts.namespace) ||\n !isSafeTechDocsEntitySegment(parts.kind) ||\n !isSafeTechDocsEntitySegment(parts.name)\n ) {\n this.logger.warn(\n 'Skipping TechDocs metadata fetch for unsafe entity path',\n {\n namespace: parts.namespace,\n kind: parts.kind,\n name: parts.name,\n },\n );\n row.site_name = row.name;\n continue;\n }\n\n rowsToFetch.push({ row, parts });\n }\n\n if (rowsToFetch.length === 0) {\n return;\n }\n\n let baseUrl: string;\n let token: string;\n try {\n baseUrl = await this.discovery.getBaseUrl('techdocs');\n const credentials = await this.httpAuth.credentials(req, {\n allow: ['user'],\n });\n ({ token } = await this.auth.getPluginRequestToken({\n onBehalfOf: credentials,\n targetPluginId: 'techdocs',\n }));\n } catch (error) {\n this.logger.warn(\n 'Failed to prepare TechDocs metadata lookup; falling back to entity names',\n {\n error: error instanceof Error ? error.message : String(error),\n },\n );\n for (const { row } of rowsToFetch) {\n row.site_name = row.name;\n }\n return;\n }\n\n const fetchTargets: { row: TechDocsCount; url: string }[] = [];\n for (const { row, parts } of rowsToFetch) {\n const url = buildTechDocsMetadataUrl(baseUrl, parts);\n if (!url) {\n this.logger.warn(\n 'Skipping TechDocs metadata fetch for unsafe entity path',\n {\n namespace: parts.namespace,\n kind: parts.kind,\n name: parts.name,\n },\n );\n row.site_name = row.name;\n continue;\n }\n fetchTargets.push({ row, url });\n }\n\n await Promise.all(\n fetchTargets.map(({ row, url }) =>\n fetch(url, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n })\n .then(async response => {\n if (!response.ok) {\n if (response.status === 404) {\n this.logger.debug(\n 'TechDocs metadata not found; falling back to entity name',\n {\n kind: row.kind,\n name: row.name,\n namespace: row.namespace,\n status: response.status,\n },\n );\n } else {\n this.logger.warn(\n 'TechDocs metadata request failed; falling back to entity name',\n {\n kind: row.kind,\n name: row.name,\n namespace: row.namespace,\n status: response.status,\n },\n );\n }\n row.site_name = row.name;\n return;\n }\n const data: unknown = await response.json();\n row.site_name = readTechDocsSiteName(data) ?? row.name;\n })\n .catch(error => {\n this.logger.warn('Failed to fetch TechDocs metadata', {\n kind: row.kind,\n name: row.name,\n namespace: row.namespace,\n error: error instanceof Error ? error.message : String(error),\n });\n row.site_name = row.name;\n }),\n ),\n );\n }\n}\n\nexport default EventApiController;\n"],"names":["Event","event","EventSchema","ValidationError","EventRequestSchema","getLicensedUsersCount","toEndOfDayUTC","toStartOfDayUTC","Parser","isSafeTechDocsEntitySegment","buildTechDocsMetadataUrl"],"mappings":"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"}
@@ -18,6 +18,8 @@ const adoptionInsightsPlugin = backendPluginApi.createBackendPlugin({
18
18
  auditor: backendPluginApi.coreServices.auditor,
19
19
  config: backendPluginApi.coreServices.rootConfig,
20
20
  logger: backendPluginApi.coreServices.logger,
21
+ auth: backendPluginApi.coreServices.auth,
22
+ discovery: backendPluginApi.coreServices.discovery,
21
23
  httpAuth: backendPluginApi.coreServices.httpAuth,
22
24
  httpRouter: backendPluginApi.coreServices.httpRouter,
23
25
  database: backendPluginApi.coreServices.database,
@@ -29,6 +31,8 @@ const adoptionInsightsPlugin = backendPluginApi.createBackendPlugin({
29
31
  auditor,
30
32
  config: config$1,
31
33
  logger,
34
+ auth,
35
+ discovery,
32
36
  httpAuth,
33
37
  httpRouter,
34
38
  database,
@@ -47,7 +51,11 @@ const adoptionInsightsPlugin = backendPluginApi.createBackendPlugin({
47
51
  db,
48
52
  processor,
49
53
  config$1,
50
- auditor
54
+ auditor,
55
+ auth,
56
+ httpAuth,
57
+ discovery,
58
+ logger
51
59
  );
52
60
  await migration.migrate(database);
53
61
  if (db.isPartitionSupported()) {
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.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 */\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { adoptionInsightsEventsReadPermission } from '@red-hat-developer-hub/backstage-plugin-adoption-insights-common';\nimport { createRouter } from './router';\nimport { migrate } from './database/migration';\nimport { DatabaseFactory } from './database/DatabaseFactory';\nimport { EventBatchProcessor } from './domain/EventBatchProcessor';\nimport EventApiController from './controllers/EventApiController';\nimport { schedulePartition } from './database/partition';\nimport { getConfigurationOptions } from './utils/config';\n\n/**\n * adoptionInsightsPlugin backend plugin\n *\n * @public\n */\nexport const adoptionInsightsPlugin = createBackendPlugin({\n pluginId: 'adoption-insights',\n register(env) {\n env.registerInit({\n deps: {\n auditor: coreServices.auditor,\n config: coreServices.rootConfig,\n logger: coreServices.logger,\n httpAuth: coreServices.httpAuth,\n httpRouter: coreServices.httpRouter,\n database: coreServices.database,\n scheduler: coreServices.scheduler,\n permissions: coreServices.permissions,\n permissionsRegistry: coreServices.permissionsRegistry,\n },\n async init({\n auditor,\n config,\n logger,\n httpAuth,\n httpRouter,\n database,\n scheduler,\n permissions,\n permissionsRegistry,\n }) {\n // Register plugin permission\n permissionsRegistry.addPermissions([\n adoptionInsightsEventsReadPermission,\n ]);\n\n // Queue configuration\n const options = getConfigurationOptions(config);\n const client = await database.getClient();\n const db = DatabaseFactory.getDatabase(client, logger);\n const processor = new EventBatchProcessor(db, logger, options);\n const eventApiController = new EventApiController(\n db,\n processor,\n config,\n auditor,\n );\n\n // Migrate database\n await migrate(database);\n\n // Schedule partition creation\n if (db.isPartitionSupported()) {\n schedulePartition(client, { logger, scheduler });\n }\n\n httpRouter.use(\n await createRouter({\n httpAuth,\n permissions,\n eventApiController,\n }),\n );\n\n httpRouter.addAuthPolicy({\n path: '/health',\n allow: 'unauthenticated',\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","config","adoptionInsightsEventsReadPermission","getConfigurationOptions","DatabaseFactory","EventBatchProcessor","EventApiController","migrate","schedulePartition","createRouter"],"mappings":";;;;;;;;;;;;AAiCO,MAAM,yBAAyBA,oCAAA,CAAoB;AAAA,EACxD,QAAA,EAAU,mBAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,SAASC,6BAAA,CAAa,OAAA;AAAA,QACtB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,YAAYA,6BAAA,CAAa,UAAA;AAAA,QACzB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,aAAaA,6BAAA,CAAa,WAAA;AAAA,QAC1B,qBAAqBA,6BAAA,CAAa;AAAA,OACpC;AAAA,MACA,MAAM,IAAA,CAAK;AAAA,QACT,OAAA;AAAA,gBACAC,QAAA;AAAA,QACA,MAAA;AAAA,QACA,QAAA;AAAA,QACA,UAAA;AAAA,QACA,QAAA;AAAA,QACA,SAAA;AAAA,QACA,WAAA;AAAA,QACA;AAAA,OACF,EAAG;AAED,QAAA,mBAAA,CAAoB,cAAA,CAAe;AAAA,UACjCC;AAAA,SACD,CAAA;AAGD,QAAA,MAAM,OAAA,GAAUC,+BAAwBF,QAAM,CAAA;AAC9C,QAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,SAAA,EAAU;AACxC,QAAA,MAAM,EAAA,GAAKG,+BAAA,CAAgB,WAAA,CAAY,MAAA,EAAQ,MAAM,CAAA;AACrD,QAAA,MAAM,SAAA,GAAY,IAAIC,uCAAA,CAAoB,EAAA,EAAI,QAAQ,OAAO,CAAA;AAC7D,QAAA,MAAM,qBAAqB,IAAIC,0BAAA;AAAA,UAC7B,EAAA;AAAA,UACA,SAAA;AAAA,UACAL,QAAA;AAAA,UACA;AAAA,SACF;AAGA,QAAA,MAAMM,kBAAQ,QAAQ,CAAA;AAGtB,QAAA,IAAI,EAAA,CAAG,sBAAqB,EAAG;AAC7B,UAAAC,2BAAA,CAAkB,MAAA,EAAQ,EAAE,MAAA,EAAQ,SAAA,EAAW,CAAA;AAAA,QACjD;AAEA,QAAA,UAAA,CAAW,GAAA;AAAA,UACT,MAAMC,mBAAA,CAAa;AAAA,YACjB,QAAA;AAAA,YACA,WAAA;AAAA,YACA;AAAA,WACD;AAAA,SACH;AAEA,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,SAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AAAA,MACH;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
1
+ {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.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 */\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { adoptionInsightsEventsReadPermission } from '@red-hat-developer-hub/backstage-plugin-adoption-insights-common';\nimport { createRouter } from './router';\nimport { migrate } from './database/migration';\nimport { DatabaseFactory } from './database/DatabaseFactory';\nimport { EventBatchProcessor } from './domain/EventBatchProcessor';\nimport EventApiController from './controllers/EventApiController';\nimport { schedulePartition } from './database/partition';\nimport { getConfigurationOptions } from './utils/config';\n\n/**\n * adoptionInsightsPlugin backend plugin\n *\n * @public\n */\nexport const adoptionInsightsPlugin = createBackendPlugin({\n pluginId: 'adoption-insights',\n register(env) {\n env.registerInit({\n deps: {\n auditor: coreServices.auditor,\n config: coreServices.rootConfig,\n logger: coreServices.logger,\n auth: coreServices.auth,\n discovery: coreServices.discovery,\n httpAuth: coreServices.httpAuth,\n httpRouter: coreServices.httpRouter,\n database: coreServices.database,\n scheduler: coreServices.scheduler,\n permissions: coreServices.permissions,\n permissionsRegistry: coreServices.permissionsRegistry,\n },\n async init({\n auditor,\n config,\n logger,\n auth,\n discovery,\n httpAuth,\n httpRouter,\n database,\n scheduler,\n permissions,\n permissionsRegistry,\n }) {\n // Register plugin permission\n permissionsRegistry.addPermissions([\n adoptionInsightsEventsReadPermission,\n ]);\n\n // Queue configuration\n const options = getConfigurationOptions(config);\n const client = await database.getClient();\n const db = DatabaseFactory.getDatabase(client, logger);\n const processor = new EventBatchProcessor(db, logger, options);\n const eventApiController = new EventApiController(\n db,\n processor,\n config,\n auditor,\n auth,\n httpAuth,\n discovery,\n logger,\n );\n\n // Migrate database\n await migrate(database);\n\n // Schedule partition creation\n if (db.isPartitionSupported()) {\n schedulePartition(client, { logger, scheduler });\n }\n\n httpRouter.use(\n await createRouter({\n httpAuth,\n permissions,\n eventApiController,\n }),\n );\n\n httpRouter.addAuthPolicy({\n path: '/health',\n allow: 'unauthenticated',\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","config","adoptionInsightsEventsReadPermission","getConfigurationOptions","DatabaseFactory","EventBatchProcessor","EventApiController","migrate","schedulePartition","createRouter"],"mappings":";;;;;;;;;;;;AAiCO,MAAM,yBAAyBA,oCAAA,CAAoB;AAAA,EACxD,QAAA,EAAU,mBAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,SAASC,6BAAA,CAAa,OAAA;AAAA,QACtB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,MAAMA,6BAAA,CAAa,IAAA;AAAA,QACnB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,YAAYA,6BAAA,CAAa,UAAA;AAAA,QACzB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,aAAaA,6BAAA,CAAa,WAAA;AAAA,QAC1B,qBAAqBA,6BAAA,CAAa;AAAA,OACpC;AAAA,MACA,MAAM,IAAA,CAAK;AAAA,QACT,OAAA;AAAA,gBACAC,QAAA;AAAA,QACA,MAAA;AAAA,QACA,IAAA;AAAA,QACA,SAAA;AAAA,QACA,QAAA;AAAA,QACA,UAAA;AAAA,QACA,QAAA;AAAA,QACA,SAAA;AAAA,QACA,WAAA;AAAA,QACA;AAAA,OACF,EAAG;AAED,QAAA,mBAAA,CAAoB,cAAA,CAAe;AAAA,UACjCC;AAAA,SACD,CAAA;AAGD,QAAA,MAAM,OAAA,GAAUC,+BAAwBF,QAAM,CAAA;AAC9C,QAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,SAAA,EAAU;AACxC,QAAA,MAAM,EAAA,GAAKG,+BAAA,CAAgB,WAAA,CAAY,MAAA,EAAQ,MAAM,CAAA;AACrD,QAAA,MAAM,SAAA,GAAY,IAAIC,uCAAA,CAAoB,EAAA,EAAI,QAAQ,OAAO,CAAA;AAC7D,QAAA,MAAM,qBAAqB,IAAIC,0BAAA;AAAA,UAC7B,EAAA;AAAA,UACA,SAAA;AAAA,UACAL,QAAA;AAAA,UACA,OAAA;AAAA,UACA,IAAA;AAAA,UACA,QAAA;AAAA,UACA,SAAA;AAAA,UACA;AAAA,SACF;AAGA,QAAA,MAAMM,kBAAQ,QAAQ,CAAA;AAGtB,QAAA,IAAI,EAAA,CAAG,sBAAqB,EAAG;AAC7B,UAAAC,2BAAA,CAAkB,MAAA,EAAQ,EAAE,MAAA,EAAQ,SAAA,EAAW,CAAA;AAAA,QACjD;AAEA,QAAA,UAAA,CAAW,GAAA;AAAA,UACT,MAAMC,mBAAA,CAAa;AAAA,YACjB,QAAA;AAAA,YACA,WAAA;AAAA,YACA;AAAA,WACD;AAAA,SACH;AAEA,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,SAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AAAA,MACH;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const MAX_SEGMENT_LENGTH = 256;
4
+ const ENTITY_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,254}[A-Za-z0-9])?$/;
5
+ function isSafeTechDocsEntitySegment(value) {
6
+ if (value.length === 0 || value.length > MAX_SEGMENT_LENGTH) {
7
+ return false;
8
+ }
9
+ if (value === "." || value === "..") {
10
+ return false;
11
+ }
12
+ if (value.includes("/") || value.includes("\\")) {
13
+ return false;
14
+ }
15
+ return ENTITY_PATH_SEGMENT_PATTERN.test(value);
16
+ }
17
+ function isTechDocsMetadataUrlContained(url, base) {
18
+ try {
19
+ const parsed = new URL(url);
20
+ const basePath = new URL(base).pathname.replace(/\/$/, "");
21
+ return parsed.pathname.startsWith(`${basePath}/metadata/techdocs/`);
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+ function buildTechDocsMetadataUrl(techdocsBaseUrl, parts) {
27
+ const { namespace, kind, name } = parts;
28
+ if (!isSafeTechDocsEntitySegment(namespace) || !isSafeTechDocsEntitySegment(kind) || !isSafeTechDocsEntitySegment(name)) {
29
+ return void 0;
30
+ }
31
+ const base = techdocsBaseUrl.endsWith("/") ? techdocsBaseUrl.slice(0, -1) : techdocsBaseUrl;
32
+ const path = `/metadata/techdocs/${encodeURIComponent(
33
+ namespace
34
+ )}/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`;
35
+ const url = `${base}${path}`;
36
+ if (!isTechDocsMetadataUrlContained(url, base)) {
37
+ return void 0;
38
+ }
39
+ return url;
40
+ }
41
+
42
+ exports.buildTechDocsMetadataUrl = buildTechDocsMetadataUrl;
43
+ exports.isSafeTechDocsEntitySegment = isSafeTechDocsEntitySegment;
44
+ exports.isTechDocsMetadataUrlContained = isTechDocsMetadataUrlContained;
45
+ //# sourceMappingURL=techdocsMetadata.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"techdocsMetadata.cjs.js","sources":["../../src/utils/techdocsMetadata.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\nexport type TechDocsEntityParts = {\n namespace: string;\n kind: string;\n name: string;\n};\n\nconst MAX_SEGMENT_LENGTH = 256;\n\n/**\n * Catalog-like entity path segment. Rejects traversal (`..`), slashes, and\n * other characters that must not be interpolated into a URL path.\n */\nconst ENTITY_PATH_SEGMENT_PATTERN =\n /^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,254}[A-Za-z0-9])?$/;\n\nexport function isSafeTechDocsEntitySegment(value: string): boolean {\n if (value.length === 0 || value.length > MAX_SEGMENT_LENGTH) {\n return false;\n }\n if (value === '.' || value === '..') {\n return false;\n }\n if (value.includes('/') || value.includes('\\\\')) {\n return false;\n }\n return ENTITY_PATH_SEGMENT_PATTERN.test(value);\n}\n\n/**\n * Returns true when `url` stays under the TechDocs metadata path for `base`\n * after URL parsing / path normalization (defense against traversal).\n */\nexport function isTechDocsMetadataUrlContained(\n url: string,\n base: string,\n): boolean {\n try {\n const parsed = new URL(url);\n const basePath = new URL(base).pathname.replace(/\\/$/, '');\n return parsed.pathname.startsWith(`${basePath}/metadata/techdocs/`);\n } catch {\n return false;\n }\n}\n\n/**\n * Builds the TechDocs metadata URL for an entity. Returns `undefined` when any\n * path segment is unsafe so callers must skip the outbound request.\n */\nexport function buildTechDocsMetadataUrl(\n techdocsBaseUrl: string,\n parts: TechDocsEntityParts,\n): string | undefined {\n const { namespace, kind, name } = parts;\n if (\n !isSafeTechDocsEntitySegment(namespace) ||\n !isSafeTechDocsEntitySegment(kind) ||\n !isSafeTechDocsEntitySegment(name)\n ) {\n return undefined;\n }\n\n const base = techdocsBaseUrl.endsWith('/')\n ? techdocsBaseUrl.slice(0, -1)\n : techdocsBaseUrl;\n const path = `/metadata/techdocs/${encodeURIComponent(\n namespace,\n )}/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`;\n const url = `${base}${path}`;\n\n if (!isTechDocsMetadataUrlContained(url, base)) {\n return undefined;\n }\n return url;\n}\n"],"names":[],"mappings":";;AAsBA,MAAM,kBAAA,GAAqB,GAAA;AAM3B,MAAM,2BAAA,GACJ,oDAAA;AAEK,SAAS,4BAA4B,KAAA,EAAwB;AAClE,EAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,IAAK,KAAA,CAAM,SAAS,kBAAA,EAAoB;AAC3D,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,KAAA,KAAU,GAAA,IAAO,KAAA,KAAU,IAAA,EAAM;AACnC,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,MAAM,QAAA,CAAS,GAAG,KAAK,KAAA,CAAM,QAAA,CAAS,IAAI,CAAA,EAAG;AAC/C,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,OAAO,2BAAA,CAA4B,KAAK,KAAK,CAAA;AAC/C;AAMO,SAAS,8BAAA,CACd,KACA,IAAA,EACS;AACT,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,GAAG,CAAA;AAC1B,IAAA,MAAM,QAAA,GAAW,IAAI,GAAA,CAAI,IAAI,EAAE,QAAA,CAAS,OAAA,CAAQ,OAAO,EAAE,CAAA;AACzD,IAAA,OAAO,MAAA,CAAO,QAAA,CAAS,UAAA,CAAW,CAAA,EAAG,QAAQ,CAAA,mBAAA,CAAqB,CAAA;AAAA,EACpE,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAMO,SAAS,wBAAA,CACd,iBACA,KAAA,EACoB;AACpB,EAAA,MAAM,EAAE,SAAA,EAAW,IAAA,EAAM,IAAA,EAAK,GAAI,KAAA;AAClC,EAAA,IACE,CAAC,2BAAA,CAA4B,SAAS,CAAA,IACtC,CAAC,2BAAA,CAA4B,IAAI,CAAA,IACjC,CAAC,2BAAA,CAA4B,IAAI,CAAA,EACjC;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,IAAA,GAAO,gBAAgB,QAAA,CAAS,GAAG,IACrC,eAAA,CAAgB,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,GAC3B,eAAA;AACJ,EAAA,MAAM,OAAO,CAAA,mBAAA,EAAsB,kBAAA;AAAA,IACjC;AAAA,GACD,IAAI,kBAAA,CAAmB,IAAI,CAAC,CAAA,CAAA,EAAI,kBAAA,CAAmB,IAAI,CAAC,CAAA,CAAA;AACzD,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,IAAI,CAAA,EAAG,IAAI,CAAA,CAAA;AAE1B,EAAA,IAAI,CAAC,8BAAA,CAA+B,GAAA,EAAK,IAAI,CAAA,EAAG;AAC9C,IAAA,OAAO,MAAA;AAAA,EACT;AACA,EAAA,OAAO,GAAA;AACT;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@red-hat-developer-hub/backstage-plugin-adoption-insights-backend",
3
- "version": "0.9.0",
3
+ "version": "1.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/index.cjs.js",
6
6
  "types": "dist/index.d.ts",
@@ -36,13 +36,13 @@
36
36
  "postpack": "backstage-cli package postpack"
37
37
  },
38
38
  "dependencies": {
39
- "@backstage/backend-defaults": "^0.17.3",
40
- "@backstage/backend-plugin-api": "^1.9.2",
41
- "@backstage/core-plugin-api": "^1.12.7",
39
+ "@backstage/backend-defaults": "^0.17.8",
40
+ "@backstage/backend-plugin-api": "^1.10.0",
41
+ "@backstage/core-plugin-api": "^1.12.9",
42
42
  "@backstage/errors": "^1.3.1",
43
- "@backstage/plugin-catalog-node": "^2.2.2",
44
- "@backstage/plugin-permission-common": "^0.9.9",
45
- "@red-hat-developer-hub/backstage-plugin-adoption-insights-common": "^0.9.0",
43
+ "@backstage/plugin-catalog-node": "^2.2.4",
44
+ "@backstage/plugin-permission-common": "^0.9.10",
45
+ "@red-hat-developer-hub/backstage-plugin-adoption-insights-common": "^1.0.0",
46
46
  "express": "^4.17.1",
47
47
  "express-promise-router": "^4.1.0",
48
48
  "json-2-csv": "^5.5.11",
@@ -52,8 +52,8 @@
52
52
  "zod": "^3.22.4"
53
53
  },
54
54
  "devDependencies": {
55
- "@backstage/backend-test-utils": "^1.11.4",
56
- "@backstage/cli": "^0.36.3",
55
+ "@backstage/backend-test-utils": "^1.11.6",
56
+ "@backstage/cli": "^0.36.5",
57
57
  "@types/express": "^4.17.6",
58
58
  "@types/luxon": "^3.5.0",
59
59
  "@types/supertest": "^2.0.12",