@voilabs/plugins 0.0.1-beta.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/README.md +634 -0
- package/dist/adapters/elysia.d.ts +31 -0
- package/dist/adapters/elysia.d.ts.map +1 -0
- package/dist/adapters/elysia.js +28 -0
- package/dist/adapters/elysia.js.map +1 -0
- package/dist/adapters/next.d.ts +25 -0
- package/dist/adapters/next.d.ts.map +1 -0
- package/dist/adapters/next.js +51 -0
- package/dist/adapters/next.js.map +1 -0
- package/dist/client.d.ts +33 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +123 -0
- package/dist/client.js.map +1 -0
- package/dist/examples/lumina.d.ts +9 -0
- package/dist/examples/lumina.d.ts.map +1 -0
- package/dist/examples/lumina.js +197 -0
- package/dist/examples/lumina.js.map +1 -0
- package/dist/http.d.ts +19 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +523 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/injection.d.ts +10 -0
- package/dist/injection.d.ts.map +1 -0
- package/dist/injection.js +174 -0
- package/dist/injection.js.map +1 -0
- package/dist/manager.d.ts +121 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +1006 -0
- package/dist/manager.js.map +1 -0
- package/dist/plugin.d.ts +25 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +209 -0
- package/dist/plugin.js.map +1 -0
- package/dist/react.d.ts +34 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +88 -0
- package/dist/react.js.map +1 -0
- package/dist/types.d.ts +416 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
package/dist/manager.js
ADDED
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
import { Plugin } from "./plugin.js";
|
|
2
|
+
import { injectHtml as injectHtmlString, renderInjectionHtml, renderPluginInjection, } from "./injection.js";
|
|
3
|
+
export class PluginManagerError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "PluginManagerError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class PluginNotFoundError extends PluginManagerError {
|
|
10
|
+
constructor(pluginId) {
|
|
11
|
+
super(`Plugin "${pluginId}" was not found.`);
|
|
12
|
+
this.name = "PluginNotFoundError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class PluginNotInstalledError extends PluginManagerError {
|
|
16
|
+
constructor(pluginId) {
|
|
17
|
+
super(`Plugin "${pluginId}" is not installed.`);
|
|
18
|
+
this.name = "PluginNotInstalledError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class MemoryPluginDatabase {
|
|
22
|
+
records = new Map();
|
|
23
|
+
async list(scope = {}) {
|
|
24
|
+
return [...this.records.values()].filter((record) => (record.tenantId ?? "") === (scope.tenantId ?? ""));
|
|
25
|
+
}
|
|
26
|
+
async load(pluginId, scope = {}) {
|
|
27
|
+
return this.records.get(storageKey(pluginId, scope.tenantId)) ?? null;
|
|
28
|
+
}
|
|
29
|
+
async save(installation) {
|
|
30
|
+
this.records.set(storageKey(installation.pluginId, installation.tenantId), installation);
|
|
31
|
+
return installation;
|
|
32
|
+
}
|
|
33
|
+
async update(pluginId, patch, scope = {}) {
|
|
34
|
+
const current = await this.load(pluginId, scope);
|
|
35
|
+
if (!current) {
|
|
36
|
+
throw new PluginNotInstalledError(pluginId);
|
|
37
|
+
}
|
|
38
|
+
const updated = {
|
|
39
|
+
...current,
|
|
40
|
+
...patch,
|
|
41
|
+
pluginId,
|
|
42
|
+
tenantId: scope.tenantId,
|
|
43
|
+
updatedAt: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
await this.save(updated);
|
|
46
|
+
return updated;
|
|
47
|
+
}
|
|
48
|
+
async delete(pluginId, scope = {}) {
|
|
49
|
+
this.records.delete(storageKey(pluginId, scope.tenantId));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export class PluginManager {
|
|
53
|
+
marketplaces;
|
|
54
|
+
redisUrl;
|
|
55
|
+
registry = new Map();
|
|
56
|
+
database;
|
|
57
|
+
encryption;
|
|
58
|
+
fetcher;
|
|
59
|
+
defaultTenantId;
|
|
60
|
+
marketplacesSynced = false;
|
|
61
|
+
marketplacesSyncedAt;
|
|
62
|
+
marketplaceSyncPromise;
|
|
63
|
+
autoSyncMarketplaces;
|
|
64
|
+
marketplaceRefreshIntervalMs;
|
|
65
|
+
github;
|
|
66
|
+
constructor(options = {}) {
|
|
67
|
+
this.marketplaces = options.marketplaces ?? [];
|
|
68
|
+
this.redisUrl = options.redisUrl;
|
|
69
|
+
this.database = options.database ?? new MemoryPluginDatabase();
|
|
70
|
+
this.encryption = options.encryption;
|
|
71
|
+
this.fetcher = options.fetcher ?? globalThis.fetch?.bind(globalThis);
|
|
72
|
+
this.defaultTenantId = options.defaultTenantId;
|
|
73
|
+
this.autoSyncMarketplaces =
|
|
74
|
+
options.autoSyncMarketplaces ?? this.marketplaces.length > 0;
|
|
75
|
+
this.marketplaceRefreshIntervalMs = options.marketplaceRefreshIntervalMs;
|
|
76
|
+
this.github = options.github ?? {};
|
|
77
|
+
if (options.plugins?.length) {
|
|
78
|
+
this.register(options.plugins);
|
|
79
|
+
}
|
|
80
|
+
if (options.syncOnInit) {
|
|
81
|
+
const initialSync = this.syncMarketplaces({ force: true });
|
|
82
|
+
initialSync.catch(() => undefined);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
register(pluginOrPlugins) {
|
|
86
|
+
const plugins = Array.isArray(pluginOrPlugins)
|
|
87
|
+
? pluginOrPlugins
|
|
88
|
+
: [pluginOrPlugins];
|
|
89
|
+
for (const pluginLike of plugins) {
|
|
90
|
+
const plugin = pluginLike instanceof Plugin ? pluginLike : new Plugin(pluginLike);
|
|
91
|
+
this.registry.set(plugin.id, plugin);
|
|
92
|
+
}
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
unregister(pluginId) {
|
|
96
|
+
return this.registry.delete(pluginId);
|
|
97
|
+
}
|
|
98
|
+
getById(pluginId) {
|
|
99
|
+
return this.registry.get(pluginId);
|
|
100
|
+
}
|
|
101
|
+
require(pluginId) {
|
|
102
|
+
const plugin = this.getById(pluginId);
|
|
103
|
+
if (!plugin) {
|
|
104
|
+
throw new PluginNotFoundError(pluginId);
|
|
105
|
+
}
|
|
106
|
+
return plugin;
|
|
107
|
+
}
|
|
108
|
+
async get(options = {}) {
|
|
109
|
+
await this.ready();
|
|
110
|
+
const page = Math.max(1, options.page ?? 1);
|
|
111
|
+
const limit = Math.max(1, Math.min(100, options.limit ?? 20));
|
|
112
|
+
const tenantId = this.resolveTenantId(options);
|
|
113
|
+
const query = options.query?.toLocaleLowerCase();
|
|
114
|
+
const tags = options.tags ?? [];
|
|
115
|
+
const installations = await this.listInstallations({ tenantId });
|
|
116
|
+
const installationByPluginId = new Map(installations.map((installation) => [installation.pluginId, installation]));
|
|
117
|
+
let items = [...this.registry.values()].map((plugin) => {
|
|
118
|
+
const installation = installationByPluginId.get(plugin.id);
|
|
119
|
+
return {
|
|
120
|
+
...plugin.toManifest(),
|
|
121
|
+
installation: installation
|
|
122
|
+
? toInstallationSummary(installation)
|
|
123
|
+
: undefined,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
if (options.category) {
|
|
127
|
+
items = items.filter((item) => item.category === options.category);
|
|
128
|
+
}
|
|
129
|
+
if (options.provider) {
|
|
130
|
+
items = items.filter((item) => item.provider === options.provider);
|
|
131
|
+
}
|
|
132
|
+
if (query) {
|
|
133
|
+
items = items.filter((item) => [item.name, item.summary, item.description, item.provider, ...(item.tags ?? [])]
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
.some((value) => String(value).toLocaleLowerCase().includes(query)));
|
|
136
|
+
}
|
|
137
|
+
if (tags.length) {
|
|
138
|
+
items = items.filter((item) => tags.every((tag) => item.tags?.includes(tag)));
|
|
139
|
+
}
|
|
140
|
+
if (options.installed !== undefined) {
|
|
141
|
+
items = items.filter((item) => Boolean(item.installation?.installed) === options.installed);
|
|
142
|
+
}
|
|
143
|
+
if (options.enabled !== undefined) {
|
|
144
|
+
items = items.filter((item) => Boolean(item.installation?.enabled) === options.enabled);
|
|
145
|
+
}
|
|
146
|
+
const total = items.length;
|
|
147
|
+
const start = (page - 1) * limit;
|
|
148
|
+
const data = items.slice(start, start + limit);
|
|
149
|
+
return {
|
|
150
|
+
data,
|
|
151
|
+
page,
|
|
152
|
+
limit,
|
|
153
|
+
total,
|
|
154
|
+
hasNextPage: start + data.length < total,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async getPluginView(pluginId, scope = {}) {
|
|
158
|
+
await this.ready();
|
|
159
|
+
const tenantId = this.resolveTenantId(scope);
|
|
160
|
+
const plugin = this.require(pluginId);
|
|
161
|
+
const installation = await this.database.load(plugin.id, { tenantId });
|
|
162
|
+
return {
|
|
163
|
+
...plugin.toManifest(),
|
|
164
|
+
installation: installation
|
|
165
|
+
? toInstallationSummary(installation)
|
|
166
|
+
: undefined,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async install(pluginId, config, options = {}) {
|
|
170
|
+
await this.ready();
|
|
171
|
+
const tenantId = this.resolveTenantId(options);
|
|
172
|
+
const plugin = this.require(pluginId);
|
|
173
|
+
const normalizedConfig = plugin.applyDefaults(config);
|
|
174
|
+
await plugin.assertValidConfig(normalizedConfig);
|
|
175
|
+
const now = new Date().toISOString();
|
|
176
|
+
const existing = await this.database.load(plugin.id, { tenantId });
|
|
177
|
+
const installation = {
|
|
178
|
+
id: storageKey(plugin.id, tenantId),
|
|
179
|
+
pluginId: plugin.id,
|
|
180
|
+
tenantId,
|
|
181
|
+
enabled: options.enabled ?? existing?.enabled ?? true,
|
|
182
|
+
config: await this.sealConfig(plugin, normalizedConfig, { tenantId }),
|
|
183
|
+
version: plugin.version,
|
|
184
|
+
meta: options.meta ?? existing?.meta,
|
|
185
|
+
createdAt: existing?.createdAt ?? now,
|
|
186
|
+
updatedAt: now,
|
|
187
|
+
};
|
|
188
|
+
const saved = (await this.database.save(installation)) ?? installation;
|
|
189
|
+
await plugin.definition.lifecycle?.onInstall?.({
|
|
190
|
+
plugin,
|
|
191
|
+
manager: this,
|
|
192
|
+
installation: saved,
|
|
193
|
+
config: normalizedConfig,
|
|
194
|
+
});
|
|
195
|
+
return saved;
|
|
196
|
+
}
|
|
197
|
+
async updateConfig(pluginId, config, options = {}) {
|
|
198
|
+
await this.ready();
|
|
199
|
+
const tenantId = this.resolveTenantId(options);
|
|
200
|
+
const plugin = this.require(pluginId);
|
|
201
|
+
const current = await this.database.load(plugin.id, { tenantId });
|
|
202
|
+
if (!current) {
|
|
203
|
+
throw new PluginNotInstalledError(plugin.id);
|
|
204
|
+
}
|
|
205
|
+
const currentConfig = await this.revealConfig(plugin, current.config, {
|
|
206
|
+
tenantId,
|
|
207
|
+
});
|
|
208
|
+
const normalizedConfig = plugin.applyDefaults(options.merge === false
|
|
209
|
+
? config
|
|
210
|
+
: {
|
|
211
|
+
...currentConfig,
|
|
212
|
+
...config,
|
|
213
|
+
});
|
|
214
|
+
await plugin.assertValidConfig(normalizedConfig);
|
|
215
|
+
const patch = {
|
|
216
|
+
config: await this.sealConfig(plugin, normalizedConfig, { tenantId }),
|
|
217
|
+
meta: options.meta ?? current.meta,
|
|
218
|
+
updatedAt: new Date().toISOString(),
|
|
219
|
+
};
|
|
220
|
+
const saved = await this.savePatch(plugin.id, patch, { tenantId });
|
|
221
|
+
await plugin.definition.lifecycle?.onUpdate?.({
|
|
222
|
+
plugin,
|
|
223
|
+
manager: this,
|
|
224
|
+
installation: saved,
|
|
225
|
+
config: normalizedConfig,
|
|
226
|
+
});
|
|
227
|
+
return saved;
|
|
228
|
+
}
|
|
229
|
+
async enable(pluginId, scope = {}) {
|
|
230
|
+
await this.ready();
|
|
231
|
+
const tenantId = this.resolveTenantId(scope);
|
|
232
|
+
const plugin = this.require(pluginId);
|
|
233
|
+
const installation = await this.savePatch(plugin.id, { enabled: true }, { tenantId });
|
|
234
|
+
await plugin.definition.lifecycle?.onEnable?.({
|
|
235
|
+
plugin,
|
|
236
|
+
manager: this,
|
|
237
|
+
installation,
|
|
238
|
+
config: await this.getInstallationConfig(plugin.id, { tenantId }),
|
|
239
|
+
});
|
|
240
|
+
return installation;
|
|
241
|
+
}
|
|
242
|
+
async disable(pluginId, scope = {}) {
|
|
243
|
+
await this.ready();
|
|
244
|
+
const tenantId = this.resolveTenantId(scope);
|
|
245
|
+
const plugin = this.require(pluginId);
|
|
246
|
+
const installation = await this.savePatch(plugin.id, { enabled: false }, { tenantId });
|
|
247
|
+
await plugin.definition.lifecycle?.onDisable?.({
|
|
248
|
+
plugin,
|
|
249
|
+
manager: this,
|
|
250
|
+
installation,
|
|
251
|
+
config: await this.getInstallationConfig(plugin.id, { tenantId }),
|
|
252
|
+
});
|
|
253
|
+
return installation;
|
|
254
|
+
}
|
|
255
|
+
async uninstall(pluginId, scope = {}) {
|
|
256
|
+
await this.ready();
|
|
257
|
+
const tenantId = this.resolveTenantId(scope);
|
|
258
|
+
const plugin = this.require(pluginId);
|
|
259
|
+
const installation = await this.database.load(plugin.id, { tenantId });
|
|
260
|
+
if (installation) {
|
|
261
|
+
await plugin.definition.lifecycle?.onUninstall?.({
|
|
262
|
+
plugin,
|
|
263
|
+
manager: this,
|
|
264
|
+
installation,
|
|
265
|
+
config: await this.revealConfig(plugin, installation.config, {
|
|
266
|
+
tenantId,
|
|
267
|
+
}),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (this.database.delete) {
|
|
271
|
+
await this.database.delete(plugin.id, { tenantId });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
await this.savePatch(plugin.id, { enabled: false }, { tenantId });
|
|
275
|
+
}
|
|
276
|
+
async getInstallation(pluginId, scope = {}) {
|
|
277
|
+
await this.ready();
|
|
278
|
+
return this.database.load(pluginId, {
|
|
279
|
+
tenantId: this.resolveTenantId(scope),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
async getInstallationConfig(pluginId, scope = {}) {
|
|
283
|
+
await this.ready();
|
|
284
|
+
const tenantId = this.resolveTenantId(scope);
|
|
285
|
+
const plugin = this.require(pluginId);
|
|
286
|
+
const installation = await this.database.load(pluginId, { tenantId });
|
|
287
|
+
if (!installation) {
|
|
288
|
+
return {};
|
|
289
|
+
}
|
|
290
|
+
return this.revealConfig(plugin, installation.config, { tenantId });
|
|
291
|
+
}
|
|
292
|
+
getManifest(pluginId) {
|
|
293
|
+
return this.require(pluginId).toManifest();
|
|
294
|
+
}
|
|
295
|
+
getAdditionalMetaTags(pluginId) {
|
|
296
|
+
if (pluginId) {
|
|
297
|
+
return this.require(pluginId).definition.additionalMetaTags ?? [];
|
|
298
|
+
}
|
|
299
|
+
return [...this.registry.values()].flatMap((plugin) => plugin.definition.additionalMetaTags ?? []);
|
|
300
|
+
}
|
|
301
|
+
getFrontendComponents(slot) {
|
|
302
|
+
return [...this.registry.values()].flatMap((plugin) => {
|
|
303
|
+
const components = plugin.definition.frontend?.components ?? [];
|
|
304
|
+
return components
|
|
305
|
+
.filter((component) => !slot || component.slot === slot)
|
|
306
|
+
.map((component) => ({
|
|
307
|
+
...component,
|
|
308
|
+
pluginId: plugin.id,
|
|
309
|
+
}));
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
async getInjections(options = {}) {
|
|
313
|
+
await this.ready();
|
|
314
|
+
const tenantId = this.resolveTenantId(options);
|
|
315
|
+
const scope = { tenantId };
|
|
316
|
+
const installedOnly = options.installedOnly ?? true;
|
|
317
|
+
const enabledOnly = options.enabledOnly ?? true;
|
|
318
|
+
const installations = await this.listInstallations(scope);
|
|
319
|
+
const installationByPluginId = new Map(installations.map((installation) => [installation.pluginId, installation]));
|
|
320
|
+
const url = resolveInjectionUrl(options);
|
|
321
|
+
const path = options.path ?? url?.pathname;
|
|
322
|
+
const resolved = [];
|
|
323
|
+
for (const plugin of this.registry.values()) {
|
|
324
|
+
const installation = installationByPluginId.get(plugin.id) ?? null;
|
|
325
|
+
if (installedOnly && !installation) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (enabledOnly && !installation?.enabled) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const config = installation
|
|
332
|
+
? await this.revealConfig(plugin, installation.config, scope)
|
|
333
|
+
: plugin.applyDefaults({});
|
|
334
|
+
for (const injection of plugin.definition.injections ?? []) {
|
|
335
|
+
if (!matchesInjectionQuery(injection, {
|
|
336
|
+
...options,
|
|
337
|
+
path,
|
|
338
|
+
area: options.area,
|
|
339
|
+
placement: options.placement,
|
|
340
|
+
installed: Boolean(installation),
|
|
341
|
+
enabled: installation?.enabled ?? false,
|
|
342
|
+
tenantId,
|
|
343
|
+
config,
|
|
344
|
+
})) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const rendered = await renderPluginInjection(injection, {
|
|
348
|
+
plugin,
|
|
349
|
+
manager: this,
|
|
350
|
+
installation,
|
|
351
|
+
config,
|
|
352
|
+
tenantId,
|
|
353
|
+
request: options.request,
|
|
354
|
+
url,
|
|
355
|
+
path,
|
|
356
|
+
area: options.area,
|
|
357
|
+
placement: injection.placement,
|
|
358
|
+
nonce: options.nonce,
|
|
359
|
+
locals: options.locals ?? {},
|
|
360
|
+
});
|
|
361
|
+
if (!rendered?.html) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
resolved.push({
|
|
365
|
+
...serializeInjection(injection),
|
|
366
|
+
pluginId: plugin.id,
|
|
367
|
+
pluginName: plugin.name,
|
|
368
|
+
html: rendered.html,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return resolved.sort(sortResolvedInjections);
|
|
373
|
+
}
|
|
374
|
+
async renderInjections(options = {}) {
|
|
375
|
+
return renderInjectionHtml(await this.getInjections(options), options);
|
|
376
|
+
}
|
|
377
|
+
async injectHtml(html, options = {}) {
|
|
378
|
+
return injectHtmlString(html, await this.getInjections(options), options);
|
|
379
|
+
}
|
|
380
|
+
async ready() {
|
|
381
|
+
if (!this.shouldSyncMarketplaces()) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
await this.syncMarketplaces({ force: this.isMarketplaceSyncStale() });
|
|
385
|
+
}
|
|
386
|
+
async syncMarketplaces(options = {}) {
|
|
387
|
+
if (!this.marketplaces.length) {
|
|
388
|
+
this.marketplacesSynced = true;
|
|
389
|
+
this.marketplacesSyncedAt = Date.now();
|
|
390
|
+
return { loaded: 0, marketplaces: 0 };
|
|
391
|
+
}
|
|
392
|
+
if (!options.force && this.marketplaceSyncPromise) {
|
|
393
|
+
return this.marketplaceSyncPromise;
|
|
394
|
+
}
|
|
395
|
+
if (!options.force && this.marketplacesSynced && !this.isMarketplaceSyncStale()) {
|
|
396
|
+
return {
|
|
397
|
+
loaded: 0,
|
|
398
|
+
marketplaces: this.marketplaces.length,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
this.marketplaceSyncPromise = this.loadMarketplaces();
|
|
402
|
+
try {
|
|
403
|
+
return await this.marketplaceSyncPromise;
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
this.marketplaceSyncPromise = undefined;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async loadMarketplaces() {
|
|
410
|
+
let loaded = 0;
|
|
411
|
+
for (const marketplace of this.marketplaces) {
|
|
412
|
+
loaded += await this.loadMarketplace(marketplace);
|
|
413
|
+
}
|
|
414
|
+
this.marketplacesSynced = true;
|
|
415
|
+
this.marketplacesSyncedAt = Date.now();
|
|
416
|
+
return {
|
|
417
|
+
loaded,
|
|
418
|
+
marketplaces: this.marketplaces.length,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
async loadMarketplace(marketplaceSource) {
|
|
422
|
+
if (!this.fetcher) {
|
|
423
|
+
throw new PluginManagerError("No fetch implementation is available.");
|
|
424
|
+
}
|
|
425
|
+
const candidates = marketplaceCandidates(marketplaceSource, this.github);
|
|
426
|
+
let lastError;
|
|
427
|
+
let successfulCandidates = 0;
|
|
428
|
+
const pluginsById = new Map();
|
|
429
|
+
for (const candidate of candidates) {
|
|
430
|
+
try {
|
|
431
|
+
const response = await this.fetcher(candidate.url, {
|
|
432
|
+
headers: candidate.headers,
|
|
433
|
+
});
|
|
434
|
+
if (!response.ok) {
|
|
435
|
+
lastError = new Error(`${response.status} ${response.statusText}`);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
successfulCandidates += 1;
|
|
439
|
+
if (candidate.parser === "github-tree") {
|
|
440
|
+
const pluginFileCandidates = await readGitHubPluginFileCandidates(response, candidate);
|
|
441
|
+
for (const pluginFileCandidate of pluginFileCandidates) {
|
|
442
|
+
try {
|
|
443
|
+
const pluginFileResponse = await this.fetcher(pluginFileCandidate.url, {
|
|
444
|
+
headers: pluginFileCandidate.headers,
|
|
445
|
+
});
|
|
446
|
+
if (!pluginFileResponse.ok) {
|
|
447
|
+
lastError = new Error(`${pluginFileResponse.status} ${pluginFileResponse.statusText}`);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
successfulCandidates += 1;
|
|
451
|
+
const payload = await readMarketplacePayload(pluginFileResponse, pluginFileCandidate);
|
|
452
|
+
for (const plugin of extractMarketplacePlugins(payload)) {
|
|
453
|
+
const normalizedPlugin = applyMarketplaceDefaults(plugin, pluginFileCandidate);
|
|
454
|
+
pluginsById.set(normalizedPlugin.id, normalizedPlugin);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
lastError = error;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const payload = await readMarketplacePayload(response, candidate);
|
|
464
|
+
for (const plugin of extractMarketplacePlugins(payload)) {
|
|
465
|
+
const normalizedPlugin = applyMarketplaceDefaults(plugin, candidate);
|
|
466
|
+
pluginsById.set(normalizedPlugin.id, normalizedPlugin);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
lastError = error;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (pluginsById.size) {
|
|
474
|
+
this.register([...pluginsById.values()]);
|
|
475
|
+
return pluginsById.size;
|
|
476
|
+
}
|
|
477
|
+
if (successfulCandidates > 0) {
|
|
478
|
+
return 0;
|
|
479
|
+
}
|
|
480
|
+
throw new PluginManagerError(`Marketplace "${marketplaceLabel(marketplaceSource)}" could not be loaded: ${String(lastError)}`);
|
|
481
|
+
}
|
|
482
|
+
shouldSyncMarketplaces() {
|
|
483
|
+
return Boolean(this.autoSyncMarketplaces &&
|
|
484
|
+
this.marketplaces.length &&
|
|
485
|
+
(!this.marketplacesSynced || this.isMarketplaceSyncStale()));
|
|
486
|
+
}
|
|
487
|
+
isMarketplaceSyncStale() {
|
|
488
|
+
if (!this.marketplaceRefreshIntervalMs || !this.marketplacesSyncedAt) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
return Date.now() - this.marketplacesSyncedAt >= this.marketplaceRefreshIntervalMs;
|
|
492
|
+
}
|
|
493
|
+
async listInstallations(scope) {
|
|
494
|
+
if (this.database.list) {
|
|
495
|
+
return this.database.list(scope);
|
|
496
|
+
}
|
|
497
|
+
const installations = await Promise.all([...this.registry.keys()].map((pluginId) => this.database.load(pluginId, scope)));
|
|
498
|
+
return installations.filter(Boolean);
|
|
499
|
+
}
|
|
500
|
+
async savePatch(pluginId, patch, scope) {
|
|
501
|
+
const current = await this.database.load(pluginId, scope);
|
|
502
|
+
if (!current) {
|
|
503
|
+
throw new PluginNotInstalledError(pluginId);
|
|
504
|
+
}
|
|
505
|
+
if (this.database.update) {
|
|
506
|
+
const updated = await this.database.update(pluginId, patch, scope);
|
|
507
|
+
if (updated) {
|
|
508
|
+
return updated;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const merged = {
|
|
512
|
+
...current,
|
|
513
|
+
...patch,
|
|
514
|
+
pluginId,
|
|
515
|
+
tenantId: scope.tenantId,
|
|
516
|
+
updatedAt: new Date().toISOString(),
|
|
517
|
+
};
|
|
518
|
+
await this.database.save(merged);
|
|
519
|
+
return merged;
|
|
520
|
+
}
|
|
521
|
+
resolveTenantId(scope = {}) {
|
|
522
|
+
return scope.tenantId ?? this.defaultTenantId;
|
|
523
|
+
}
|
|
524
|
+
async sealConfig(plugin, config, scope) {
|
|
525
|
+
const sealed = {};
|
|
526
|
+
for (const [key, value] of Object.entries(config)) {
|
|
527
|
+
const field = plugin.fields.find((candidate) => candidate.key === key);
|
|
528
|
+
if (field && shouldEncrypt(plugin, field) && this.encryption && value != null) {
|
|
529
|
+
sealed[key] = {
|
|
530
|
+
__voilabsEncrypted: true,
|
|
531
|
+
value: await this.encryption.encrypt(String(value), {
|
|
532
|
+
pluginId: plugin.id,
|
|
533
|
+
fieldKey: key,
|
|
534
|
+
tenantId: scope.tenantId,
|
|
535
|
+
}),
|
|
536
|
+
};
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
sealed[key] = value;
|
|
540
|
+
}
|
|
541
|
+
return sealed;
|
|
542
|
+
}
|
|
543
|
+
async revealConfig(plugin, config, scope) {
|
|
544
|
+
const revealed = {};
|
|
545
|
+
for (const [key, value] of Object.entries(config)) {
|
|
546
|
+
if (isEncryptedConfigValue(value) && this.encryption) {
|
|
547
|
+
revealed[key] = await this.encryption.decrypt(value.value, {
|
|
548
|
+
pluginId: plugin.id,
|
|
549
|
+
fieldKey: key,
|
|
550
|
+
tenantId: scope.tenantId,
|
|
551
|
+
});
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
revealed[key] = value;
|
|
555
|
+
}
|
|
556
|
+
return revealed;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function storageKey(pluginId, tenantId) {
|
|
560
|
+
return `${tenantId ?? "default"}:${pluginId}`;
|
|
561
|
+
}
|
|
562
|
+
function toInstallationSummary(installation) {
|
|
563
|
+
return {
|
|
564
|
+
installed: true,
|
|
565
|
+
enabled: installation.enabled,
|
|
566
|
+
pluginId: installation.pluginId,
|
|
567
|
+
tenantId: installation.tenantId,
|
|
568
|
+
version: installation.version,
|
|
569
|
+
meta: installation.meta,
|
|
570
|
+
createdAt: installation.createdAt,
|
|
571
|
+
updatedAt: installation.updatedAt,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function shouldEncrypt(plugin, field) {
|
|
575
|
+
return Boolean(plugin.definition.needEncryption ||
|
|
576
|
+
field.encrypt ||
|
|
577
|
+
field.secret ||
|
|
578
|
+
field.type === "password" ||
|
|
579
|
+
field.type === "secret");
|
|
580
|
+
}
|
|
581
|
+
function isEncryptedConfigValue(value) {
|
|
582
|
+
return Boolean(value &&
|
|
583
|
+
typeof value === "object" &&
|
|
584
|
+
value.__voilabsEncrypted === true &&
|
|
585
|
+
typeof value.value === "string");
|
|
586
|
+
}
|
|
587
|
+
function matchesInjectionQuery(injection, context) {
|
|
588
|
+
if (injection.enabled === false) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
if (!placementMatches(injection.placement, context.placement)) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
if (!areaMatches(injection, context.area)) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
const condition = injection.condition;
|
|
598
|
+
if (!condition) {
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
if (condition.installedOnly && !context.installed) {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
if (condition.enabledOnly && !context.enabled) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
if (condition.tenantIds?.length &&
|
|
608
|
+
(!context.tenantId || !condition.tenantIds.includes(context.tenantId))) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
if (condition.paths?.length && !matchesAnyPath(context.path, condition.paths)) {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
if (condition.excludePaths?.length &&
|
|
615
|
+
matchesAnyPath(context.path, condition.excludePaths)) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
if (condition.config && !matchesConfig(context.config, condition.config)) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
function placementMatches(injectionPlacement, requested) {
|
|
624
|
+
if (!requested) {
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
if (injectionPlacement === requested) {
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
const aliases = {
|
|
631
|
+
head: ["head", "head:start", "head:end", "admin:head", "public:head"],
|
|
632
|
+
body: ["body", "body:start", "body:end", "admin:body", "public:body"],
|
|
633
|
+
footer: ["footer", "admin:footer", "public:footer"],
|
|
634
|
+
};
|
|
635
|
+
return aliases[requested]?.includes(injectionPlacement) ?? false;
|
|
636
|
+
}
|
|
637
|
+
function areaMatches(injection, area) {
|
|
638
|
+
const placementArea = injection.placement.includes(":")
|
|
639
|
+
? injection.placement.split(":")[0]
|
|
640
|
+
: undefined;
|
|
641
|
+
const conditionArea = injection.condition?.area;
|
|
642
|
+
if (conditionArea) {
|
|
643
|
+
const allowedAreas = Array.isArray(conditionArea)
|
|
644
|
+
? conditionArea
|
|
645
|
+
: [conditionArea];
|
|
646
|
+
if (!area || !allowedAreas.includes(area)) {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (!placementArea || ["head", "body", "footer"].includes(placementArea)) {
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
return area === placementArea;
|
|
654
|
+
}
|
|
655
|
+
function matchesAnyPath(path, patterns) {
|
|
656
|
+
if (!path) {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
return patterns.some((pattern) => matchPathPattern(path, pattern));
|
|
660
|
+
}
|
|
661
|
+
function matchPathPattern(path, pattern) {
|
|
662
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
663
|
+
const normalizedPattern = pattern.startsWith("/") ? pattern : `/${pattern}`;
|
|
664
|
+
const regex = new RegExp(`^${normalizedPattern
|
|
665
|
+
.split("*")
|
|
666
|
+
.map(escapeRegex)
|
|
667
|
+
.join(".*")}$`);
|
|
668
|
+
return regex.test(normalizedPath);
|
|
669
|
+
}
|
|
670
|
+
function matchesConfig(config, expected) {
|
|
671
|
+
return Object.entries(expected).every(([key, value]) => config[key] === value);
|
|
672
|
+
}
|
|
673
|
+
function resolveInjectionUrl(options) {
|
|
674
|
+
if (options.url instanceof URL) {
|
|
675
|
+
return options.url;
|
|
676
|
+
}
|
|
677
|
+
if (typeof options.url === "string") {
|
|
678
|
+
return new URL(options.url, "http://voilabs.local");
|
|
679
|
+
}
|
|
680
|
+
if (options.request) {
|
|
681
|
+
return new URL(options.request.url);
|
|
682
|
+
}
|
|
683
|
+
return undefined;
|
|
684
|
+
}
|
|
685
|
+
function serializeInjection(injection) {
|
|
686
|
+
const { render: _render, ...serializable } = injection;
|
|
687
|
+
return serializable;
|
|
688
|
+
}
|
|
689
|
+
function sortResolvedInjections(left, right) {
|
|
690
|
+
return (left.order ?? 0) - (right.order ?? 0) || left.key.localeCompare(right.key);
|
|
691
|
+
}
|
|
692
|
+
function escapeRegex(value) {
|
|
693
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
694
|
+
}
|
|
695
|
+
function marketplaceCandidates(input, defaults) {
|
|
696
|
+
if (typeof input === "string") {
|
|
697
|
+
const githubLocation = parseGitHubLocation(input);
|
|
698
|
+
if (githubLocation) {
|
|
699
|
+
return githubMarketplaceCandidates(githubLocation, defaults);
|
|
700
|
+
}
|
|
701
|
+
return [
|
|
702
|
+
{
|
|
703
|
+
url: input,
|
|
704
|
+
parser: "json",
|
|
705
|
+
},
|
|
706
|
+
];
|
|
707
|
+
}
|
|
708
|
+
if ("url" in input) {
|
|
709
|
+
return [
|
|
710
|
+
{
|
|
711
|
+
url: input.url,
|
|
712
|
+
headers: mergeHeaders(defaults.headers, input.headers),
|
|
713
|
+
parser: "json",
|
|
714
|
+
},
|
|
715
|
+
];
|
|
716
|
+
}
|
|
717
|
+
const repository = input.github ?? input.repository;
|
|
718
|
+
const parsed = repository ? parseGitHubLocation(repository) : null;
|
|
719
|
+
const owner = input.owner ?? parsed?.owner;
|
|
720
|
+
const repo = input.repo ?? parsed?.repo;
|
|
721
|
+
if (!owner || !repo) {
|
|
722
|
+
return [];
|
|
723
|
+
}
|
|
724
|
+
return githubMarketplaceCandidates({
|
|
725
|
+
owner,
|
|
726
|
+
repo,
|
|
727
|
+
branch: input.branch ?? parsed?.branch,
|
|
728
|
+
path: input.path ?? parsed?.path,
|
|
729
|
+
exactPath: parsed?.exactPath,
|
|
730
|
+
}, {
|
|
731
|
+
...defaults,
|
|
732
|
+
...input,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
function githubMarketplaceCandidates(location, options) {
|
|
736
|
+
const rawBaseUrl = trimTrailingSlash(options.rawBaseUrl ?? "https://raw.githubusercontent.com");
|
|
737
|
+
const apiBaseUrl = trimTrailingSlash(options.apiBaseUrl ?? "https://api.github.com");
|
|
738
|
+
const defaultFiles = options.files ?? [
|
|
739
|
+
"marketplace.json",
|
|
740
|
+
"plugins.json",
|
|
741
|
+
"index.json",
|
|
742
|
+
];
|
|
743
|
+
const pluginFileExtensions = normalizePluginFileExtensions(options.pluginFileExtensions);
|
|
744
|
+
const branchWasExplicit = Boolean(location.branch ?? options.branch);
|
|
745
|
+
const branches = branchWasExplicit
|
|
746
|
+
? [location.branch ?? options.branch ?? "main"]
|
|
747
|
+
: ["main", "master"];
|
|
748
|
+
const paths = location.exactPath && location.path
|
|
749
|
+
? [location.path]
|
|
750
|
+
: defaultFiles.map((file) => joinUrlPath(location.path, file));
|
|
751
|
+
const headers = mergeHeaders(options.headers, options.token
|
|
752
|
+
? {
|
|
753
|
+
accept: "application/vnd.github+json",
|
|
754
|
+
authorization: `Bearer ${options.token}`,
|
|
755
|
+
}
|
|
756
|
+
: undefined);
|
|
757
|
+
const candidates = [];
|
|
758
|
+
for (const branch of branches) {
|
|
759
|
+
if (!location.exactPath && options.pluginFiles !== false) {
|
|
760
|
+
candidates.push({
|
|
761
|
+
url: `${apiBaseUrl}/repos/${location.owner}/${location.repo}/git/trees/${encodeURIComponent(branch)}?recursive=${options.recursivePluginFiles === false ? "0" : "1"}`,
|
|
762
|
+
headers,
|
|
763
|
+
parser: "github-tree",
|
|
764
|
+
github: {
|
|
765
|
+
owner: location.owner,
|
|
766
|
+
repo: location.repo,
|
|
767
|
+
branch,
|
|
768
|
+
pathPrefix: normalizePathPrefix(location.path),
|
|
769
|
+
rawBaseUrl,
|
|
770
|
+
apiBaseUrl,
|
|
771
|
+
extensions: pluginFileExtensions,
|
|
772
|
+
avatarSize: options.avatarSize ?? 128,
|
|
773
|
+
headers,
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
for (const path of paths) {
|
|
778
|
+
const normalizedPath = path.replace(/^\/+/, "");
|
|
779
|
+
candidates.push({
|
|
780
|
+
url: `${rawBaseUrl}/${location.owner}/${location.repo}/${branch}/${normalizedPath}`,
|
|
781
|
+
headers,
|
|
782
|
+
parser: "json",
|
|
783
|
+
github: {
|
|
784
|
+
owner: location.owner,
|
|
785
|
+
repo: location.repo,
|
|
786
|
+
branch,
|
|
787
|
+
pathPrefix: normalizePathPrefix(location.path),
|
|
788
|
+
rawBaseUrl,
|
|
789
|
+
apiBaseUrl,
|
|
790
|
+
extensions: pluginFileExtensions,
|
|
791
|
+
avatarSize: options.avatarSize ?? 128,
|
|
792
|
+
headers,
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
candidates.push({
|
|
796
|
+
url: `${apiBaseUrl}/repos/${location.owner}/${location.repo}/contents/${encodeUrlPath(normalizedPath)}?ref=${encodeURIComponent(branch)}`,
|
|
797
|
+
headers,
|
|
798
|
+
parser: "github-content",
|
|
799
|
+
github: {
|
|
800
|
+
owner: location.owner,
|
|
801
|
+
repo: location.repo,
|
|
802
|
+
branch,
|
|
803
|
+
pathPrefix: normalizePathPrefix(location.path),
|
|
804
|
+
rawBaseUrl,
|
|
805
|
+
apiBaseUrl,
|
|
806
|
+
extensions: pluginFileExtensions,
|
|
807
|
+
avatarSize: options.avatarSize ?? 128,
|
|
808
|
+
headers,
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return candidates;
|
|
814
|
+
}
|
|
815
|
+
function parseGitHubLocation(input) {
|
|
816
|
+
const normalized = input.replace(/^github:/, "").replace(/\.git$/, "");
|
|
817
|
+
try {
|
|
818
|
+
const url = new URL(normalized);
|
|
819
|
+
const hostname = url.hostname.toLocaleLowerCase();
|
|
820
|
+
if (hostname === "raw.githubusercontent.com") {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
if (hostname !== "github.com" && hostname !== "www.github.com") {
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
const [owner, repo, marker, branch, ...rest] = url.pathname
|
|
827
|
+
.split("/")
|
|
828
|
+
.filter(Boolean);
|
|
829
|
+
if (!owner || !repo) {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
if ((marker === "blob" || marker === "tree") && branch) {
|
|
833
|
+
return {
|
|
834
|
+
owner,
|
|
835
|
+
repo: repo.replace(/\.git$/, ""),
|
|
836
|
+
branch,
|
|
837
|
+
path: rest.join("/") || undefined,
|
|
838
|
+
exactPath: marker === "blob",
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
return {
|
|
842
|
+
owner,
|
|
843
|
+
repo: repo.replace(/\.git$/, ""),
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
const [owner, repo] = normalized.split("/").filter(Boolean);
|
|
848
|
+
if (!owner || !repo) {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
return {
|
|
852
|
+
owner,
|
|
853
|
+
repo: repo.replace(/\.git$/, ""),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
async function readMarketplacePayload(response, candidate) {
|
|
858
|
+
if (candidate.parser === "github-tree") {
|
|
859
|
+
throw new PluginManagerError(`GitHub tree response must be expanded before parsing ${candidate.url}.`);
|
|
860
|
+
}
|
|
861
|
+
if (candidate.parser === "json") {
|
|
862
|
+
return response.json();
|
|
863
|
+
}
|
|
864
|
+
const payload = (await response.json());
|
|
865
|
+
if (!payload.content || payload.encoding !== "base64") {
|
|
866
|
+
throw new PluginManagerError(`GitHub contents response did not include base64 JSON content from ${candidate.url}.`);
|
|
867
|
+
}
|
|
868
|
+
return JSON.parse(decodeBase64(payload.content));
|
|
869
|
+
}
|
|
870
|
+
function extractMarketplacePlugins(payload) {
|
|
871
|
+
if (Array.isArray(payload)) {
|
|
872
|
+
return payload;
|
|
873
|
+
}
|
|
874
|
+
if (isMarketplacePluginDefinition(payload)) {
|
|
875
|
+
return [payload];
|
|
876
|
+
}
|
|
877
|
+
return payload.plugin
|
|
878
|
+
? [payload.plugin, ...(payload.plugins ?? payload.marketplace?.plugins ?? [])]
|
|
879
|
+
: payload.plugins ?? payload.marketplace?.plugins ?? [];
|
|
880
|
+
}
|
|
881
|
+
async function readGitHubPluginFileCandidates(response, candidate) {
|
|
882
|
+
if (!candidate.github) {
|
|
883
|
+
return [];
|
|
884
|
+
}
|
|
885
|
+
const discovery = candidate.github;
|
|
886
|
+
const payload = (await response.json());
|
|
887
|
+
return (payload.tree ?? [])
|
|
888
|
+
.filter((item) => Boolean(item.path && item.type === "blob"))
|
|
889
|
+
.filter((item) => isPluginFilePath(item.path, discovery))
|
|
890
|
+
.flatMap((item) => {
|
|
891
|
+
const encodedPath = encodeUrlPath(item.path);
|
|
892
|
+
return [
|
|
893
|
+
{
|
|
894
|
+
url: `${discovery.rawBaseUrl}/${discovery.owner}/${discovery.repo}/${discovery.branch}/${encodedPath}`,
|
|
895
|
+
headers: discovery.headers,
|
|
896
|
+
parser: "json",
|
|
897
|
+
github: discovery,
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
url: `${discovery.apiBaseUrl}/repos/${discovery.owner}/${discovery.repo}/contents/${encodedPath}?ref=${encodeURIComponent(discovery.branch)}`,
|
|
901
|
+
headers: discovery.headers,
|
|
902
|
+
parser: "github-content",
|
|
903
|
+
github: discovery,
|
|
904
|
+
},
|
|
905
|
+
];
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
function isMarketplacePluginDefinition(value) {
|
|
909
|
+
return Boolean(value &&
|
|
910
|
+
typeof value === "object" &&
|
|
911
|
+
typeof value.id === "string" &&
|
|
912
|
+
typeof value.name === "string" &&
|
|
913
|
+
typeof value.summary === "string" &&
|
|
914
|
+
typeof value.category === "string");
|
|
915
|
+
}
|
|
916
|
+
function applyMarketplaceDefaults(plugin, candidate) {
|
|
917
|
+
const github = candidate.github;
|
|
918
|
+
if (!github) {
|
|
919
|
+
if (!plugin.provider) {
|
|
920
|
+
throw new PluginManagerError(`Marketplace plugin "${plugin.id}" must define provider when it is not loaded from GitHub.`);
|
|
921
|
+
}
|
|
922
|
+
return plugin;
|
|
923
|
+
}
|
|
924
|
+
return {
|
|
925
|
+
...plugin,
|
|
926
|
+
provider: github.owner,
|
|
927
|
+
iconUrl: githubAvatarUrl(github.owner, github.avatarSize),
|
|
928
|
+
repositoryUrl: plugin.repositoryUrl ??
|
|
929
|
+
`https://github.com/${github.owner}/${github.repo}`,
|
|
930
|
+
meta: {
|
|
931
|
+
...plugin.meta,
|
|
932
|
+
github: {
|
|
933
|
+
owner: github.owner,
|
|
934
|
+
repo: github.repo,
|
|
935
|
+
branch: github.branch,
|
|
936
|
+
},
|
|
937
|
+
},
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
function isPluginFilePath(path, discovery) {
|
|
941
|
+
const normalizedPath = path.replace(/^\/+/, "");
|
|
942
|
+
const prefix = discovery.pathPrefix;
|
|
943
|
+
if (prefix &&
|
|
944
|
+
normalizedPath !== prefix &&
|
|
945
|
+
!normalizedPath.startsWith(`${prefix}/`)) {
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
return discovery.extensions.some((extension) => normalizedPath.toLocaleLowerCase().endsWith(extension));
|
|
949
|
+
}
|
|
950
|
+
function marketplaceLabel(source) {
|
|
951
|
+
if (typeof source === "string") {
|
|
952
|
+
return source;
|
|
953
|
+
}
|
|
954
|
+
if ("url" in source) {
|
|
955
|
+
return source.url;
|
|
956
|
+
}
|
|
957
|
+
return source.github ?? source.repository ?? `${source.owner}/${source.repo}`;
|
|
958
|
+
}
|
|
959
|
+
function mergeHeaders(...headersList) {
|
|
960
|
+
const headers = new Headers();
|
|
961
|
+
for (const headersInit of headersList) {
|
|
962
|
+
if (!headersInit) {
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
new Headers(headersInit).forEach((value, key) => {
|
|
966
|
+
headers.set(key, value);
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
let hasHeaders = false;
|
|
970
|
+
headers.forEach(() => {
|
|
971
|
+
hasHeaders = true;
|
|
972
|
+
});
|
|
973
|
+
return hasHeaders ? headers : undefined;
|
|
974
|
+
}
|
|
975
|
+
function decodeBase64(value) {
|
|
976
|
+
const normalized = value.replace(/\s/g, "");
|
|
977
|
+
const buffer = globalThis.Buffer;
|
|
978
|
+
if (buffer) {
|
|
979
|
+
return buffer.from(normalized, "base64").toString("utf8");
|
|
980
|
+
}
|
|
981
|
+
const binary = atob(normalized);
|
|
982
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
983
|
+
return new TextDecoder().decode(bytes);
|
|
984
|
+
}
|
|
985
|
+
function encodeUrlPath(path) {
|
|
986
|
+
return path.split("/").map(encodeURIComponent).join("/");
|
|
987
|
+
}
|
|
988
|
+
function joinUrlPath(base, path) {
|
|
989
|
+
return [base, path].filter(Boolean).join("/");
|
|
990
|
+
}
|
|
991
|
+
function normalizePathPrefix(path) {
|
|
992
|
+
const normalized = path?.replace(/^\/+|\/+$/g, "");
|
|
993
|
+
return normalized || undefined;
|
|
994
|
+
}
|
|
995
|
+
function normalizePluginFileExtensions(extensions) {
|
|
996
|
+
return (extensions?.length ? extensions : [".plugin"]).map((extension) => extension.startsWith(".")
|
|
997
|
+
? extension.toLocaleLowerCase()
|
|
998
|
+
: `.${extension.toLocaleLowerCase()}`);
|
|
999
|
+
}
|
|
1000
|
+
function githubAvatarUrl(owner, size) {
|
|
1001
|
+
return `https://github.com/${owner}.png?size=${size}`;
|
|
1002
|
+
}
|
|
1003
|
+
function trimTrailingSlash(value) {
|
|
1004
|
+
return value.replace(/\/+$/, "");
|
|
1005
|
+
}
|
|
1006
|
+
//# sourceMappingURL=manager.js.map
|