@ubiquity-os/plugin-sdk 3.8.4 → 3.9.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.
@@ -33,7 +33,8 @@ __export(configuration_exports, {
33
33
  CONFIG_DEV_FULL_PATH: () => CONFIG_DEV_FULL_PATH,
34
34
  CONFIG_ORG_REPO: () => CONFIG_ORG_REPO,
35
35
  CONFIG_PROD_FULL_PATH: () => CONFIG_PROD_FULL_PATH,
36
- ConfigurationHandler: () => ConfigurationHandler
36
+ ConfigurationHandler: () => ConfigurationHandler,
37
+ isGithubPlugin: () => isGithubPlugin
37
38
  });
38
39
  module.exports = __toCommonJS(configuration_exports);
39
40
  var import_value = require("@sinclair/typebox/value");
@@ -44,7 +45,11 @@ var import_node_buffer = require("node:buffer");
44
45
  var import_webhooks = require("@octokit/webhooks");
45
46
  var import_typebox = require("@sinclair/typebox");
46
47
  var pluginNameRegex = new RegExp("^([0-9a-zA-Z-._]+)\\/([0-9a-zA-Z-._]+)(?::([0-9a-zA-Z-._]+))?(?:@([0-9a-zA-Z-._]+(?:\\/[0-9a-zA-Z-._]+)*))?$");
48
+ var urlRegex = /^https?:\/\/\S+$/;
47
49
  function parsePluginIdentifier(value) {
50
+ if (urlRegex.test(value)) {
51
+ return value;
52
+ }
48
53
  const matches = pluginNameRegex.exec(value);
49
54
  if (!matches) {
50
55
  throw new Error(`Invalid plugin name: ${value}`);
@@ -78,6 +83,7 @@ var pluginSettingsSchema = import_typebox.Type.Union(
78
83
  );
79
84
  var configSchema = import_typebox.Type.Object(
80
85
  {
86
+ imports: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String(), { default: [] })),
81
87
  plugins: import_typebox.Type.Record(import_typebox.Type.String(), pluginSettingsSchema, { default: {} })
82
88
  },
83
89
  {
@@ -85,6 +91,15 @@ var configSchema = import_typebox.Type.Object(
85
91
  }
86
92
  );
87
93
 
94
+ // src/helpers/urls.ts
95
+ function normalizeBaseUrl(baseUrl) {
96
+ let normalized = baseUrl.trim();
97
+ while (normalized.endsWith("/")) {
98
+ normalized = normalized.slice(0, -1);
99
+ }
100
+ return normalized;
101
+ }
102
+
88
103
  // src/types/manifest.ts
89
104
  var import_typebox2 = require("@sinclair/typebox");
90
105
  var import_webhooks2 = require("@octokit/webhooks");
@@ -118,14 +133,137 @@ var manifestSchema = import_typebox2.Type.Object({
118
133
  var CONFIG_PROD_FULL_PATH = ".github/.ubiquity-os.config.yml";
119
134
  var CONFIG_DEV_FULL_PATH = ".github/.ubiquity-os.config.dev.yml";
120
135
  var CONFIG_ORG_REPO = ".ubiquity-os";
136
+ var EMPTY_STRING = "";
137
+ var ENVIRONMENT_TO_CONFIG_SUFFIX = {
138
+ development: "dev"
139
+ };
140
+ var VALID_CONFIG_SUFFIX = /^[a-z0-9][a-z0-9_-]*$/i;
141
+ var MAX_IMPORT_DEPTH = 6;
142
+ function normalizeEnvironmentName(environment) {
143
+ return String(environment ?? EMPTY_STRING).trim().toLowerCase();
144
+ }
145
+ function getConfigPathCandidatesForEnvironment(environment) {
146
+ const normalized = normalizeEnvironmentName(environment);
147
+ if (!normalized) {
148
+ return [CONFIG_PROD_FULL_PATH, CONFIG_DEV_FULL_PATH];
149
+ }
150
+ if (normalized === "production" || normalized === "prod") {
151
+ return [CONFIG_PROD_FULL_PATH];
152
+ }
153
+ const suffix = ENVIRONMENT_TO_CONFIG_SUFFIX[normalized] ?? normalized;
154
+ if (suffix === "dev") {
155
+ return [CONFIG_DEV_FULL_PATH];
156
+ }
157
+ if (!VALID_CONFIG_SUFFIX.test(suffix)) {
158
+ return [CONFIG_DEV_FULL_PATH];
159
+ }
160
+ return [`.github/.ubiquity-os.config.${suffix}.yml`, CONFIG_PROD_FULL_PATH];
161
+ }
162
+ function normalizeImportKey(location) {
163
+ return `${location.owner}`.trim().toLowerCase() + "/" + `${location.repo}`.trim().toLowerCase();
164
+ }
165
+ function isHttpUrl(value) {
166
+ const trimmed = value.trim();
167
+ return trimmed.startsWith("http://") || trimmed.startsWith("https://");
168
+ }
169
+ function resolveManifestUrl(pluginUrl) {
170
+ try {
171
+ const parsed = new URL(pluginUrl.trim());
172
+ let pathname = parsed.pathname;
173
+ while (pathname.endsWith("/") && pathname.length > 1) {
174
+ pathname = pathname.slice(0, -1);
175
+ }
176
+ if (pathname.endsWith(".json")) {
177
+ parsed.search = EMPTY_STRING;
178
+ parsed.hash = EMPTY_STRING;
179
+ return parsed.toString();
180
+ }
181
+ parsed.pathname = `${pathname}/manifest.json`;
182
+ parsed.search = EMPTY_STRING;
183
+ parsed.hash = EMPTY_STRING;
184
+ return parsed.toString();
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+ function parseImportSpec(value) {
190
+ const trimmed = value.trim();
191
+ if (!trimmed) return null;
192
+ const parts = trimmed.split("/");
193
+ if (parts.length !== 2) return null;
194
+ const [owner, repo] = parts;
195
+ if (!owner || !repo) return null;
196
+ return { owner, repo };
197
+ }
198
+ function readImports(logger, value, source) {
199
+ if (!value) return [];
200
+ if (!Array.isArray(value)) {
201
+ logger.warn("Invalid imports; expected a list of strings.", { source });
202
+ return [];
203
+ }
204
+ const seen = /* @__PURE__ */ new Set();
205
+ const imports = [];
206
+ for (const entry of value) {
207
+ if (typeof entry !== "string") {
208
+ logger.warn("Ignoring invalid import entry; expected string.", { source, entry });
209
+ continue;
210
+ }
211
+ const parsed = parseImportSpec(entry);
212
+ if (!parsed) {
213
+ logger.warn("Ignoring invalid import entry; expected owner/repo.", { source, entry });
214
+ continue;
215
+ }
216
+ const key = normalizeImportKey(parsed);
217
+ if (seen.has(key)) continue;
218
+ seen.add(key);
219
+ imports.push(parsed);
220
+ }
221
+ return imports;
222
+ }
223
+ function stripImports(config) {
224
+ if (!config || typeof config !== "object") return config;
225
+ const rest = { ...config };
226
+ delete rest.imports;
227
+ return rest;
228
+ }
229
+ function mergeImportedConfigs(imported, base) {
230
+ if (!imported.length) {
231
+ return base;
232
+ }
233
+ let merged = imported[0];
234
+ for (let i = 1; i < imported.length; i++) {
235
+ merged = {
236
+ ...merged,
237
+ ...imported[i],
238
+ plugins: { ...merged.plugins, ...imported[i].plugins }
239
+ };
240
+ }
241
+ return base ? {
242
+ ...merged,
243
+ ...base,
244
+ plugins: { ...merged.plugins, ...base.plugins }
245
+ } : merged;
246
+ }
247
+ function logOk(logger, message, metadata) {
248
+ if (logger.ok) {
249
+ logger.ok(message, metadata);
250
+ } else {
251
+ logger.info(message, metadata);
252
+ }
253
+ }
254
+ function isGithubPlugin(plugin) {
255
+ return typeof plugin !== "string";
256
+ }
121
257
  var ConfigurationHandler = class {
122
- constructor(_logger, _octokit, _environment = null) {
258
+ constructor(_logger, _octokit, _environment = null, options) {
123
259
  this._logger = _logger;
124
260
  this._octokit = _octokit;
125
261
  this._environment = _environment;
262
+ this._octokitFactory = options?.octokitFactory;
126
263
  }
127
264
  _manifestCache = {};
128
265
  _manifestPromiseCache = {};
266
+ _octokitFactory;
129
267
  /**
130
268
  * Retrieves the configuration for the current plugin based on its manifest.
131
269
  * @param manifest - The plugin manifest containing the `short_name` identifier
@@ -134,9 +272,15 @@ var ConfigurationHandler = class {
134
272
  **/
135
273
  async getSelfConfiguration(manifest, location) {
136
274
  const cfg = await this.getConfiguration(location);
137
- const name = manifest.short_name.split("@")[0];
138
- const selfConfig = Object.keys(cfg.plugins).find((key) => new RegExp(`^${name}(?:$|@.+)`).exec(key.replace(/:[^@]+/, "")));
139
- return selfConfig && cfg.plugins[selfConfig] ? cfg.plugins[selfConfig]["with"] : null;
275
+ let selfConfig;
276
+ if (manifest.homepage_url) {
277
+ const name = manifest.homepage_url;
278
+ selfConfig = Object.keys(cfg.plugins).find((key) => normalizeBaseUrl(key) === normalizeBaseUrl(name));
279
+ } else {
280
+ const name = manifest.short_name.split("@")[0];
281
+ selfConfig = Object.keys(cfg.plugins).find((key) => new RegExp(`^${name}(?:$|@.+)`).exec(key.replace(/:[^@]+/, "")));
282
+ }
283
+ return selfConfig && cfg.plugins[selfConfig] ? cfg.plugins[selfConfig]?.["with"] : null;
140
284
  }
141
285
  /**
142
286
  * Retrieves and merges configuration from organization and repository levels.
@@ -144,14 +288,22 @@ var ConfigurationHandler = class {
144
288
  * @returns The merged plugin configuration with resolved plugin settings.
145
289
  */
146
290
  async getConfiguration(location) {
147
- const defaultConfiguration = import_value.Value.Decode(configSchema, import_value.Value.Default(configSchema, {}));
291
+ const defaultConfiguration = stripImports(import_value.Value.Decode(configSchema, import_value.Value.Default(configSchema, {})));
148
292
  if (!location) {
149
- this._logger.debug("No location was provided, using the default configuration");
293
+ this._logger.info("No location was provided, using the default configuration");
150
294
  return defaultConfiguration;
151
295
  }
296
+ const mergedConfiguration = await this._getMergedConfiguration(location, defaultConfiguration);
297
+ const resolvedPlugins = await this._resolvePlugins(mergedConfiguration, location);
298
+ return {
299
+ ...mergedConfiguration,
300
+ plugins: resolvedPlugins
301
+ };
302
+ }
303
+ async _getMergedConfiguration(location, defaultConfiguration) {
152
304
  const { owner, repo } = location;
153
305
  let mergedConfiguration = defaultConfiguration;
154
- this._logger.debug("Fetching configurations from the organization and repository", {
306
+ this._logger.info("Fetching configurations from the organization and repository", {
155
307
  orgRepo: `${owner}/${CONFIG_ORG_REPO}`,
156
308
  repo: `${owner}/${repo}`
157
309
  });
@@ -163,39 +315,50 @@ var ConfigurationHandler = class {
163
315
  if (repoConfig.config) {
164
316
  mergedConfiguration = this.mergeConfigurations(mergedConfiguration, repoConfig.config);
165
317
  }
318
+ return mergedConfiguration;
319
+ }
320
+ async _resolvePlugins(mergedConfiguration, location) {
166
321
  const resolvedPlugins = {};
167
- this._logger.debug("Found plugins enabled", { repo: `${owner}/${repo}`, plugins: Object.keys(mergedConfiguration.plugins).length });
322
+ logOk(this._logger, "Found plugins enabled", { repo: `${location.owner}/${location.repo}`, plugins: Object.keys(mergedConfiguration.plugins).length });
168
323
  for (const [pluginKey, pluginSettings] of Object.entries(mergedConfiguration.plugins)) {
324
+ const resolved = await this._resolvePluginSettings(pluginKey, pluginSettings);
325
+ if (!resolved) continue;
326
+ resolvedPlugins[pluginKey] = resolved;
327
+ }
328
+ return resolvedPlugins;
329
+ }
330
+ async _resolvePluginSettings(pluginKey, pluginSettings) {
331
+ const isUrlPlugin = isHttpUrl(pluginKey);
332
+ let manifest = null;
333
+ if (!isUrlPlugin) {
169
334
  let pluginIdentifier;
170
335
  try {
171
336
  pluginIdentifier = parsePluginIdentifier(pluginKey);
172
337
  } catch (error) {
173
- this._logger.error("Invalid plugin identifier; skipping", { plugin: pluginKey, err: error });
174
- continue;
338
+ this._logger.warn("Invalid plugin identifier; skipping", { plugin: pluginKey, err: error });
339
+ return null;
175
340
  }
176
- const manifest = await this.getManifest(pluginIdentifier);
177
- let runsOn = pluginSettings?.runsOn ?? [];
178
- let shouldSkipBotEvents = pluginSettings?.skipBotEvents;
179
- if (manifest) {
180
- if (!runsOn.length) {
181
- runsOn = manifest["ubiquity:listeners"] ?? [];
182
- }
183
- if (shouldSkipBotEvents === void 0) {
184
- shouldSkipBotEvents = manifest.skipBotEvents ?? true;
185
- }
186
- } else {
187
- shouldSkipBotEvents = true;
341
+ manifest = await this.getManifest(pluginIdentifier);
342
+ } else {
343
+ manifest = await this._fetchUrlManifest(pluginKey);
344
+ }
345
+ let runsOn = pluginSettings?.runsOn ?? [];
346
+ let shouldSkipBotEvents = pluginSettings?.skipBotEvents;
347
+ if (manifest) {
348
+ if (!runsOn.length) {
349
+ runsOn = manifest["ubiquity:listeners"] ?? [];
188
350
  }
189
- resolvedPlugins[pluginKey] = {
190
- ...pluginSettings,
191
- with: pluginSettings?.with ?? {},
192
- runsOn,
193
- skipBotEvents: shouldSkipBotEvents
194
- };
351
+ if (shouldSkipBotEvents === void 0) {
352
+ shouldSkipBotEvents = manifest.skipBotEvents ?? true;
353
+ }
354
+ } else {
355
+ shouldSkipBotEvents = true;
195
356
  }
196
357
  return {
197
- ...mergedConfiguration,
198
- plugins: resolvedPlugins
358
+ ...pluginSettings,
359
+ with: pluginSettings?.with ?? {},
360
+ runsOn,
361
+ skipBotEvents: shouldSkipBotEvents
199
362
  };
200
363
  }
201
364
  /**
@@ -205,87 +368,201 @@ var ConfigurationHandler = class {
205
368
  * @param repository The repository name
206
369
  */
207
370
  async getConfigurationFromRepo(owner, repository) {
371
+ const location = { owner, repo: repository };
372
+ const state = this._createImportState();
373
+ const octokit = await this._getOctokitForLocation(location, state);
374
+ if (!octokit) {
375
+ this._logger.warn("No Octokit available for configuration load", { owner, repository });
376
+ return { config: null, errors: null, rawData: null };
377
+ }
378
+ const { config, imports, errors, rawData } = await this._loadConfigSource(location, octokit);
379
+ if (!rawData) {
380
+ return { config: null, errors: null, rawData: null };
381
+ }
382
+ if (errors && errors.length) {
383
+ this._logger.warn("YAML could not be decoded", { owner, repository, errors });
384
+ return { config: null, errors, rawData };
385
+ }
386
+ if (!config) {
387
+ this._logger.warn("YAML could not be decoded", { owner, repository });
388
+ return { config: null, errors, rawData };
389
+ }
390
+ const importedConfigs = [];
391
+ for (const next of imports) {
392
+ const resolved = await this._resolveImportedConfiguration(next, state, 1);
393
+ if (resolved) importedConfigs.push(resolved);
394
+ }
395
+ const mergedConfig = mergeImportedConfigs(importedConfigs, config);
396
+ if (!mergedConfig) {
397
+ return { config: null, errors: null, rawData };
398
+ }
399
+ const decoded = this._decodeConfiguration(location, mergedConfig);
400
+ return { config: decoded.config, errors: decoded.errors, rawData };
401
+ }
402
+ _createImportState() {
403
+ return {
404
+ cache: /* @__PURE__ */ new Map(),
405
+ inFlight: /* @__PURE__ */ new Set(),
406
+ octokitByLocation: /* @__PURE__ */ new Map()
407
+ };
408
+ }
409
+ async _getOctokitForLocation(location, state) {
410
+ const key = normalizeImportKey(location);
411
+ if (state.octokitByLocation.has(key)) {
412
+ return state.octokitByLocation.get(key) ?? null;
413
+ }
414
+ if (this._octokitFactory) {
415
+ const resolved = await this._octokitFactory(location);
416
+ if (resolved) {
417
+ state.octokitByLocation.set(key, resolved);
418
+ return resolved;
419
+ }
420
+ }
421
+ state.octokitByLocation.set(key, this._octokit);
422
+ return this._octokit;
423
+ }
424
+ async _loadConfigSource(location, octokit) {
208
425
  const rawData = await this._download({
209
- repository,
210
- owner
426
+ repository: location.repo,
427
+ owner: location.owner,
428
+ octokit
211
429
  });
212
- this._logger.debug("Downloaded configuration file", { owner, repository });
213
430
  if (!rawData) {
214
- this._logger.debug("No raw configuration data", { owner, repository });
215
- return { config: null, errors: null, rawData: null };
431
+ this._logger.warn("No raw configuration data", { owner: location.owner, repository: location.repo });
432
+ return { config: null, imports: [], errors: null, rawData: null };
433
+ }
434
+ logOk(this._logger, "Downloaded configuration file", { owner: location.owner, repository: location.repo });
435
+ const { yaml, errors } = this.parseYaml(rawData);
436
+ const imports = readImports(this._logger, yaml?.imports, location);
437
+ if (yaml && typeof yaml === "object" && !Array.isArray(yaml) && "imports" in yaml) {
438
+ delete yaml.imports;
216
439
  }
217
- const { yaml, errors } = this._parseYaml(rawData);
218
440
  const targetRepoConfiguration = yaml;
219
- this._logger.debug("Decoding configuration", { owner, repository });
220
- if (targetRepoConfiguration) {
221
- try {
222
- const configSchemaWithDefaults = import_value.Value.Default(configSchema, targetRepoConfiguration);
223
- const errors2 = import_value.Value.Errors(configSchema, configSchemaWithDefaults);
224
- if (errors2.First()) {
225
- for (const error of errors2) {
226
- this._logger.warn("Configuration validation error", { err: error });
227
- }
441
+ return { config: targetRepoConfiguration, imports, errors, rawData };
442
+ }
443
+ _decodeConfiguration(location, config) {
444
+ this._logger.info("Decoding configuration", { owner: location.owner, repository: location.repo });
445
+ try {
446
+ const configSchemaWithDefaults = import_value.Value.Default(configSchema, config);
447
+ const errors = import_value.Value.Errors(configSchema, configSchemaWithDefaults);
448
+ if (errors.First()) {
449
+ for (const error of errors) {
450
+ this._logger.warn("Configuration validation error", { err: error });
228
451
  }
229
- const decodedConfig = import_value.Value.Decode(configSchema, configSchemaWithDefaults);
230
- return { config: decodedConfig, errors: errors2.First() ? errors2 : null, rawData };
231
- } catch (error) {
232
- this._logger.error("Error decoding configuration; Will ignore.", { err: error, owner, repository });
233
- return { config: null, errors: [error instanceof import_value.TransformDecodeCheckError ? error.error : error], rawData };
234
452
  }
453
+ const decodedConfig = import_value.Value.Decode(configSchema, configSchemaWithDefaults);
454
+ return { config: stripImports(decodedConfig), errors: errors.First() ? errors : null };
455
+ } catch (error) {
456
+ this._logger.warn("Error decoding configuration; Will ignore.", { err: error, owner: location.owner, repository: location.repo });
457
+ return { config: null, errors: [error instanceof import_value.TransformDecodeCheckError ? error.error : error] };
235
458
  }
236
- this._logger.error("YAML could not be decoded", { owner, repository, errors });
237
- return { config: null, errors, rawData };
238
459
  }
239
- async _download({ repository, owner }) {
240
- if (!repository || !owner) {
241
- this._logger.error("Repo or owner is not defined, cannot download the requested file");
460
+ async _resolveImportedConfiguration(location, state, depth) {
461
+ const key = normalizeImportKey(location);
462
+ if (state.cache.has(key)) {
463
+ return state.cache.get(key) ?? null;
464
+ }
465
+ if (state.inFlight.has(key)) {
466
+ this._logger.warn("Skipping import due to circular reference.", { location });
242
467
  return null;
243
468
  }
244
- let pathList;
245
- switch (this._environment) {
246
- case "development":
247
- pathList = [CONFIG_DEV_FULL_PATH];
248
- break;
249
- case "production":
250
- pathList = [CONFIG_PROD_FULL_PATH];
251
- break;
252
- default:
253
- pathList = [CONFIG_PROD_FULL_PATH, CONFIG_DEV_FULL_PATH];
469
+ if (depth > MAX_IMPORT_DEPTH) {
470
+ this._logger.warn("Skipping import; maximum depth exceeded.", { location, depth });
471
+ return null;
254
472
  }
255
- for (const filePath of pathList) {
256
- try {
257
- this._logger.debug("Attempting to fetch configuration", { owner, repository, filePath });
258
- const { data, headers } = await this._octokit.rest.repos.getContent({
259
- owner,
260
- repo: repository,
261
- path: filePath,
262
- mediaType: { format: "raw" }
263
- });
264
- this._logger.debug("Configuration file found", { owner, repository, filePath, rateLimitRemaining: headers?.["x-ratelimit-remaining"], data });
265
- return data;
266
- } catch (err) {
267
- if (err && typeof err === "object" && "status" in err && err.status === 404) {
268
- this._logger.warn("No configuration file found", { owner, repository, filePath });
269
- } else {
270
- this._logger.error("Failed to download the requested file", { err, owner, repository, filePath });
271
- }
473
+ state.inFlight.add(key);
474
+ let resolved = null;
475
+ try {
476
+ const octokit = await this._getOctokitForLocation(location, state);
477
+ if (!octokit) {
478
+ this._logger.warn("Skipping import; no authorized Octokit for owner.", { location });
479
+ return null;
480
+ }
481
+ const { config, imports, errors } = await this._loadConfigSource(location, octokit);
482
+ if (errors && errors.length) {
483
+ this._logger.warn("Skipping import due to YAML parsing errors.", { location, errors });
484
+ return null;
485
+ }
486
+ if (!config) {
487
+ return null;
488
+ }
489
+ const importedConfigs = [];
490
+ for (const next of imports) {
491
+ const nested = await this._resolveImportedConfiguration(next, state, depth + 1);
492
+ if (nested) importedConfigs.push(nested);
272
493
  }
494
+ const mergedConfig = mergeImportedConfigs(importedConfigs, config);
495
+ if (!mergedConfig) return null;
496
+ const decoded = this._decodeConfiguration(location, mergedConfig);
497
+ resolved = decoded.config;
498
+ } finally {
499
+ state.inFlight.delete(key);
500
+ state.cache.set(key, resolved);
501
+ }
502
+ return resolved;
503
+ }
504
+ async _download({ repository, owner, octokit }) {
505
+ if (!repository || !owner) {
506
+ this._logger.warn("Repo or owner is not defined, cannot download the requested file");
507
+ return null;
508
+ }
509
+ const pathList = getConfigPathCandidatesForEnvironment(this._environment);
510
+ for (const filePath of pathList) {
511
+ const content = await this._tryDownloadPath({ repository, owner, octokit, filePath });
512
+ if (content !== null) return content;
273
513
  }
274
514
  return null;
275
515
  }
276
- _parseYaml(data) {
277
- this._logger.debug("Will attempt to parse YAML data", { data });
516
+ async _tryDownloadPath({
517
+ repository,
518
+ owner,
519
+ octokit,
520
+ filePath
521
+ }) {
522
+ try {
523
+ this._logger.info("Attempting to fetch configuration", { owner, repository, filePath });
524
+ const { data, headers } = await octokit.rest.repos.getContent({
525
+ owner,
526
+ repo: repository,
527
+ path: filePath,
528
+ mediaType: { format: "raw" }
529
+ });
530
+ logOk(this._logger, "Configuration file found", { owner, repository, filePath, rateLimitRemaining: headers?.["x-ratelimit-remaining"] });
531
+ return data;
532
+ } catch (err) {
533
+ this._handleDownloadError(err, { owner, repository, filePath });
534
+ return null;
535
+ }
536
+ }
537
+ _handleDownloadError(err, context) {
538
+ const status = err && typeof err === "object" && "status" in err ? Number(err.status) : null;
539
+ if (status === 404) {
540
+ this._logger.warn("No configuration file found", context);
541
+ return;
542
+ }
543
+ const metadata = { err, ...context, ...status ? { status } : {} };
544
+ if (status && status >= 500) {
545
+ this._logger.error("Failed to download the requested file", metadata);
546
+ } else {
547
+ this._logger.warn("Failed to download the requested file", metadata);
548
+ }
549
+ }
550
+ /*
551
+ * Parse the raw YAML content and returns the loaded YAML, or errors if any.
552
+ */
553
+ parseYaml(data) {
554
+ this._logger.info("Will attempt to parse YAML data");
278
555
  try {
279
556
  if (data) {
280
557
  const parsedData = import_js_yaml.default.load(data);
281
- this._logger.debug("Parsed yaml data", { parsedData });
558
+ logOk(this._logger, "Parsed yaml data successfully");
282
559
  return { yaml: parsedData ?? null, errors: null };
283
560
  }
284
561
  } catch (error) {
285
- this._logger.error("Error parsing YAML", { error });
562
+ this._logger.warn("Error parsing YAML", { error });
286
563
  return { errors: [error], yaml: null };
287
564
  }
288
- this._logger.debug("Could not parse YAML");
565
+ this._logger.warn("Could not parse YAML");
289
566
  return { yaml: null, errors: null };
290
567
  }
291
568
  mergeConfigurations(configuration1, configuration2) {
@@ -300,7 +577,67 @@ var ConfigurationHandler = class {
300
577
  };
301
578
  }
302
579
  getManifest(plugin) {
303
- return this._fetchActionManifest(plugin);
580
+ return isGithubPlugin(plugin) ? this._fetchActionManifest(plugin) : this._fetchWorkerManifest(plugin);
581
+ }
582
+ async _fetchWorkerManifest(url) {
583
+ if (this._manifestCache[url]) {
584
+ return this._manifestCache[url];
585
+ }
586
+ const manifestUrl = `${url}/manifest.json`;
587
+ try {
588
+ const result = await fetch(manifestUrl);
589
+ if (!result.ok) {
590
+ this._logger.error("Could not find a manifest for Worker", { manifestUrl, status: result.status });
591
+ return null;
592
+ }
593
+ const jsonData = await result.json();
594
+ const manifest = this._decodeManifest(jsonData);
595
+ this._manifestCache[url] = manifest;
596
+ return manifest;
597
+ } catch (e) {
598
+ this._logger.error("Could not find a manifest for Worker", { manifestUrl, err: e });
599
+ }
600
+ return null;
601
+ }
602
+ async _fetchUrlManifest(pluginUrl) {
603
+ const manifestUrl = resolveManifestUrl(pluginUrl);
604
+ if (!manifestUrl) {
605
+ this._logger.warn("Invalid plugin URL; cannot fetch manifest", { pluginUrl });
606
+ return null;
607
+ }
608
+ const manifestKey = `url:${manifestUrl}`;
609
+ if (this._manifestCache[manifestKey]) {
610
+ return this._manifestCache[manifestKey];
611
+ }
612
+ if (this._manifestPromiseCache[manifestKey]) {
613
+ return this._manifestPromiseCache[manifestKey];
614
+ }
615
+ const manifestPromise = (async () => {
616
+ if (typeof fetch !== "function") {
617
+ this._logger.warn("Fetch is unavailable; cannot load URL manifest", { manifestUrl });
618
+ return null;
619
+ }
620
+ try {
621
+ const response = await fetch(manifestUrl);
622
+ if (!response.ok) {
623
+ this._logger.warn("URL manifest request failed", { manifestUrl, status: response.status });
624
+ return null;
625
+ }
626
+ const data = await response.json();
627
+ const manifest = this._decodeManifest(data);
628
+ this._manifestCache[manifestKey] = manifest;
629
+ return manifest;
630
+ } catch (e) {
631
+ this._logger.warn("Could not load URL manifest", { manifestUrl, err: e });
632
+ return null;
633
+ }
634
+ })();
635
+ this._manifestPromiseCache[manifestKey] = manifestPromise;
636
+ try {
637
+ return await manifestPromise;
638
+ } finally {
639
+ delete this._manifestPromiseCache[manifestKey];
640
+ }
304
641
  }
305
642
  async _fetchActionManifest({ owner, repo, ref }) {
306
643
  const manifestKey = ref ? `${owner}:${repo}:${ref}` : `${owner}:${repo}`;
@@ -326,7 +663,7 @@ var ConfigurationHandler = class {
326
663
  return manifest;
327
664
  }
328
665
  } catch (e) {
329
- this._logger.error("Could not find a valid manifest", { owner, repo, err: e });
666
+ this._logger.warn("Could not find a valid manifest", { owner, repo, err: e });
330
667
  }
331
668
  return null;
332
669
  })();
@@ -341,7 +678,7 @@ var ConfigurationHandler = class {
341
678
  const errors = [...import_value.Value.Errors(manifestSchema, manifest)];
342
679
  if (errors.length) {
343
680
  for (const error of errors) {
344
- this._logger.error("Manifest validation error", { error });
681
+ this._logger.warn("Manifest validation error", { error });
345
682
  }
346
683
  throw new Error("Manifest is invalid.");
347
684
  }
@@ -354,5 +691,6 @@ var ConfigurationHandler = class {
354
691
  CONFIG_DEV_FULL_PATH,
355
692
  CONFIG_ORG_REPO,
356
693
  CONFIG_PROD_FULL_PATH,
357
- ConfigurationHandler
694
+ ConfigurationHandler,
695
+ isGithubPlugin
358
696
  });