@voilabs/plugins 0.0.1-beta.0 → 0.0.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/manager.js CHANGED
@@ -49,6 +49,88 @@ export class MemoryPluginDatabase {
49
49
  this.records.delete(storageKey(pluginId, scope.tenantId));
50
50
  }
51
51
  }
52
+ export class RedisPluginDatabase {
53
+ client;
54
+ prefix;
55
+ constructor(client, options = {}) {
56
+ this.client = client;
57
+ this.prefix = options.prefix ?? "voilabs:plugins";
58
+ }
59
+ async list(scope = {}) {
60
+ const pluginIds = await this.readIndex(scope);
61
+ const installations = await Promise.all(pluginIds.map((pluginId) => this.load(pluginId, scope)));
62
+ return installations.filter((installation) => Boolean(installation));
63
+ }
64
+ async load(pluginId, scope = {}) {
65
+ const value = await this.client.get(this.installationKey(pluginId, scope));
66
+ if (!value) {
67
+ return null;
68
+ }
69
+ return parseRedisJson(value);
70
+ }
71
+ async save(installation) {
72
+ const scope = { tenantId: installation.tenantId };
73
+ await this.client.set(this.installationKey(installation.pluginId, scope), JSON.stringify(installation));
74
+ await this.addToIndex(installation.pluginId, scope);
75
+ return installation;
76
+ }
77
+ async update(pluginId, patch, scope = {}) {
78
+ const current = await this.load(pluginId, scope);
79
+ if (!current) {
80
+ throw new PluginNotInstalledError(pluginId);
81
+ }
82
+ const updated = {
83
+ ...current,
84
+ ...patch,
85
+ pluginId,
86
+ tenantId: scope.tenantId,
87
+ updatedAt: new Date().toISOString(),
88
+ };
89
+ await this.save(updated);
90
+ return updated;
91
+ }
92
+ async delete(pluginId, scope = {}) {
93
+ await this.client.del(this.installationKey(pluginId, scope));
94
+ await this.removeFromIndex(pluginId, scope);
95
+ }
96
+ async readIndex(scope) {
97
+ if (this.client.smembers) {
98
+ const members = await this.client.smembers(this.indexKey(scope));
99
+ return normalizeRedisMembers(members);
100
+ }
101
+ const value = await this.client.get(this.indexKey(scope));
102
+ if (!value) {
103
+ return [];
104
+ }
105
+ const parsed = parseRedisJson(value);
106
+ return Array.isArray(parsed)
107
+ ? parsed.filter((item) => typeof item === "string")
108
+ : [];
109
+ }
110
+ async addToIndex(pluginId, scope) {
111
+ if (this.client.sadd) {
112
+ await this.client.sadd(this.indexKey(scope), pluginId);
113
+ return;
114
+ }
115
+ const ids = new Set(await this.readIndex(scope));
116
+ ids.add(pluginId);
117
+ await this.client.set(this.indexKey(scope), JSON.stringify([...ids]));
118
+ }
119
+ async removeFromIndex(pluginId, scope) {
120
+ if (this.client.srem) {
121
+ await this.client.srem(this.indexKey(scope), pluginId);
122
+ return;
123
+ }
124
+ const ids = (await this.readIndex(scope)).filter((id) => id !== pluginId);
125
+ await this.client.set(this.indexKey(scope), JSON.stringify(ids));
126
+ }
127
+ installationKey(pluginId, scope) {
128
+ return `${this.prefix}:installation:${scopeKey(scope.tenantId)}:${encodeKeyPart(pluginId)}`;
129
+ }
130
+ indexKey(scope) {
131
+ return `${this.prefix}:index:${scopeKey(scope.tenantId)}`;
132
+ }
133
+ }
52
134
  export class PluginManager {
53
135
  marketplaces;
54
136
  redisUrl;
@@ -62,17 +144,27 @@ export class PluginManager {
62
144
  marketplaceSyncPromise;
63
145
  autoSyncMarketplaces;
64
146
  marketplaceRefreshIntervalMs;
147
+ marketplaceRequestTimeoutMs;
65
148
  github;
149
+ remotePluginAssets = new Map();
66
150
  constructor(options = {}) {
67
151
  this.marketplaces = options.marketplaces ?? [];
68
152
  this.redisUrl = options.redisUrl;
69
- this.database = options.database ?? new MemoryPluginDatabase();
153
+ this.database =
154
+ options.database ??
155
+ (options.redis
156
+ ? new RedisPluginDatabase(options.redis, {
157
+ prefix: options.redisPrefix,
158
+ })
159
+ : new MemoryPluginDatabase());
70
160
  this.encryption = options.encryption;
71
161
  this.fetcher = options.fetcher ?? globalThis.fetch?.bind(globalThis);
72
162
  this.defaultTenantId = options.defaultTenantId;
73
163
  this.autoSyncMarketplaces =
74
164
  options.autoSyncMarketplaces ?? this.marketplaces.length > 0;
75
165
  this.marketplaceRefreshIntervalMs = options.marketplaceRefreshIntervalMs;
166
+ this.marketplaceRequestTimeoutMs =
167
+ options.marketplaceRequestTimeoutMs ?? 15_000;
76
168
  this.github = options.github ?? {};
77
169
  if (options.plugins?.length) {
78
170
  this.register(options.plugins);
@@ -306,9 +398,70 @@ export class PluginManager {
306
398
  .map((component) => ({
307
399
  ...component,
308
400
  pluginId: plugin.id,
401
+ pluginName: plugin.name,
402
+ pluginVersion: plugin.version,
309
403
  }));
310
404
  });
311
405
  }
406
+ async listFrontendComponents(options = {}) {
407
+ await this.ready();
408
+ const installedOnly = options.installedOnly ?? true;
409
+ const enabledOnly = options.enabledOnly ?? true;
410
+ const tenantId = this.resolveTenantId(options);
411
+ const components = this.getFrontendComponents(options.slot).filter((component) => !options.pluginId || component.pluginId === options.pluginId);
412
+ if (!installedOnly && !enabledOnly) {
413
+ return components;
414
+ }
415
+ const installations = await this.listInstallations({ tenantId });
416
+ const installationByPluginId = new Map(installations.map((installation) => [installation.pluginId, installation]));
417
+ return components.filter((component) => {
418
+ const installation = installationByPluginId.get(component.pluginId);
419
+ if (installedOnly && !installation) {
420
+ return false;
421
+ }
422
+ if (enabledOnly && !installation?.enabled) {
423
+ return false;
424
+ }
425
+ return true;
426
+ });
427
+ }
428
+ async fetchPluginAsset(pluginId, assetPath) {
429
+ await this.ready();
430
+ if (!this.fetcher) {
431
+ throw new PluginManagerError("No fetch implementation is available.");
432
+ }
433
+ this.require(pluginId);
434
+ const source = this.remotePluginAssets.get(pluginId);
435
+ if (!source) {
436
+ throw new PluginManagerError(`Plugin "${pluginId}" does not expose remote marketplace assets.`);
437
+ }
438
+ const normalizedAssetPath = normalizeSafeAssetPath(assetPath);
439
+ const githubPath = joinGitHubPath(source.rootPath, normalizedAssetPath);
440
+ const response = await fetchWithTimeout(this.fetcher, githubRawFileUrl(source, githubPath), {
441
+ headers: source.headers,
442
+ }, this.marketplaceRequestTimeoutMs);
443
+ if (!response.ok) {
444
+ return new Response(null, {
445
+ status: response.status,
446
+ statusText: response.statusText,
447
+ });
448
+ }
449
+ const headers = new Headers();
450
+ const contentType = guessContentType(normalizedAssetPath, response.headers.get("content-type") ?? undefined);
451
+ if (contentType) {
452
+ headers.set("content-type", contentType);
453
+ }
454
+ const cacheControl = response.headers.get("cache-control");
455
+ if (cacheControl) {
456
+ headers.set("cache-control", cacheControl);
457
+ }
458
+ headers.set("x-content-type-options", "nosniff");
459
+ return new Response(response.body, {
460
+ status: response.status,
461
+ statusText: response.statusText,
462
+ headers,
463
+ });
464
+ }
312
465
  async getInjections(options = {}) {
313
466
  await this.ready();
314
467
  const tenantId = this.resolveTenantId(options);
@@ -428,21 +581,21 @@ export class PluginManager {
428
581
  const pluginsById = new Map();
429
582
  for (const candidate of candidates) {
430
583
  try {
431
- const response = await this.fetcher(candidate.url, {
584
+ const response = await fetchWithTimeout(this.fetcher, candidate.url, {
432
585
  headers: candidate.headers,
433
- });
586
+ }, this.marketplaceRequestTimeoutMs);
434
587
  if (!response.ok) {
435
588
  lastError = new Error(`${response.status} ${response.statusText}`);
436
589
  continue;
437
590
  }
438
591
  successfulCandidates += 1;
439
592
  if (candidate.parser === "github-tree") {
440
- const pluginFileCandidates = await readGitHubPluginFileCandidates(response, candidate);
441
- for (const pluginFileCandidate of pluginFileCandidates) {
593
+ const pluginSchemaCandidates = await readGitHubPluginSchemaCandidates(response, candidate);
594
+ for (const pluginFileCandidate of pluginSchemaCandidates) {
442
595
  try {
443
- const pluginFileResponse = await this.fetcher(pluginFileCandidate.url, {
596
+ const pluginFileResponse = await fetchWithTimeout(this.fetcher, pluginFileCandidate.url, {
444
597
  headers: pluginFileCandidate.headers,
445
- });
598
+ }, this.marketplaceRequestTimeoutMs);
446
599
  if (!pluginFileResponse.ok) {
447
600
  lastError = new Error(`${pluginFileResponse.status} ${pluginFileResponse.statusText}`);
448
601
  continue;
@@ -451,6 +604,7 @@ export class PluginManager {
451
604
  const payload = await readMarketplacePayload(pluginFileResponse, pluginFileCandidate);
452
605
  for (const plugin of extractMarketplacePlugins(payload)) {
453
606
  const normalizedPlugin = applyMarketplaceDefaults(plugin, pluginFileCandidate);
607
+ this.rememberRemotePluginAssetSource(normalizedPlugin, pluginFileCandidate);
454
608
  pluginsById.set(normalizedPlugin.id, normalizedPlugin);
455
609
  }
456
610
  }
@@ -463,6 +617,7 @@ export class PluginManager {
463
617
  const payload = await readMarketplacePayload(response, candidate);
464
618
  for (const plugin of extractMarketplacePlugins(payload)) {
465
619
  const normalizedPlugin = applyMarketplaceDefaults(plugin, candidate);
620
+ this.rememberRemotePluginAssetSource(normalizedPlugin, candidate);
466
621
  pluginsById.set(normalizedPlugin.id, normalizedPlugin);
467
622
  }
468
623
  }
@@ -479,6 +634,15 @@ export class PluginManager {
479
634
  }
480
635
  throw new PluginManagerError(`Marketplace "${marketplaceLabel(marketplaceSource)}" could not be loaded: ${String(lastError)}`);
481
636
  }
637
+ rememberRemotePluginAssetSource(plugin, candidate) {
638
+ if (!candidate.github) {
639
+ return;
640
+ }
641
+ this.remotePluginAssets.set(plugin.id, {
642
+ ...candidate.github,
643
+ rootPath: pluginRootPath(candidate),
644
+ });
645
+ }
482
646
  shouldSyncMarketplaces() {
483
647
  return Boolean(this.autoSyncMarketplaces &&
484
648
  this.marketplaces.length &&
@@ -559,6 +723,27 @@ export class PluginManager {
559
723
  function storageKey(pluginId, tenantId) {
560
724
  return `${tenantId ?? "default"}:${pluginId}`;
561
725
  }
726
+ function scopeKey(tenantId) {
727
+ return encodeKeyPart(tenantId ?? "default");
728
+ }
729
+ function encodeKeyPart(value) {
730
+ return encodeURIComponent(value).replace(/%/g, "~");
731
+ }
732
+ function parseRedisJson(value) {
733
+ if (typeof value === "string") {
734
+ return JSON.parse(value);
735
+ }
736
+ return value;
737
+ }
738
+ function normalizeRedisMembers(value) {
739
+ if (Array.isArray(value)) {
740
+ return value.filter((item) => typeof item === "string");
741
+ }
742
+ if (value instanceof Set) {
743
+ return [...value].filter((item) => typeof item === "string");
744
+ }
745
+ return [];
746
+ }
562
747
  function toInstallationSummary(installation) {
563
748
  return {
564
749
  installed: true,
@@ -740,7 +925,10 @@ function githubMarketplaceCandidates(location, options) {
740
925
  "plugins.json",
741
926
  "index.json",
742
927
  ];
743
- const pluginFileExtensions = normalizePluginFileExtensions(options.pluginFileExtensions);
928
+ const schemaFileNames = normalizeSchemaFileNames(options.schemaFileNames);
929
+ const legacyPluginFileExtensions = normalizePluginFileExtensions(options.pluginFileExtensions);
930
+ const includeSchemaFiles = options.schemaFiles !== false;
931
+ const includeLegacyPluginFiles = options.pluginFiles === true;
744
932
  const branchWasExplicit = Boolean(location.branch ?? options.branch);
745
933
  const branches = branchWasExplicit
746
934
  ? [location.branch ?? options.branch ?? "main"]
@@ -756,7 +944,7 @@ function githubMarketplaceCandidates(location, options) {
756
944
  : undefined);
757
945
  const candidates = [];
758
946
  for (const branch of branches) {
759
- if (!location.exactPath && options.pluginFiles !== false) {
947
+ if (!location.exactPath && (includeSchemaFiles || includeLegacyPluginFiles)) {
760
948
  candidates.push({
761
949
  url: `${apiBaseUrl}/repos/${location.owner}/${location.repo}/git/trees/${encodeURIComponent(branch)}?recursive=${options.recursivePluginFiles === false ? "0" : "1"}`,
762
950
  headers,
@@ -768,7 +956,9 @@ function githubMarketplaceCandidates(location, options) {
768
956
  pathPrefix: normalizePathPrefix(location.path),
769
957
  rawBaseUrl,
770
958
  apiBaseUrl,
771
- extensions: pluginFileExtensions,
959
+ schemaFileNames,
960
+ legacyPluginFileExtensions,
961
+ includeLegacyPluginFiles,
772
962
  avatarSize: options.avatarSize ?? 128,
773
963
  headers,
774
964
  },
@@ -776,10 +966,13 @@ function githubMarketplaceCandidates(location, options) {
776
966
  }
777
967
  for (const path of paths) {
778
968
  const normalizedPath = path.replace(/^\/+/, "");
969
+ const sourceKind = githubSourceKindForPath(normalizedPath, schemaFileNames, legacyPluginFileExtensions, includeLegacyPluginFiles);
779
970
  candidates.push({
780
971
  url: `${rawBaseUrl}/${location.owner}/${location.repo}/${branch}/${normalizedPath}`,
781
972
  headers,
782
973
  parser: "json",
974
+ sourcePath: normalizedPath,
975
+ sourceKind,
783
976
  github: {
784
977
  owner: location.owner,
785
978
  repo: location.repo,
@@ -787,7 +980,9 @@ function githubMarketplaceCandidates(location, options) {
787
980
  pathPrefix: normalizePathPrefix(location.path),
788
981
  rawBaseUrl,
789
982
  apiBaseUrl,
790
- extensions: pluginFileExtensions,
983
+ schemaFileNames,
984
+ legacyPluginFileExtensions,
985
+ includeLegacyPluginFiles,
791
986
  avatarSize: options.avatarSize ?? 128,
792
987
  headers,
793
988
  },
@@ -796,6 +991,8 @@ function githubMarketplaceCandidates(location, options) {
796
991
  url: `${apiBaseUrl}/repos/${location.owner}/${location.repo}/contents/${encodeUrlPath(normalizedPath)}?ref=${encodeURIComponent(branch)}`,
797
992
  headers,
798
993
  parser: "github-content",
994
+ sourcePath: normalizedPath,
995
+ sourceKind,
799
996
  github: {
800
997
  owner: location.owner,
801
998
  repo: location.repo,
@@ -803,7 +1000,9 @@ function githubMarketplaceCandidates(location, options) {
803
1000
  pathPrefix: normalizePathPrefix(location.path),
804
1001
  rawBaseUrl,
805
1002
  apiBaseUrl,
806
- extensions: pluginFileExtensions,
1003
+ schemaFileNames,
1004
+ legacyPluginFileExtensions,
1005
+ includeLegacyPluginFiles,
807
1006
  avatarSize: options.avatarSize ?? 128,
808
1007
  headers,
809
1008
  },
@@ -878,7 +1077,7 @@ function extractMarketplacePlugins(payload) {
878
1077
  ? [payload.plugin, ...(payload.plugins ?? payload.marketplace?.plugins ?? [])]
879
1078
  : payload.plugins ?? payload.marketplace?.plugins ?? [];
880
1079
  }
881
- async function readGitHubPluginFileCandidates(response, candidate) {
1080
+ async function readGitHubPluginSchemaCandidates(response, candidate) {
882
1081
  if (!candidate.github) {
883
1082
  return [];
884
1083
  }
@@ -886,20 +1085,25 @@ async function readGitHubPluginFileCandidates(response, candidate) {
886
1085
  const payload = (await response.json());
887
1086
  return (payload.tree ?? [])
888
1087
  .filter((item) => Boolean(item.path && item.type === "blob"))
889
- .filter((item) => isPluginFilePath(item.path, discovery))
1088
+ .filter((item) => isPluginSchemaPath(item.path, discovery))
890
1089
  .flatMap((item) => {
891
1090
  const encodedPath = encodeUrlPath(item.path);
1091
+ const sourceKind = githubSourceKindForPath(item.path, discovery.schemaFileNames, discovery.legacyPluginFileExtensions, discovery.includeLegacyPluginFiles);
892
1092
  return [
893
1093
  {
894
1094
  url: `${discovery.rawBaseUrl}/${discovery.owner}/${discovery.repo}/${discovery.branch}/${encodedPath}`,
895
1095
  headers: discovery.headers,
896
1096
  parser: "json",
1097
+ sourcePath: item.path,
1098
+ sourceKind,
897
1099
  github: discovery,
898
1100
  },
899
1101
  {
900
1102
  url: `${discovery.apiBaseUrl}/repos/${discovery.owner}/${discovery.repo}/contents/${encodedPath}?ref=${encodeURIComponent(discovery.branch)}`,
901
1103
  headers: discovery.headers,
902
1104
  parser: "github-content",
1105
+ sourcePath: item.path,
1106
+ sourceKind,
903
1107
  github: discovery,
904
1108
  },
905
1109
  ];
@@ -921,23 +1125,26 @@ function applyMarketplaceDefaults(plugin, candidate) {
921
1125
  }
922
1126
  return plugin;
923
1127
  }
1128
+ const normalizedPlugin = normalizeGitHubPluginReferences(plugin, candidate);
924
1129
  return {
925
- ...plugin,
1130
+ ...normalizedPlugin,
926
1131
  provider: github.owner,
927
1132
  iconUrl: githubAvatarUrl(github.owner, github.avatarSize),
928
- repositoryUrl: plugin.repositoryUrl ??
1133
+ repositoryUrl: normalizedPlugin.repositoryUrl ??
929
1134
  `https://github.com/${github.owner}/${github.repo}`,
930
1135
  meta: {
931
- ...plugin.meta,
1136
+ ...normalizedPlugin.meta,
932
1137
  github: {
933
1138
  owner: github.owner,
934
1139
  repo: github.repo,
935
1140
  branch: github.branch,
1141
+ rootPath: pluginRootPath(candidate),
1142
+ schemaPath: candidate.sourceKind === "schema" ? candidate.sourcePath : undefined,
936
1143
  },
937
1144
  },
938
1145
  };
939
1146
  }
940
- function isPluginFilePath(path, discovery) {
1147
+ function isPluginSchemaPath(path, discovery) {
941
1148
  const normalizedPath = path.replace(/^\/+/, "");
942
1149
  const prefix = discovery.pathPrefix;
943
1150
  if (prefix &&
@@ -945,7 +1152,205 @@ function isPluginFilePath(path, discovery) {
945
1152
  !normalizedPath.startsWith(`${prefix}/`)) {
946
1153
  return false;
947
1154
  }
948
- return discovery.extensions.some((extension) => normalizedPath.toLocaleLowerCase().endsWith(extension));
1155
+ const lowerPath = normalizedPath.toLocaleLowerCase();
1156
+ const fileName = lowerPath.split("/").pop() ?? lowerPath;
1157
+ if (discovery.schemaFileNames.includes(fileName)) {
1158
+ return true;
1159
+ }
1160
+ return (discovery.includeLegacyPluginFiles &&
1161
+ discovery.legacyPluginFileExtensions.some((extension) => lowerPath.endsWith(extension)));
1162
+ }
1163
+ function normalizeGitHubPluginReferences(plugin, candidate) {
1164
+ const github = candidate.github;
1165
+ if (!github) {
1166
+ return plugin;
1167
+ }
1168
+ const rootPath = pluginRootPath(candidate);
1169
+ const resolve = (value) => value ? resolveGitHubReferenceUrl(github, rootPath, value) : value;
1170
+ return {
1171
+ ...plugin,
1172
+ docsUrl: resolve(plugin.docsUrl),
1173
+ websiteUrl: resolve(plugin.websiteUrl),
1174
+ frontend: normalizeFrontendManifest(plugin.frontend, github, rootPath),
1175
+ injections: plugin.injections?.map((injection) => ({
1176
+ ...injection,
1177
+ src: resolve(injection.src),
1178
+ attributes: normalizeUrlAttributes(injection.attributes, resolve),
1179
+ })),
1180
+ assets: plugin.assets?.map((asset) => normalizePluginAsset(asset, resolve)),
1181
+ };
1182
+ }
1183
+ function normalizeFrontendManifest(frontend, github, rootPath) {
1184
+ if (!frontend) {
1185
+ return frontend;
1186
+ }
1187
+ return {
1188
+ ...frontend,
1189
+ registry: frontend.registry
1190
+ ? Object.fromEntries(Object.entries(frontend.registry).map(([key, component]) => [
1191
+ key,
1192
+ normalizeComponentReference(component, github, rootPath),
1193
+ ]))
1194
+ : frontend.registry,
1195
+ components: frontend.components?.map((component) => ({
1196
+ ...component,
1197
+ component: normalizeComponentReference(component.component, github, rootPath),
1198
+ })),
1199
+ };
1200
+ }
1201
+ function normalizeComponentReference(reference, github, rootPath) {
1202
+ if (reference.type !== "remote") {
1203
+ return reference;
1204
+ }
1205
+ const referencePath = reference.path ??
1206
+ (reference.url && isRelativeReference(reference.url)
1207
+ ? reference.url
1208
+ : undefined);
1209
+ const urlSource = reference.url ?? referencePath;
1210
+ return {
1211
+ ...reference,
1212
+ path: referencePath ? normalizeClientAssetPath(referencePath) : reference.path,
1213
+ url: urlSource
1214
+ ? resolveGitHubReferenceUrl(github, rootPath, urlSource)
1215
+ : reference.url,
1216
+ };
1217
+ }
1218
+ function normalizePluginAsset(asset, resolve) {
1219
+ return {
1220
+ ...asset,
1221
+ url: resolve(asset.url) ?? asset.url,
1222
+ };
1223
+ }
1224
+ function normalizeUrlAttributes(attributes, resolve) {
1225
+ if (!attributes) {
1226
+ return attributes;
1227
+ }
1228
+ const urlAttributes = new Set(["href", "src", "poster"]);
1229
+ return Object.fromEntries(Object.entries(attributes).map(([key, value]) => [
1230
+ key,
1231
+ typeof value === "string" && urlAttributes.has(key.toLocaleLowerCase())
1232
+ ? resolve(value)
1233
+ : value,
1234
+ ]));
1235
+ }
1236
+ function pluginRootPath(candidate) {
1237
+ if (!candidate.sourcePath) {
1238
+ return candidate.github?.pathPrefix;
1239
+ }
1240
+ return dirname(candidate.sourcePath) ?? candidate.github?.pathPrefix;
1241
+ }
1242
+ function githubSourceKindForPath(path, schemaFileNames, legacyPluginFileExtensions, includeLegacyPluginFiles) {
1243
+ const lowerPath = path.replace(/^\/+/, "").toLocaleLowerCase();
1244
+ const fileName = lowerPath.split("/").pop() ?? lowerPath;
1245
+ if (schemaFileNames.includes(fileName)) {
1246
+ return "schema";
1247
+ }
1248
+ if (includeLegacyPluginFiles &&
1249
+ legacyPluginFileExtensions.some((extension) => lowerPath.endsWith(extension))) {
1250
+ return "legacy-plugin";
1251
+ }
1252
+ return "marketplace";
1253
+ }
1254
+ function resolveGitHubReferenceUrl(github, rootPath, value) {
1255
+ if (!isRelativeReference(value)) {
1256
+ return value;
1257
+ }
1258
+ const { path, suffix } = splitReferenceSuffix(value);
1259
+ const basePath = path.startsWith("/") ? undefined : rootPath;
1260
+ const githubPath = joinGitHubPath(basePath, path);
1261
+ return `${githubRawFileUrl(github, githubPath)}${suffix}`;
1262
+ }
1263
+ function githubRawFileUrl(github, path) {
1264
+ return `${github.rawBaseUrl}/${github.owner}/${github.repo}/${github.branch}/${encodeUrlPath(path)}`;
1265
+ }
1266
+ function splitReferenceSuffix(value) {
1267
+ const index = value.search(/[?#]/);
1268
+ if (index === -1) {
1269
+ return { path: value, suffix: "" };
1270
+ }
1271
+ return {
1272
+ path: value.slice(0, index),
1273
+ suffix: value.slice(index),
1274
+ };
1275
+ }
1276
+ function isRelativeReference(value) {
1277
+ return (!/^[a-z][a-z0-9+.-]*:/i.test(value) &&
1278
+ !value.startsWith("//") &&
1279
+ !value.startsWith("#") &&
1280
+ !value.startsWith("{{"));
1281
+ }
1282
+ function normalizeClientAssetPath(path) {
1283
+ if (!isRelativeReference(path) || path.startsWith("/")) {
1284
+ return undefined;
1285
+ }
1286
+ const { path: withoutSuffix } = splitReferenceSuffix(path);
1287
+ if (withoutSuffix.split("/").some((part) => part === "..")) {
1288
+ return undefined;
1289
+ }
1290
+ return normalizeGitHubPath(withoutSuffix);
1291
+ }
1292
+ function normalizeSafeAssetPath(path) {
1293
+ const decoded = safeDecodeURIComponent(path).replace(/^\/+/, "");
1294
+ const parts = decoded.split("/").filter(Boolean);
1295
+ if (!parts.length) {
1296
+ throw new PluginManagerError("Plugin asset path is required.");
1297
+ }
1298
+ if (parts.some((part) => part === "." || part === "..")) {
1299
+ throw new PluginManagerError("Plugin asset path cannot leave the plugin folder.");
1300
+ }
1301
+ return parts.join("/");
1302
+ }
1303
+ function joinGitHubPath(basePath, path) {
1304
+ return normalizeGitHubPath([basePath, path.replace(/^\/+/, "")].filter(Boolean).join("/"));
1305
+ }
1306
+ function normalizeGitHubPath(path) {
1307
+ const parts = [];
1308
+ for (const part of path.replace(/^\/+/, "").split("/")) {
1309
+ if (!part || part === ".") {
1310
+ continue;
1311
+ }
1312
+ if (part === "..") {
1313
+ parts.pop();
1314
+ continue;
1315
+ }
1316
+ parts.push(part);
1317
+ }
1318
+ return parts.join("/");
1319
+ }
1320
+ function dirname(path) {
1321
+ const parts = normalizeGitHubPath(path).split("/");
1322
+ parts.pop();
1323
+ return parts.length ? parts.join("/") : undefined;
1324
+ }
1325
+ function safeDecodeURIComponent(value) {
1326
+ try {
1327
+ return decodeURIComponent(value);
1328
+ }
1329
+ catch {
1330
+ return value;
1331
+ }
1332
+ }
1333
+ function guessContentType(path, upstream) {
1334
+ const extension = path.split(".").pop()?.toLocaleLowerCase();
1335
+ const knownTypes = {
1336
+ css: "text/css; charset=utf-8",
1337
+ gif: "image/gif",
1338
+ html: "text/html; charset=utf-8",
1339
+ ico: "image/x-icon",
1340
+ jpeg: "image/jpeg",
1341
+ jpg: "image/jpeg",
1342
+ js: "application/javascript; charset=utf-8",
1343
+ json: "application/json; charset=utf-8",
1344
+ jsx: "application/javascript; charset=utf-8",
1345
+ mjs: "application/javascript; charset=utf-8",
1346
+ png: "image/png",
1347
+ svg: "image/svg+xml",
1348
+ txt: "text/plain; charset=utf-8",
1349
+ webp: "image/webp",
1350
+ woff: "font/woff",
1351
+ woff2: "font/woff2",
1352
+ };
1353
+ return (extension && knownTypes[extension]) || upstream;
949
1354
  }
950
1355
  function marketplaceLabel(source) {
951
1356
  if (typeof source === "string") {
@@ -972,6 +1377,33 @@ function mergeHeaders(...headersList) {
972
1377
  });
973
1378
  return hasHeaders ? headers : undefined;
974
1379
  }
1380
+ async function fetchWithTimeout(fetcher, input, init, timeoutMs) {
1381
+ if (!timeoutMs || timeoutMs <= 0) {
1382
+ return fetcher(input, init);
1383
+ }
1384
+ const controller = new AbortController();
1385
+ let timeout;
1386
+ const timeoutPromise = new Promise((_resolve, reject) => {
1387
+ timeout = setTimeout(() => {
1388
+ controller.abort();
1389
+ reject(new PluginManagerError(`Marketplace request timed out after ${timeoutMs}ms.`));
1390
+ }, timeoutMs);
1391
+ });
1392
+ try {
1393
+ return await Promise.race([
1394
+ fetcher(input, {
1395
+ ...init,
1396
+ signal: controller.signal,
1397
+ }),
1398
+ timeoutPromise,
1399
+ ]);
1400
+ }
1401
+ finally {
1402
+ if (timeout) {
1403
+ clearTimeout(timeout);
1404
+ }
1405
+ }
1406
+ }
975
1407
  function decodeBase64(value) {
976
1408
  const normalized = value.replace(/\s/g, "");
977
1409
  const buffer = globalThis.Buffer;
@@ -992,6 +1424,9 @@ function normalizePathPrefix(path) {
992
1424
  const normalized = path?.replace(/^\/+|\/+$/g, "");
993
1425
  return normalized || undefined;
994
1426
  }
1427
+ function normalizeSchemaFileNames(fileNames) {
1428
+ return (fileNames?.length ? fileNames : ["schema.json"]).map((fileName) => fileName.replace(/^\/+/, "").toLocaleLowerCase());
1429
+ }
995
1430
  function normalizePluginFileExtensions(extensions) {
996
1431
  return (extensions?.length ? extensions : [".plugin"]).map((extension) => extension.startsWith(".")
997
1432
  ? extension.toLocaleLowerCase()