@red-hat-developer-hub/backstage-plugin-catalog-techdoc-url-reader-backend 0.6.3 → 0.8.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,19 @@
1
1
  # @red-hat-developer-hub/backstage-plugin-catalog-techdoc-url-reader-backend
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0a42ebd: Bump minor after the release-2.1 cut so main patch versions do not collide with the maintenance line on npm.
8
+
9
+ ## 0.7.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 3f3a377: Add OCI skill documentation URL reader: read skill source configurations from `catalog.providers.skillsCatalogProvider`, match the exact discovery-resolved connector origin and complete base pathname followed by `/skilldocs/<sourceId>/<digest>`, authenticate via service-to-service tokens, and generate TechDocs-ready MkDocs projects with response ETag caching.
14
+
15
+ Reject off-origin URLs, malformed route encoding, authenticated redirects, and successful skill-docs responses lacking a nonblank ETag. Preserve discovery deployment prefixes and prevent skill routes from falling through to legacy model-card matching.
16
+
3
17
  ## 0.6.3
4
18
 
5
19
  ### Patch Changes
package/README.md CHANGED
@@ -28,6 +28,62 @@ backend.add(
28
28
  );
29
29
  ```
30
30
 
31
+ ## OCI Skill Documentation URL Reader
32
+
33
+ In addition to model-card TechDocs, this plugin reads OCI skill documentation
34
+ from configured connector destinations.
35
+
36
+ ### Configuration
37
+
38
+ Skill documentation sources are read from the
39
+ `catalog.providers.skillsCatalogProvider` config section. Only sources with
40
+ `sourceType: 'oci'` are matched:
41
+
42
+ ```yaml
43
+ catalog:
44
+ providers:
45
+ skillsCatalogProvider:
46
+ production:
47
+ sources:
48
+ - connectorPluginId: skill-image-connector
49
+ sourceId: quay-public
50
+ sourceType: oci
51
+ ```
52
+
53
+ ### URL pattern
54
+
55
+ At service initialization, the reader resolves each configured OCI connector's
56
+ base URL through Backstage discovery. The synchronous predicate and direct
57
+ `readUrl` calls require that exact origin (scheme, host, and port) and the full
58
+ discovered base pathname. For example:
59
+
60
+ ```text
61
+ https://rhdh.example/prefix/api/skill-image-connector/skilldocs/<sourceId>/<digest>
62
+ ```
63
+
64
+ The `/prefix/api/skill-image-connector` base is preserved; `/api/<pluginId>` is
65
+ not hardcoded. Only trailing slashes on the discovered base are normalized. No
66
+ additional destination configuration is required. Directly constructed readers
67
+ must receive trusted connector bases; without them, skill documentation is not
68
+ matched. Normal backend registration supplies these bases automatically.
69
+
70
+ where `<digest>` is a `sha256:<64 hex chars>` OCI manifest digest. URLs
71
+ with query strings, fragments, URL credentials, extra path segments, or
72
+ invalid digest formats are rejected. Each route segment is decoded once;
73
+ malformed percent encoding is rejected without throwing from reader selection.
74
+ Skill-documentation routes cannot fall through to legacy model-card substring
75
+ matching, including when source IDs contain `modelcard`. Untrusted destinations
76
+ are rejected before obtaining or sending a service token.
77
+
78
+ The reader authenticates with the connector plugin via
79
+ `auth.getPluginRequestToken`, rejects redirects without forwarding service
80
+ tokens, and propagates `404` as `NotFoundError` and `304` as
81
+ `NotModifiedError`. The connector's response `ETag` is passed through for
82
+ TechDocs caching. A successful skill-docs response without a nonblank ETag is a
83
+ protocol error; the reader does not invent a validator or silently use an empty
84
+ one. Model-card requests also reject redirects; their existing ETag behavior is
85
+ preserved.
86
+
31
87
  ## Development
32
88
 
33
89
  This plugin backend can be started in a standalone mode from directly in this
@@ -14,11 +14,12 @@ var platformPath__default = /*#__PURE__*/_interopDefaultCompat(platformPath);
14
14
  var os__default = /*#__PURE__*/_interopDefaultCompat(os);
15
15
 
16
16
  class ModelCatalogBridgeUrlReaderServiceReadTreeResponse {
17
- constructor(workDir, etag, buffer, logger) {
17
+ constructor(workDir, etag, buffer, logger, siteName) {
18
18
  this.workDir = workDir;
19
19
  this.etag = etag;
20
20
  this.buffer = buffer;
21
21
  this.logger = logger;
22
+ this.siteName = siteName ?? "Model Card";
22
23
  this.logger = logger.child({
23
24
  source: 'ModelCatalogBridgeUrlReaderServiceReadTreeResponse"'
24
25
  });
@@ -27,6 +28,7 @@ class ModelCatalogBridgeUrlReaderServiceReadTreeResponse {
27
28
  etag;
28
29
  buffer;
29
30
  logger;
31
+ siteName;
30
32
  async files() {
31
33
  throw new Error(
32
34
  "ModelCatalogBridgeUrlReaderServiceReadTreeResponse does not implement files"
@@ -42,7 +44,11 @@ class ModelCatalogBridgeUrlReaderServiceReadTreeResponse {
42
44
  platformPath__default.default.join(this.workDir, "backstage-")
43
45
  );
44
46
  const mkdocsPath = platformPath__default.default.join(dir, "mkdocs.yml");
45
- const mkdocsContent = "site_name: Model Card\nnav:\n - Home: index.md\n";
47
+ const escapedSiteName = this.siteName.replace(/'/g, "''");
48
+ const mkdocsContent = `site_name: '${escapedSiteName}'
49
+ nav:
50
+ - Home: index.md
51
+ `;
46
52
  try {
47
53
  await fs__default.default.promises.writeFile(mkdocsPath, mkdocsContent);
48
54
  } catch (error) {
@@ -100,24 +106,82 @@ function readBridgeConfig(id, config) {
100
106
  defaultLifecycle: safeGetOptionalString(config, "default-lifecycle")
101
107
  };
102
108
  }
109
+ const DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
110
+ function readSkillSourceConfigs(config) {
111
+ const providerConfigs = config.getOptionalConfig(
112
+ "catalog.providers.skillsCatalogProvider"
113
+ );
114
+ if (!providerConfigs) {
115
+ return [];
116
+ }
117
+ const result = [];
118
+ const seen = /* @__PURE__ */ new Set();
119
+ for (const providerId of providerConfigs.keys()) {
120
+ const providerConfig = providerConfigs.getConfig(providerId);
121
+ const sourcesConfig = providerConfig.getOptionalConfigArray("sources");
122
+ if (!sourcesConfig) {
123
+ continue;
124
+ }
125
+ for (const sourceConfig of sourcesConfig) {
126
+ const sourceType = safeGetOptionalString(sourceConfig, "sourceType");
127
+ if (sourceType !== "oci") {
128
+ continue;
129
+ }
130
+ const connectorPluginId = safeGetOptionalString(
131
+ sourceConfig,
132
+ "connectorPluginId"
133
+ );
134
+ if (!connectorPluginId) {
135
+ continue;
136
+ }
137
+ const sourceId = safeGetOptionalString(sourceConfig, "sourceId");
138
+ if (!sourceId.trim() || sourceId.trim() === "." || sourceId.trim() === "..") {
139
+ continue;
140
+ }
141
+ const key = `${connectorPluginId}:${sourceId}`;
142
+ if (seen.has(key)) {
143
+ continue;
144
+ }
145
+ seen.add(key);
146
+ result.push({ connectorPluginId, sourceId });
147
+ }
148
+ }
149
+ return result;
150
+ }
103
151
  class ModeCatalogBridgeTechdocUrlReader {
104
152
  workDir;
105
153
  logger;
106
154
  bridgeConfigs;
155
+ skillSourceConfigs;
107
156
  // TODO we are limited in which core services can be passed into the ReaderFactory, so as a work around we
108
157
  // define these globals that are set in the plugin init method; if we merge this plugin with the
109
158
  // kserve-kubeflow-connector plugins, and bypass the need for plugin to plugin REST calls, we can
110
159
  // bypass the need for the auth service.
111
160
  static auth;
161
+ /**
162
+ * Legacy factory for model-card URLs only; skill documentation fails closed
163
+ * without discovery-resolved connector bases. Register
164
+ * `catalogTechdocUrlReaderPlugin` for OCI skill documentation support.
165
+ */
112
166
  static factory = ({ config, logger }) => {
113
167
  const reader = new ModeCatalogBridgeTechdocUrlReader(config, logger);
114
168
  return [{ reader, predicate: reader.bridgePredicate }];
115
169
  };
116
- constructor(config, logger) {
170
+ constructor(config, logger, trustedConnectorBases = /* @__PURE__ */ new Map()) {
117
171
  this.logger = logger.child({
118
172
  source: 'ModeCatalogBridgeTechdocUrlReader"'
119
173
  });
120
174
  this.bridgeConfigs = readBridgeConfigs(config);
175
+ this.skillSourceConfigs = readSkillSourceConfigs(config).flatMap((source) => {
176
+ const base = trustedConnectorBases.get(source.connectorPluginId);
177
+ if (!base) return [];
178
+ const connectorBase = new URL(base);
179
+ if (!["http:", "https:"].includes(connectorBase.protocol) || connectorBase.username || connectorBase.password || connectorBase.search || connectorBase.hash)
180
+ throw new Error(
181
+ `Invalid discovered connector base for ${source.connectorPluginId}`
182
+ );
183
+ return [{ ...source, connectorBase }];
184
+ });
121
185
  this.workDir = os__default.default.tmpdir();
122
186
  const bkend = config.getOptionalString("backend.workingDirectory");
123
187
  if (bkend) {
@@ -125,6 +189,7 @@ class ModeCatalogBridgeTechdocUrlReader {
125
189
  }
126
190
  }
127
191
  matchingBridgeConfig(url) {
192
+ if (url.pathname.includes("/skilldocs/")) return void 0;
128
193
  for (let index = 0; index < this.bridgeConfigs.length; index++) {
129
194
  const bc = this.bridgeConfigs[index];
130
195
  if (url.pathname.includes("modelcard") && url.pathname.includes(bc.id)) {
@@ -133,28 +198,72 @@ class ModeCatalogBridgeTechdocUrlReader {
133
198
  }
134
199
  return void 0;
135
200
  }
201
+ /**
202
+ * Match a URL against configured OCI skill source destinations.
203
+ *
204
+ * Requires the discovered origin and complete base pathname, followed by
205
+ * /skilldocs/<sourceId>/<digest>
206
+ *
207
+ * Rejects URLs with query strings, fragments, URL credentials,
208
+ * extra path segments, or invalid digest format.
209
+ */
210
+ matchingSkillSourceConfig(url) {
211
+ if (url.username || url.password) return void 0;
212
+ if (url.search || url.hash) return void 0;
213
+ const pathname = url.pathname;
214
+ for (const source of this.skillSourceConfigs) {
215
+ if (url.origin !== source.connectorBase.origin) continue;
216
+ const basePath = `${source.connectorBase.pathname.replace(
217
+ /\/+$/,
218
+ ""
219
+ )}/skilldocs/`;
220
+ if (!pathname.startsWith(basePath)) continue;
221
+ const remainder = pathname.substring(basePath.length);
222
+ const parts = remainder.split("/");
223
+ if (parts.length !== 2 || parts[0] === "" || parts[1] === "") continue;
224
+ let sourceId;
225
+ let digest;
226
+ try {
227
+ sourceId = decodeURIComponent(parts[0]);
228
+ digest = decodeURIComponent(parts[1]);
229
+ } catch {
230
+ continue;
231
+ }
232
+ if (sourceId !== source.sourceId) continue;
233
+ if (!DIGEST_RE.test(digest)) continue;
234
+ return source;
235
+ }
236
+ return void 0;
237
+ }
136
238
  bridgePredicate = (url) => {
137
- return this.matchingBridgeConfig(url) !== void 0;
239
+ return this.matchingBridgeConfig(url) !== void 0 || this.matchingSkillSourceConfig(url) !== void 0;
138
240
  };
139
241
  async readUrl(url, options) {
140
242
  this.logger.info(`ModelCatalogBridgeTechdocUrlReader.readUrl of ${url}`);
141
243
  const parsedUrl = new URL(url);
142
244
  const bc = this.matchingBridgeConfig(parsedUrl);
143
- if (!bc) {
144
- throw new Error(`No matching bridge config for ${url}`);
245
+ if (bc) {
246
+ return this.readModelCardUrl(url, bc, options);
247
+ }
248
+ const sc = this.matchingSkillSourceConfig(parsedUrl);
249
+ if (sc) {
250
+ return this.readSkillDocsUrl(url, sc, options);
145
251
  }
252
+ throw new Error(`No matching config for ${url}`);
253
+ }
254
+ async readModelCardUrl(url, bc, options) {
146
255
  let response;
147
256
  try {
148
257
  const token = await ModeCatalogBridgeTechdocUrlReader.auth.getPluginRequestToken({
149
258
  onBehalfOf: await ModeCatalogBridgeTechdocUrlReader.auth.getOwnServiceCredentials(),
150
259
  targetPluginId: bc.id
151
260
  });
152
- const tok = token.token;
153
261
  this.logger.info(`Using service-to-service token for ${url}`);
154
262
  response = await fetch(url, {
155
263
  method: "GET",
264
+ redirect: "error",
156
265
  headers: {
157
- Authorization: `Bearer ${tok}`
266
+ Authorization: `Bearer ${token.token}`
158
267
  },
159
268
  signal: options?.signal
160
269
  });
@@ -173,6 +282,57 @@ class ModeCatalogBridgeTechdocUrlReader {
173
282
  }
174
283
  throw new Error(message);
175
284
  }
285
+ /**
286
+ * Fetch OCI skill documentation from the connector.
287
+ *
288
+ * - Authenticates with `auth.getPluginRequestToken` targeting the
289
+ * connector plugin's registered pluginId.
290
+ * - Forwards the caller's ETag as `If-None-Match` for conditional requests.
291
+ * - Rejects redirects without forwarding service tokens.
292
+ * - Propagates 404 as NotFoundError, 304 as NotModifiedError.
293
+ */
294
+ async readSkillDocsUrl(url, source, options) {
295
+ const headers = {};
296
+ let response;
297
+ try {
298
+ const token = await ModeCatalogBridgeTechdocUrlReader.auth.getPluginRequestToken({
299
+ onBehalfOf: await ModeCatalogBridgeTechdocUrlReader.auth.getOwnServiceCredentials(),
300
+ targetPluginId: source.connectorPluginId
301
+ });
302
+ headers.Authorization = `Bearer ${token.token}`;
303
+ if (options?.etag) {
304
+ headers["If-None-Match"] = options.etag;
305
+ }
306
+ this.logger.info(
307
+ `Fetching OCI skill documentation from ${url} (connector: ${source.connectorPluginId})`
308
+ );
309
+ response = await fetch(url, {
310
+ method: "GET",
311
+ headers,
312
+ redirect: "error",
313
+ signal: options?.signal
314
+ });
315
+ } catch (e) {
316
+ throw new Error(`Unable to read ${url}, ${e}`);
317
+ }
318
+ if (response.ok) {
319
+ if (!response.headers.get("etag")?.trim()) {
320
+ await response.body?.cancel();
321
+ throw new Error(
322
+ "Skill documentation response is missing a nonblank ETag"
323
+ );
324
+ }
325
+ return urlReader.ReadUrlResponseFactory.fromResponse(response);
326
+ }
327
+ const message = `could not read ${url}, ${response.status} ${response.statusText}`;
328
+ if (response.status === 404) {
329
+ throw new errors.NotFoundError(message);
330
+ }
331
+ if (response.status === 304) {
332
+ throw new errors.NotModifiedError();
333
+ }
334
+ throw new Error(message);
335
+ }
176
336
  async search(url, options) {
177
337
  this.logger.info(
178
338
  `ModelCatalogBridgeTechdocUrlReader.search of ${url} with options ${JSON.stringify(
@@ -185,17 +345,33 @@ class ModeCatalogBridgeTechdocUrlReader {
185
345
  }
186
346
  async readTree(url, options) {
187
347
  this.logger.info(`ModelCatalogBridgeTechdocUrlReader.readTree of ${url}`);
348
+ const parsedUrl = new URL(url);
188
349
  const data = await this.readUrl(url, options);
350
+ const isSkillDocs = this.matchingBridgeConfig(parsedUrl) === void 0 && this.matchingSkillSourceConfig(parsedUrl) !== void 0;
351
+ if (isSkillDocs) {
352
+ if (!data.etag?.trim()) {
353
+ throw new Error(
354
+ "Skill documentation response is missing a nonblank ETag"
355
+ );
356
+ }
357
+ return new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(
358
+ this.workDir,
359
+ data.etag,
360
+ data.buffer(),
361
+ this.logger,
362
+ "Skill Documentation"
363
+ );
364
+ }
189
365
  const et = options?.etag;
190
366
  if (!et) {
191
- return await new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(
367
+ return new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(
192
368
  this.workDir,
193
369
  "",
194
370
  data.buffer(),
195
371
  this.logger
196
372
  );
197
373
  }
198
- return await new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(
374
+ return new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(
199
375
  this.workDir,
200
376
  et,
201
377
  data.buffer(),
@@ -207,12 +383,31 @@ const catalogTechdocUrlReaderPlugin = backendPluginApi.createServiceFactory({
207
383
  service: urlReader.urlReaderFactoriesServiceRef,
208
384
  deps: {
209
385
  logger: backendPluginApi.coreServices.logger,
210
- auth: backendPluginApi.coreServices.auth
386
+ auth: backendPluginApi.coreServices.auth,
387
+ discovery: backendPluginApi.coreServices.discovery,
388
+ config: backendPluginApi.coreServices.rootConfig
211
389
  },
212
- async factory({ logger, auth }) {
390
+ async factory({ logger, auth, discovery, config }) {
213
391
  logger.child({ source: 'catalogTechdocUrlReaderPlugin"' }).info("Registering the model catalog bridge URL reader ");
214
392
  ModeCatalogBridgeTechdocUrlReader.auth = auth;
215
- return ModeCatalogBridgeTechdocUrlReader.factory;
393
+ const trustedConnectorBases = /* @__PURE__ */ new Map();
394
+ const connectorIds = new Set(
395
+ readSkillSourceConfigs(config).map((source) => source.connectorPluginId)
396
+ );
397
+ for (const connectorId of connectorIds) {
398
+ trustedConnectorBases.set(
399
+ connectorId,
400
+ await discovery.getBaseUrl(connectorId)
401
+ );
402
+ }
403
+ return ({ config: readerConfig, logger: readerLogger }) => {
404
+ const reader = new ModeCatalogBridgeTechdocUrlReader(
405
+ readerConfig,
406
+ readerLogger,
407
+ trustedConnectorBases
408
+ );
409
+ return [{ reader, predicate: reader.bridgePredicate }];
410
+ };
216
411
  }
217
412
  });
218
413
 
@@ -221,4 +416,5 @@ exports.ModelCatalogBridgeUrlReaderServiceReadTreeResponse = ModelCatalogBridgeU
221
416
  exports.catalogTechdocUrlReaderPlugin = catalogTechdocUrlReaderPlugin;
222
417
  exports.readBridgeConfig = readBridgeConfig;
223
418
  exports.readBridgeConfigs = readBridgeConfigs;
419
+ exports.readSkillSourceConfigs = readSkillSourceConfigs;
224
420
  //# sourceMappingURL=plugin.cjs.js.map
@@ -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 */\n\nimport {\n ReaderFactory,\n ReadUrlResponseFactory,\n urlReaderFactoriesServiceRef,\n} from '@backstage/backend-defaults/urlReader';\nimport {\n coreServices,\n createServiceFactory,\n UrlReaderService,\n UrlReaderServiceReadUrlOptions,\n UrlReaderServiceSearchOptions,\n UrlReaderServiceSearchResponse,\n UrlReaderServiceReadUrlResponse,\n UrlReaderServiceReadTreeOptions,\n UrlReaderServiceReadTreeResponse,\n UrlReaderServiceReadTreeResponseFile,\n UrlReaderServiceReadTreeResponseDirOptions,\n LoggerService,\n AuthService,\n} from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { NotFoundError, NotModifiedError } from '@backstage/errors';\nimport { Readable } from 'stream';\nimport fs from 'fs';\nimport platformPath from 'path';\nimport os from 'os';\n\nexport class ModelCatalogBridgeUrlReaderServiceReadTreeResponse\n implements UrlReaderServiceReadTreeResponse\n{\n constructor(\n private readonly workDir: string,\n public readonly etag: string,\n private readonly buffer: Promise<Buffer>,\n private readonly logger: LoggerService,\n ) {\n this.logger = logger.child({\n source: 'ModelCatalogBridgeUrlReaderServiceReadTreeResponse\"',\n });\n }\n async files(): Promise<UrlReaderServiceReadTreeResponseFile[]> {\n throw new Error(\n 'ModelCatalogBridgeUrlReaderServiceReadTreeResponse does not implement files',\n );\n }\n async archive(): Promise<Readable> {\n throw new Error(\n 'ModelCatalogBridgeUrlReaderServiceReadTreeResponse does not implement archive',\n );\n }\n\n async dir(\n options?: UrlReaderServiceReadTreeResponseDirOptions,\n ): Promise<string> {\n const dir =\n options?.targetDir ??\n (await fs.promises.mkdtemp(\n platformPath.join(this.workDir, 'backstage-'),\n ));\n\n // Write a minimal mkdocs.yml that the TechDocs builder (mkdocs)\n // requires at the project root.\n const mkdocsPath = platformPath.join(dir, 'mkdocs.yml');\n const mkdocsContent = 'site_name: Model Card\\nnav:\\n - Home: index.md\\n';\n try {\n await fs.promises.writeFile(mkdocsPath, mkdocsContent);\n } catch (error) {\n this.logger.error(`Error writing mkdocs.yml to ${mkdocsPath}:`, error);\n }\n\n const subDir = platformPath.join(dir, 'docs');\n const filePath = platformPath.join(subDir, 'index.md');\n try {\n fs.mkdirSync(subDir);\n const buf = await this.buffer;\n await fs.promises.writeFile(filePath, buf);\n } catch (error) {\n this.logger.error(`Error writing to file ${filePath}:`, error);\n }\n\n return dir;\n }\n}\n\nexport type BridgeConfig = {\n id: string;\n name: string;\n kubeflowModelCatalogUrl: string;\n defaultOwner: string;\n defaultLifecycle: string;\n};\n\nexport function readBridgeConfigs(config: Config): BridgeConfig[] {\n const configs = config.getOptionalConfig('catalog.providers.modelCatalog');\n if (!configs) {\n return [];\n }\n const result: BridgeConfig[] = [];\n for (const connectorId of configs.keys()) {\n const connectorConfig = configs.getConfig(connectorId);\n for (const clusterKey of connectorConfig.keys()) {\n const clusterConfig = connectorConfig.getOptionalConfig(clusterKey);\n if (!clusterConfig) {\n continue;\n }\n if (clusterConfig.has('frequency') || clusterConfig.has('timeout')) {\n continue;\n }\n result.push(readBridgeConfig(connectorId, clusterConfig));\n }\n }\n return result;\n}\n\nfunction safeGetOptionalString(config: Config, key: string): string {\n try {\n return config.getOptionalString(key) ?? '';\n } catch {\n // Backstage ConfigReader throws TypeError for empty-string values\n // (e.g. from env var substitution like ${VAR:-}).\n return '';\n }\n}\n\nexport function readBridgeConfig(id: string, config: Config): BridgeConfig {\n return {\n id,\n name: safeGetOptionalString(config, 'name'),\n kubeflowModelCatalogUrl: safeGetOptionalString(\n config,\n 'kubeflow-model-catalog-url',\n ),\n defaultOwner: safeGetOptionalString(config, 'default-owner'),\n defaultLifecycle: safeGetOptionalString(config, 'default-lifecycle'),\n };\n}\n\nexport class ModeCatalogBridgeTechdocUrlReader implements UrlReaderService {\n private readonly workDir: string;\n private readonly logger: LoggerService;\n private readonly bridgeConfigs: BridgeConfig[];\n\n // TODO we are limited in which core services can be passed into the ReaderFactory, so as a work around we\n // define these globals that are set in the plugin init method; if we merge this plugin with the\n // kserve-kubeflow-connector plugins, and bypass the need for plugin to plugin REST calls, we can\n // bypass the need for the auth service.\n static auth: AuthService;\n\n static factory: ReaderFactory = ({ config, logger }) => {\n const reader = new ModeCatalogBridgeTechdocUrlReader(config, logger);\n return [{ reader, predicate: reader.bridgePredicate }];\n };\n\n constructor(config: Config, logger: LoggerService) {\n this.logger = logger.child({\n source: 'ModeCatalogBridgeTechdocUrlReader\"',\n });\n this.bridgeConfigs = readBridgeConfigs(config);\n this.workDir = os.tmpdir();\n\n const bkend = config.getOptionalString('backend.workingDirectory');\n if (bkend) {\n this.workDir = bkend;\n }\n }\n\n private matchingBridgeConfig(url: URL): BridgeConfig | undefined {\n for (let index = 0; index < this.bridgeConfigs.length; index++) {\n const bc = this.bridgeConfigs[index];\n if (url.pathname.includes('modelcard') && url.pathname.includes(bc.id)) {\n return bc;\n }\n }\n return undefined;\n }\n\n bridgePredicate = (url: URL): boolean => {\n return this.matchingBridgeConfig(url) !== undefined;\n };\n\n async readUrl(\n url: string,\n options?: UrlReaderServiceReadUrlOptions,\n ): Promise<UrlReaderServiceReadUrlResponse> {\n this.logger.info(`ModelCatalogBridgeTechdocUrlReader.readUrl of ${url}`);\n const parsedUrl = new URL(url);\n const bc = this.matchingBridgeConfig(parsedUrl);\n if (!bc) {\n throw new Error(`No matching bridge config for ${url}`);\n }\n let response: Response;\n try {\n const token =\n await ModeCatalogBridgeTechdocUrlReader.auth.getPluginRequestToken({\n onBehalfOf:\n await ModeCatalogBridgeTechdocUrlReader.auth.getOwnServiceCredentials(),\n targetPluginId: bc.id,\n });\n const tok = token.token;\n this.logger.info(`Using service-to-service token for ${url}`);\n response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${tok}`,\n },\n signal: options?.signal as any,\n });\n } catch (e) {\n throw new Error(`Unable to read ${url}, ${e}`);\n }\n\n if (response.ok) {\n return ReadUrlResponseFactory.fromResponse(response);\n }\n\n const message = `could not read ${url}, ${response.status} ${response.statusText}`;\n if (response.status === 404) {\n throw new NotFoundError(message);\n }\n if (response.status === 304) {\n throw new NotModifiedError();\n }\n throw new Error(message);\n }\n\n async search(\n url: string,\n options?: UrlReaderServiceSearchOptions,\n ): Promise<UrlReaderServiceSearchResponse> {\n this.logger.info(\n `ModelCatalogBridgeTechdocUrlReader.search of ${url} with options ${JSON.stringify(\n options,\n )}`,\n );\n\n throw new Error(\n 'ModeCatalogBridgeTechdocUrlReader does not implement search',\n );\n // const { pathname } = new URL(url);\n\n // if (pathname.match(/[*?]/)) {\n // throw new Error('Unsupported search pattern URL');\n // }\n\n // try {\n // const data = await this.readUrl(url, options);\n\n // return {\n // files: [\n // {\n // url: url,\n // content: data.buffer,\n // lastModifiedAt: data.lastModifiedAt,\n // },\n // ],\n // etag: data.etag ?? '',\n // };\n // } catch (error) {\n // assertError(error);\n // if (error.name === 'NotFoundError') {\n // return {\n // files: [],\n // etag: '',\n // };\n // }\n // throw error;\n // }\n }\n\n async readTree(\n url: string,\n options?: UrlReaderServiceReadTreeOptions,\n ): Promise<UrlReaderServiceReadTreeResponse> {\n this.logger.info(`ModelCatalogBridgeTechdocUrlReader.readTree of ${url}`);\n const data = await this.readUrl(url, options);\n\n const et = options?.etag;\n if (!et) {\n return await new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(\n this.workDir,\n '',\n data.buffer(),\n this.logger,\n );\n }\n return await new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(\n this.workDir,\n et,\n data.buffer(),\n this.logger,\n );\n }\n}\n\n/**\n * catalogTechdocUrlReaderPlugin define the factory for the ModelCatalogBridgeUrlReader service\n *\n * @public\n */\nexport const catalogTechdocUrlReaderPlugin = createServiceFactory({\n service: urlReaderFactoriesServiceRef,\n deps: {\n logger: coreServices.logger,\n auth: coreServices.auth,\n },\n async factory({ logger, auth }) {\n logger\n .child({ source: 'catalogTechdocUrlReaderPlugin\"' })\n .info('Registering the model catalog bridge URL reader ');\n\n ModeCatalogBridgeTechdocUrlReader.auth = auth;\n\n return ModeCatalogBridgeTechdocUrlReader.factory;\n },\n});\n"],"names":["fs","platformPath","os","ReadUrlResponseFactory","NotFoundError","NotModifiedError","createServiceFactory","urlReaderFactoriesServiceRef","coreServices"],"mappings":"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"}
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 */\n\nimport {\n ReaderFactory,\n ReadUrlResponseFactory,\n urlReaderFactoriesServiceRef,\n} from '@backstage/backend-defaults/urlReader';\nimport {\n coreServices,\n createServiceFactory,\n UrlReaderService,\n UrlReaderServiceReadUrlOptions,\n UrlReaderServiceSearchOptions,\n UrlReaderServiceSearchResponse,\n UrlReaderServiceReadUrlResponse,\n UrlReaderServiceReadTreeOptions,\n UrlReaderServiceReadTreeResponse,\n UrlReaderServiceReadTreeResponseFile,\n UrlReaderServiceReadTreeResponseDirOptions,\n LoggerService,\n AuthService,\n} from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { NotFoundError, NotModifiedError } from '@backstage/errors';\nimport { Readable } from 'stream';\nimport fs from 'fs';\nimport platformPath from 'path';\nimport os from 'os';\n\nexport class ModelCatalogBridgeUrlReaderServiceReadTreeResponse\n implements UrlReaderServiceReadTreeResponse\n{\n private readonly siteName: string;\n\n constructor(\n private readonly workDir: string,\n public readonly etag: string,\n private readonly buffer: Promise<Buffer>,\n private readonly logger: LoggerService,\n siteName?: string,\n ) {\n this.siteName = siteName ?? 'Model Card';\n this.logger = logger.child({\n source: 'ModelCatalogBridgeUrlReaderServiceReadTreeResponse\"',\n });\n }\n async files(): Promise<UrlReaderServiceReadTreeResponseFile[]> {\n throw new Error(\n 'ModelCatalogBridgeUrlReaderServiceReadTreeResponse does not implement files',\n );\n }\n async archive(): Promise<Readable> {\n throw new Error(\n 'ModelCatalogBridgeUrlReaderServiceReadTreeResponse does not implement archive',\n );\n }\n\n async dir(\n options?: UrlReaderServiceReadTreeResponseDirOptions,\n ): Promise<string> {\n const dir =\n options?.targetDir ??\n (await fs.promises.mkdtemp(\n platformPath.join(this.workDir, 'backstage-'),\n ));\n\n // Write a minimal mkdocs.yml that the TechDocs builder (mkdocs)\n // requires at the project root.\n const mkdocsPath = platformPath.join(dir, 'mkdocs.yml');\n const escapedSiteName = this.siteName.replace(/'/g, \"''\");\n const mkdocsContent = `site_name: '${escapedSiteName}'\\nnav:\\n - Home: index.md\\n`;\n try {\n await fs.promises.writeFile(mkdocsPath, mkdocsContent);\n } catch (error) {\n this.logger.error(`Error writing mkdocs.yml to ${mkdocsPath}:`, error);\n }\n\n const subDir = platformPath.join(dir, 'docs');\n const filePath = platformPath.join(subDir, 'index.md');\n try {\n fs.mkdirSync(subDir);\n const buf = await this.buffer;\n await fs.promises.writeFile(filePath, buf);\n } catch (error) {\n this.logger.error(`Error writing to file ${filePath}:`, error);\n }\n\n return dir;\n }\n}\n\nexport type BridgeConfig = {\n id: string;\n name: string;\n kubeflowModelCatalogUrl: string;\n defaultOwner: string;\n defaultLifecycle: string;\n};\n\nexport function readBridgeConfigs(config: Config): BridgeConfig[] {\n const configs = config.getOptionalConfig('catalog.providers.modelCatalog');\n if (!configs) {\n return [];\n }\n const result: BridgeConfig[] = [];\n for (const connectorId of configs.keys()) {\n const connectorConfig = configs.getConfig(connectorId);\n for (const clusterKey of connectorConfig.keys()) {\n const clusterConfig = connectorConfig.getOptionalConfig(clusterKey);\n if (!clusterConfig) {\n continue;\n }\n if (clusterConfig.has('frequency') || clusterConfig.has('timeout')) {\n continue;\n }\n result.push(readBridgeConfig(connectorId, clusterConfig));\n }\n }\n return result;\n}\n\nfunction safeGetOptionalString(config: Config, key: string): string {\n try {\n return config.getOptionalString(key) ?? '';\n } catch {\n // Backstage ConfigReader throws TypeError for empty-string values\n // (e.g. from env var substitution like ${VAR:-}).\n return '';\n }\n}\n\nexport function readBridgeConfig(id: string, config: Config): BridgeConfig {\n return {\n id,\n name: safeGetOptionalString(config, 'name'),\n kubeflowModelCatalogUrl: safeGetOptionalString(\n config,\n 'kubeflow-model-catalog-url',\n ),\n defaultOwner: safeGetOptionalString(config, 'default-owner'),\n defaultLifecycle: safeGetOptionalString(config, 'default-lifecycle'),\n };\n}\n\n// ─── OCI skill TechDocs source configuration ────────────────────────\n\n/** Digest format: sha256:<64 lowercase hex digits>. */\nconst DIGEST_RE = /^sha256:[0-9a-f]{64}$/;\n\nexport type SkillSourceConfig = {\n /** Backstage plugin ID of the connector (e.g. 'skill-image-connector'). */\n connectorPluginId: string;\n /** Configured source identifier. */\n sourceId: string;\n};\n\n/**\n * Read OCI skill source configurations from\n * `catalog.providers.skillsCatalogProvider.*.sources[]`.\n *\n * Only `oci` source types are supported;\n * `npx` sources are skipped.\n *\n * Source IDs that are blank, `.`, or `..` are rejected because URL\n * parsing normalizes literal and encoded dot segments, making them\n * unreachable as stable path components.\n */\nexport function readSkillSourceConfigs(config: Config): SkillSourceConfig[] {\n const providerConfigs = config.getOptionalConfig(\n 'catalog.providers.skillsCatalogProvider',\n );\n if (!providerConfigs) {\n return [];\n }\n\n const result: SkillSourceConfig[] = [];\n const seen = new Set<string>();\n\n for (const providerId of providerConfigs.keys()) {\n const providerConfig = providerConfigs.getConfig(providerId);\n const sourcesConfig = providerConfig.getOptionalConfigArray('sources');\n if (!sourcesConfig) {\n continue;\n }\n\n for (const sourceConfig of sourcesConfig) {\n const sourceType = safeGetOptionalString(sourceConfig, 'sourceType');\n if (sourceType !== 'oci') {\n continue; // npx and other source types are not supported\n }\n\n const connectorPluginId = safeGetOptionalString(\n sourceConfig,\n 'connectorPluginId',\n );\n if (!connectorPluginId) {\n continue;\n }\n\n const sourceId = safeGetOptionalString(sourceConfig, 'sourceId');\n if (\n !sourceId.trim() ||\n sourceId.trim() === '.' ||\n sourceId.trim() === '..'\n ) {\n continue; // Reject blank, dot, and dot-dot source IDs\n }\n\n // Deduplicate across provider instances\n const key = `${connectorPluginId}:${sourceId}`;\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n\n result.push({ connectorPluginId, sourceId });\n }\n }\n\n return result;\n}\n\nexport class ModeCatalogBridgeTechdocUrlReader implements UrlReaderService {\n private readonly workDir: string;\n private readonly logger: LoggerService;\n private readonly bridgeConfigs: BridgeConfig[];\n private readonly skillSourceConfigs: Array<\n SkillSourceConfig & { connectorBase: URL }\n >;\n\n // TODO we are limited in which core services can be passed into the ReaderFactory, so as a work around we\n // define these globals that are set in the plugin init method; if we merge this plugin with the\n // kserve-kubeflow-connector plugins, and bypass the need for plugin to plugin REST calls, we can\n // bypass the need for the auth service.\n static auth: AuthService;\n\n /**\n * Legacy factory for model-card URLs only; skill documentation fails closed\n * without discovery-resolved connector bases. Register\n * `catalogTechdocUrlReaderPlugin` for OCI skill documentation support.\n */\n static factory: ReaderFactory = ({ config, logger }) => {\n const reader = new ModeCatalogBridgeTechdocUrlReader(config, logger);\n return [{ reader, predicate: reader.bridgePredicate }];\n };\n\n constructor(\n config: Config,\n logger: LoggerService,\n trustedConnectorBases: ReadonlyMap<string, string> = new Map(),\n ) {\n this.logger = logger.child({\n source: 'ModeCatalogBridgeTechdocUrlReader\"',\n });\n this.bridgeConfigs = readBridgeConfigs(config);\n // Bases come only from service discovery during factory initialization.\n // Direct construction without them fails closed for skill documentation.\n this.skillSourceConfigs = readSkillSourceConfigs(config).flatMap(source => {\n const base = trustedConnectorBases.get(source.connectorPluginId);\n if (!base) return [];\n const connectorBase = new URL(base);\n if (\n !['http:', 'https:'].includes(connectorBase.protocol) ||\n connectorBase.username ||\n connectorBase.password ||\n connectorBase.search ||\n connectorBase.hash\n )\n throw new Error(\n `Invalid discovered connector base for ${source.connectorPluginId}`,\n );\n return [{ ...source, connectorBase }];\n });\n this.workDir = os.tmpdir();\n\n const bkend = config.getOptionalString('backend.workingDirectory');\n if (bkend) {\n this.workDir = bkend;\n }\n }\n\n private matchingBridgeConfig(url: URL): BridgeConfig | undefined {\n // Skill documentation must never fall through to the legacy substring matcher.\n if (url.pathname.includes('/skilldocs/')) return undefined;\n for (let index = 0; index < this.bridgeConfigs.length; index++) {\n const bc = this.bridgeConfigs[index];\n if (url.pathname.includes('modelcard') && url.pathname.includes(bc.id)) {\n return bc;\n }\n }\n return undefined;\n }\n\n /**\n * Match a URL against configured OCI skill source destinations.\n *\n * Requires the discovered origin and complete base pathname, followed by\n * /skilldocs/<sourceId>/<digest>\n *\n * Rejects URLs with query strings, fragments, URL credentials,\n * extra path segments, or invalid digest format.\n */\n private matchingSkillSourceConfig(url: URL): SkillSourceConfig | undefined {\n // Reject URL credentials\n if (url.username || url.password) return undefined;\n // Reject query strings and fragments\n if (url.search || url.hash) return undefined;\n\n const pathname = url.pathname;\n\n for (const source of this.skillSourceConfigs) {\n if (url.origin !== source.connectorBase.origin) continue;\n const basePath = `${source.connectorBase.pathname.replace(\n /\\/+$/,\n '',\n )}/skilldocs/`;\n if (!pathname.startsWith(basePath)) continue;\n\n const remainder = pathname.substring(basePath.length);\n // remainder must be exactly <encodedSourceId>/<encodedDigest>\n const parts = remainder.split('/');\n if (parts.length !== 2 || parts[0] === '' || parts[1] === '') continue;\n\n let sourceId: string;\n let digest: string;\n try {\n sourceId = decodeURIComponent(parts[0]);\n digest = decodeURIComponent(parts[1]);\n } catch {\n // Reader predicates reject malformed input rather than breaking selection.\n continue;\n }\n\n // Validate source ID matches configuration\n if (sourceId !== source.sourceId) continue;\n\n // Validate digest format: sha256:<64 lowercase hex digits>\n if (!DIGEST_RE.test(digest)) continue;\n\n return source;\n }\n return undefined;\n }\n\n bridgePredicate = (url: URL): boolean => {\n return (\n this.matchingBridgeConfig(url) !== undefined ||\n this.matchingSkillSourceConfig(url) !== undefined\n );\n };\n\n async readUrl(\n url: string,\n options?: UrlReaderServiceReadUrlOptions,\n ): Promise<UrlReaderServiceReadUrlResponse> {\n this.logger.info(`ModelCatalogBridgeTechdocUrlReader.readUrl of ${url}`);\n const parsedUrl = new URL(url);\n\n // Try model-card match first (preserve existing behavior)\n const bc = this.matchingBridgeConfig(parsedUrl);\n if (bc) {\n return this.readModelCardUrl(url, bc, options);\n }\n\n // Try OCI skill docs match\n const sc = this.matchingSkillSourceConfig(parsedUrl);\n if (sc) {\n return this.readSkillDocsUrl(url, sc, options);\n }\n\n throw new Error(`No matching config for ${url}`);\n }\n\n private async readModelCardUrl(\n url: string,\n bc: BridgeConfig,\n options?: UrlReaderServiceReadUrlOptions,\n ): Promise<UrlReaderServiceReadUrlResponse> {\n let response: Response;\n try {\n const token =\n await ModeCatalogBridgeTechdocUrlReader.auth.getPluginRequestToken({\n onBehalfOf:\n await ModeCatalogBridgeTechdocUrlReader.auth.getOwnServiceCredentials(),\n targetPluginId: bc.id,\n });\n this.logger.info(`Using service-to-service token for ${url}`);\n response = await fetch(url, {\n method: 'GET',\n redirect: 'error',\n headers: {\n Authorization: `Bearer ${token.token}`,\n },\n signal: options?.signal as any,\n });\n } catch (e) {\n throw new Error(`Unable to read ${url}, ${e}`);\n }\n\n if (response.ok) {\n return ReadUrlResponseFactory.fromResponse(response);\n }\n\n const message = `could not read ${url}, ${response.status} ${response.statusText}`;\n if (response.status === 404) {\n throw new NotFoundError(message);\n }\n if (response.status === 304) {\n throw new NotModifiedError();\n }\n throw new Error(message);\n }\n\n /**\n * Fetch OCI skill documentation from the connector.\n *\n * - Authenticates with `auth.getPluginRequestToken` targeting the\n * connector plugin's registered pluginId.\n * - Forwards the caller's ETag as `If-None-Match` for conditional requests.\n * - Rejects redirects without forwarding service tokens.\n * - Propagates 404 as NotFoundError, 304 as NotModifiedError.\n */\n private async readSkillDocsUrl(\n url: string,\n source: SkillSourceConfig,\n options?: UrlReaderServiceReadUrlOptions,\n ): Promise<UrlReaderServiceReadUrlResponse> {\n const headers: Record<string, string> = {};\n\n let response: Response;\n try {\n const token =\n await ModeCatalogBridgeTechdocUrlReader.auth.getPluginRequestToken({\n onBehalfOf:\n await ModeCatalogBridgeTechdocUrlReader.auth.getOwnServiceCredentials(),\n targetPluginId: source.connectorPluginId,\n });\n headers.Authorization = `Bearer ${token.token}`;\n\n // Forward caller's ETag for conditional requests (D8)\n if (options?.etag) {\n headers['If-None-Match'] = options.etag;\n }\n\n this.logger.info(\n `Fetching OCI skill documentation from ${url} (connector: ${source.connectorPluginId})`,\n );\n response = await fetch(url, {\n method: 'GET',\n headers,\n redirect: 'error',\n signal: options?.signal as any,\n });\n } catch (e) {\n throw new Error(`Unable to read ${url}, ${e}`);\n }\n\n if (response.ok) {\n if (!response.headers.get('etag')?.trim()) {\n await response.body?.cancel();\n throw new Error(\n 'Skill documentation response is missing a nonblank ETag',\n );\n }\n return ReadUrlResponseFactory.fromResponse(response);\n }\n\n const message = `could not read ${url}, ${response.status} ${response.statusText}`;\n if (response.status === 404) {\n throw new NotFoundError(message);\n }\n if (response.status === 304) {\n throw new NotModifiedError();\n }\n throw new Error(message);\n }\n\n async search(\n url: string,\n options?: UrlReaderServiceSearchOptions,\n ): Promise<UrlReaderServiceSearchResponse> {\n this.logger.info(\n `ModelCatalogBridgeTechdocUrlReader.search of ${url} with options ${JSON.stringify(\n options,\n )}`,\n );\n\n throw new Error(\n 'ModeCatalogBridgeTechdocUrlReader does not implement search',\n );\n // const { pathname } = new URL(url);\n\n // if (pathname.match(/[*?]/)) {\n // throw new Error('Unsupported search pattern URL');\n // }\n\n // try {\n // const data = await this.readUrl(url, options);\n\n // return {\n // files: [\n // {\n // url: url,\n // content: data.buffer,\n // lastModifiedAt: data.lastModifiedAt,\n // },\n // ],\n // etag: data.etag ?? '',\n // };\n // } catch (error) {\n // assertError(error);\n // if (error.name === 'NotFoundError') {\n // return {\n // files: [],\n // etag: '',\n // };\n // }\n // throw error;\n // }\n }\n\n async readTree(\n url: string,\n options?: UrlReaderServiceReadTreeOptions,\n ): Promise<UrlReaderServiceReadTreeResponse> {\n this.logger.info(`ModelCatalogBridgeTechdocUrlReader.readTree of ${url}`);\n const parsedUrl = new URL(url);\n\n const data = await this.readUrl(url, options);\n\n // Mirror readUrl's priority: bridge config first, then skill docs\n const isSkillDocs =\n this.matchingBridgeConfig(parsedUrl) === undefined &&\n this.matchingSkillSourceConfig(parsedUrl) !== undefined;\n\n if (isSkillDocs) {\n // Use the connector's response ETag for TechDocs caching (D8).\n if (!data.etag?.trim()) {\n throw new Error(\n 'Skill documentation response is missing a nonblank ETag',\n );\n }\n return new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(\n this.workDir,\n data.etag,\n data.buffer(),\n this.logger,\n 'Skill Documentation',\n );\n }\n\n // Preserve existing model-card behavior\n const et = options?.etag;\n if (!et) {\n return new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(\n this.workDir,\n '',\n data.buffer(),\n this.logger,\n );\n }\n return new ModelCatalogBridgeUrlReaderServiceReadTreeResponse(\n this.workDir,\n et,\n data.buffer(),\n this.logger,\n );\n }\n}\n\n/**\n * catalogTechdocUrlReaderPlugin define the factory for the ModelCatalogBridgeUrlReader service\n *\n * @public\n */\nexport const catalogTechdocUrlReaderPlugin = createServiceFactory({\n service: urlReaderFactoriesServiceRef,\n deps: {\n logger: coreServices.logger,\n auth: coreServices.auth,\n discovery: coreServices.discovery,\n config: coreServices.rootConfig,\n },\n async factory({ logger, auth, discovery, config }) {\n logger\n .child({ source: 'catalogTechdocUrlReaderPlugin\"' })\n .info('Registering the model catalog bridge URL reader ');\n\n ModeCatalogBridgeTechdocUrlReader.auth = auth;\n\n // Reader predicates are synchronous, so resolve trusted destinations before\n // publishing the factory. Do not infer a destination from annotation text.\n const trustedConnectorBases = new Map<string, string>();\n const connectorIds = new Set(\n readSkillSourceConfigs(config).map(source => source.connectorPluginId),\n );\n for (const connectorId of connectorIds) {\n trustedConnectorBases.set(\n connectorId,\n await discovery.getBaseUrl(connectorId),\n );\n }\n return ({ config: readerConfig, logger: readerLogger }) => {\n const reader = new ModeCatalogBridgeTechdocUrlReader(\n readerConfig,\n readerLogger,\n trustedConnectorBases,\n );\n return [{ reader, predicate: reader.bridgePredicate }];\n };\n },\n});\n"],"names":["fs","platformPath","os","ReadUrlResponseFactory","NotFoundError","NotModifiedError","createServiceFactory","urlReaderFactoriesServiceRef","coreServices"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@red-hat-developer-hub/backstage-plugin-catalog-techdoc-url-reader-backend",
3
- "version": "0.6.3",
3
+ "version": "0.8.0",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/index.cjs.js",
6
6
  "types": "dist/index.d.ts",