fluxplugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +50 -0
  3. package/dist/browser/chunks/chunk-Q75AOCXD.js +2544 -0
  4. package/dist/browser/chunks/chunk-ZZ25CRMR.js +759 -0
  5. package/dist/browser/index.js +138 -0
  6. package/dist/browser/manager-ui.js +165 -0
  7. package/dist/browser/react.js +73 -0
  8. package/dist/cli.js +1635 -0
  9. package/dist/node/chunks/chunk-BZ7NZF7I.js +990 -0
  10. package/dist/node/chunks/chunk-XPLGAEBH.js +2623 -0
  11. package/dist/node/cli-api.js +843 -0
  12. package/dist/node/index.js +138 -0
  13. package/dist/node/main.js +61 -0
  14. package/dist/node/preload.js +56 -0
  15. package/dist/types/cli/bin.d.ts +1 -0
  16. package/dist/types/cli/build.d.ts +7 -0
  17. package/dist/types/cli/cli.d.ts +2 -0
  18. package/dist/types/cli/config.d.ts +9 -0
  19. package/dist/types/cli/host-templates.d.ts +6 -0
  20. package/dist/types/cli/host.d.ts +16 -0
  21. package/dist/types/cli/index.d.ts +6 -0
  22. package/dist/types/cli/shared-modules.d.ts +8 -0
  23. package/dist/types/cli/templates.d.ts +10 -0
  24. package/dist/types/core/api.d.ts +11 -0
  25. package/dist/types/core/common.d.ts +69 -0
  26. package/dist/types/core/deps.d.ts +34 -0
  27. package/dist/types/core/events.d.ts +58 -0
  28. package/dist/types/core/hooks.d.ts +55 -0
  29. package/dist/types/core/index.d.ts +18 -0
  30. package/dist/types/core/ipc.d.ts +148 -0
  31. package/dist/types/core/manager.d.ts +155 -0
  32. package/dist/types/core/manifest.d.ts +88 -0
  33. package/dist/types/core/permissions.d.ts +45 -0
  34. package/dist/types/core/plugin.d.ts +214 -0
  35. package/dist/types/core/protocol.d.ts +86 -0
  36. package/dist/types/core/registry.d.ts +88 -0
  37. package/dist/types/core/routes.d.ts +105 -0
  38. package/dist/types/core/schema.d.ts +99 -0
  39. package/dist/types/core/semver.d.ts +16 -0
  40. package/dist/types/core/slots.d.ts +75 -0
  41. package/dist/types/core/storage.d.ts +55 -0
  42. package/dist/types/core/styles.d.ts +34 -0
  43. package/dist/types/main/adapters.d.ts +81 -0
  44. package/dist/types/main/discover.d.ts +21 -0
  45. package/dist/types/main/framework.d.ts +78 -0
  46. package/dist/types/main/fs-storage.d.ts +33 -0
  47. package/dist/types/main/index.d.ts +8 -0
  48. package/dist/types/main/registry.d.ts +75 -0
  49. package/dist/types/main/security.d.ts +24 -0
  50. package/dist/types/main/serve-web.d.ts +14 -0
  51. package/dist/types/main/tar.d.ts +10 -0
  52. package/dist/types/manager-ui/index.d.ts +7 -0
  53. package/dist/types/preload/index.d.ts +27 -0
  54. package/dist/types/react/base-css.d.ts +2 -0
  55. package/dist/types/react/components.d.ts +89 -0
  56. package/dist/types/react/http-bridge.d.ts +10 -0
  57. package/dist/types/react/index.d.ts +6 -0
  58. package/dist/types/react/palette.d.ts +10 -0
  59. package/dist/types/react/router.d.ts +47 -0
  60. package/dist/types/react/runtime.d.ts +99 -0
  61. package/docs/ARCHITECTURE.md +31 -0
  62. package/docs/PLUGIN-AUTHORS.md +11 -0
  63. package/docs/RELEASE.md +20 -0
  64. package/docs/SECURITY.md +18 -0
  65. package/docs/screenshots/command-palette.png +0 -0
  66. package/docs/screenshots/dashboard.png +0 -0
  67. package/docs/screenshots/plugin-details.png +0 -0
  68. package/docs/screenshots/plugin-manager.png +0 -0
  69. package/docs/screenshots/stats.png +0 -0
  70. package/package.json +112 -0
@@ -0,0 +1,990 @@
1
+ import {
2
+ CHANNELS,
3
+ FRAMEWORK_VERSION,
4
+ LogStore,
5
+ MGMT,
6
+ PermissionManager,
7
+ PluginManager,
8
+ compareVersions,
9
+ core_exports,
10
+ errorMessage,
11
+ serializeError,
12
+ validateManifest
13
+ } from "./chunk-XPLGAEBH.js";
14
+
15
+ // src/main/framework.ts
16
+ import { watch } from "node:fs";
17
+ import path6 from "node:path";
18
+
19
+ // src/main/discover.ts
20
+ import { promises as fs2 } from "node:fs";
21
+ import path2 from "node:path";
22
+ import { pathToFileURL } from "node:url";
23
+
24
+ // src/main/security.ts
25
+ import crypto from "node:crypto";
26
+ import { promises as fs } from "node:fs";
27
+ import path from "node:path";
28
+ var IntegrityError = class extends Error {
29
+ constructor(message) {
30
+ super(message);
31
+ this.name = "IntegrityError";
32
+ }
33
+ };
34
+ var sha256File = async (file) => "sha256-" + crypto.createHash("sha256").update(await fs.readFile(file)).digest("hex");
35
+ function canonicalJson(v) {
36
+ if (Array.isArray(v)) return `[${v.map(canonicalJson).join(",")}]`;
37
+ if (v && typeof v === "object") {
38
+ const o = v;
39
+ return `{${Object.keys(o).filter((k) => o[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
40
+ }
41
+ return JSON.stringify(v);
42
+ }
43
+ function resolveInside(root, rel) {
44
+ const abs = path.resolve(root, rel);
45
+ if (abs !== root && !abs.startsWith(path.resolve(root) + path.sep)) throw new IntegrityError(`path escapes plugin directory: ${rel}`);
46
+ return abs;
47
+ }
48
+ async function computeIntegrity(root, manifest) {
49
+ const files = [manifest.main, manifest.preload, manifest.renderer, ...manifest.styles ?? []].filter((f) => !!f);
50
+ const out = {};
51
+ for (const f of files) {
52
+ const rel = f.replace(/^\.\//, "");
53
+ try {
54
+ out[rel] = await sha256File(resolveInside(root, rel));
55
+ } catch {
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+ function generateSigningKeyPair() {
61
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
62
+ return {
63
+ publicKey: publicKey.export({ type: "spki", format: "der" }).toString("base64"),
64
+ privateKey: privateKey.export({ type: "pkcs8", format: "der" }).toString("base64")
65
+ };
66
+ }
67
+ var signedPayload = (m) => {
68
+ const { signature: _s, ...rest } = m;
69
+ return canonicalJson(rest);
70
+ };
71
+ function signManifest(manifest, privateKeyB64) {
72
+ const key = crypto.createPrivateKey({ key: Buffer.from(privateKeyB64, "base64"), format: "der", type: "pkcs8" });
73
+ const pub = crypto.createPublicKey(key).export({ type: "spki", format: "der" }).toString("base64");
74
+ const value = crypto.sign(null, Buffer.from(signedPayload(manifest)), key).toString("base64");
75
+ return { ...manifest, signature: { algorithm: "ed25519", publicKey: pub, value } };
76
+ }
77
+ async function verifyPlugin(root, raw, manifest, opts = {}) {
78
+ const warnings = [];
79
+ if (opts.verifyIntegrity !== false && manifest.integrity) {
80
+ for (const [rel, expected] of Object.entries(manifest.integrity)) {
81
+ let actual;
82
+ try {
83
+ actual = await sha256File(resolveInside(root, rel));
84
+ } catch {
85
+ throw new IntegrityError(`integrity: file "${rel}" is missing`);
86
+ }
87
+ if (actual !== expected) throw new IntegrityError(`integrity: "${rel}" does not match its recorded hash (file was modified)`);
88
+ }
89
+ } else if (opts.verifyIntegrity !== false && opts.requireSignature) {
90
+ throw new IntegrityError("integrity hashes are required but missing");
91
+ }
92
+ const sig = manifest.signature;
93
+ if (sig) {
94
+ let ok = false;
95
+ try {
96
+ const pub = crypto.createPublicKey({ key: Buffer.from(sig.publicKey, "base64"), format: "der", type: "spki" });
97
+ ok = crypto.verify(null, Buffer.from(signedPayload(raw)), pub, Buffer.from(sig.value, "base64"));
98
+ } catch {
99
+ ok = false;
100
+ }
101
+ if (!ok) throw new IntegrityError("signature is invalid");
102
+ if (opts.trustedKeys && !opts.trustedKeys.includes(sig.publicKey)) {
103
+ if (opts.requireSignature) throw new IntegrityError("signed by an untrusted key");
104
+ warnings.push("signed by an untrusted key");
105
+ }
106
+ } else if (opts.requireSignature) {
107
+ throw new IntegrityError("plugin is not signed");
108
+ }
109
+ return warnings;
110
+ }
111
+
112
+ // src/main/discover.ts
113
+ var MANIFEST_FILE = "plugin.json";
114
+ var counter = 0;
115
+ async function importFresh(file) {
116
+ const url = pathToFileURL(file).href + `?flux=${Date.now()}.${counter++}`;
117
+ const mod = await import(
118
+ /* @vite-ignore */
119
+ url
120
+ );
121
+ return mod.default ?? mod;
122
+ }
123
+ function toDefinition(x, entry) {
124
+ const def = x && typeof x === "object" && "default" in x ? x.default : x;
125
+ if (!def || typeof def !== "object") throw new Error(`"${entry}" must default-export definePlugin({...})`);
126
+ return def;
127
+ }
128
+ async function readPluginDir(root, opts = {}) {
129
+ const abs = path2.resolve(root);
130
+ let raw;
131
+ try {
132
+ raw = JSON.parse(await fs2.readFile(path2.join(abs, MANIFEST_FILE), "utf8"));
133
+ } catch (e) {
134
+ raw = { id: `invalid.${path2.basename(abs)}`.toLowerCase().replace(/[^a-z0-9.-]/g, "-") };
135
+ return {
136
+ root: abs,
137
+ manifest: raw,
138
+ problem: `cannot read ${MANIFEST_FILE}: ${e.message}`,
139
+ source: { manifest: raw, root: abs, origin: `dir:${abs}`, load: async () => ({}) }
140
+ };
141
+ }
142
+ let problem;
143
+ const v = validateManifest(raw);
144
+ if (v.manifest) {
145
+ try {
146
+ await verifyPlugin(abs, raw, v.manifest, opts);
147
+ } catch (e) {
148
+ problem = e instanceof IntegrityError ? e.message : `verification failed: ${e.message}`;
149
+ }
150
+ }
151
+ const mainRel = v.manifest?.main;
152
+ const load = opts.loadEntry ?? importFresh;
153
+ const source = {
154
+ manifest: raw,
155
+ root: abs,
156
+ origin: `dir:${abs}`,
157
+ load: async () => {
158
+ if (problem) throw new IntegrityError(problem);
159
+ if (!mainRel) return {};
160
+ const file = path2.resolve(abs, mainRel);
161
+ if (!file.startsWith(abs + path2.sep)) throw new IntegrityError(`main entry escapes plugin directory`);
162
+ return toDefinition(await load(file), mainRel);
163
+ }
164
+ };
165
+ return { root: abs, manifest: raw, source, problem };
166
+ }
167
+ async function discoverPlugins(dir, opts = {}) {
168
+ let entries = [];
169
+ try {
170
+ entries = (await fs2.readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
171
+ } catch (e) {
172
+ if (e.code === "ENOENT") return [];
173
+ throw e;
174
+ }
175
+ const out = [];
176
+ for (const name of entries.sort()) {
177
+ const root = path2.join(dir, name);
178
+ try {
179
+ await fs2.access(path2.join(root, MANIFEST_FILE));
180
+ } catch {
181
+ continue;
182
+ }
183
+ out.push(await readPluginDir(root, opts));
184
+ }
185
+ return out;
186
+ }
187
+
188
+ // src/main/fs-storage.ts
189
+ import { promises as fs3 } from "node:fs";
190
+ import path3 from "node:path";
191
+ var safe = (ns) => ns.replace(/[^a-zA-Z0-9._-]/g, "_");
192
+ async function atomicWrite(file, data) {
193
+ await fs3.mkdir(path3.dirname(file), { recursive: true });
194
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
195
+ await fs3.writeFile(tmp, data, "utf8");
196
+ await fs3.rename(tmp, file);
197
+ }
198
+ var FsStorageBackend = class {
199
+ constructor(dir) {
200
+ this.dir = dir;
201
+ }
202
+ dir;
203
+ file(ns) {
204
+ return path3.join(this.dir, `${safe(ns)}.json`);
205
+ }
206
+ async read(ns) {
207
+ try {
208
+ return JSON.parse(await fs3.readFile(this.file(ns), "utf8"));
209
+ } catch (e) {
210
+ if (e.code === "ENOENT") return {};
211
+ throw e;
212
+ }
213
+ }
214
+ async write(ns, data) {
215
+ await atomicWrite(this.file(ns), JSON.stringify(data));
216
+ }
217
+ async remove(ns) {
218
+ await fs3.rm(this.file(ns), { force: true });
219
+ }
220
+ };
221
+ var JsonStateFile = class {
222
+ constructor(file) {
223
+ this.file = file;
224
+ }
225
+ file;
226
+ queue = Promise.resolve();
227
+ async readAll() {
228
+ try {
229
+ const j = JSON.parse(await fs3.readFile(this.file, "utf8"));
230
+ return { disabled: Array.isArray(j.disabled) ? j.disabled : [], approved: j.approved && typeof j.approved === "object" ? j.approved : {} };
231
+ } catch {
232
+ return { disabled: [], approved: {} };
233
+ }
234
+ }
235
+ /** Merges `patch(current)` into the file. Concurrent calls run one after the other. */
236
+ update(patch) {
237
+ const run = this.queue.then(async () => {
238
+ const cur = await this.readAll();
239
+ const p = typeof patch === "function" ? patch(cur) : patch;
240
+ await atomicWrite(this.file, JSON.stringify({ ...cur, ...p }, null, 2));
241
+ });
242
+ this.queue = run.catch(() => void 0);
243
+ return run;
244
+ }
245
+ /** Resolves once every queued write has hit the disk. */
246
+ async flush() {
247
+ await this.queue;
248
+ }
249
+ };
250
+
251
+ // src/main/registry.ts
252
+ import { promises as fs5 } from "node:fs";
253
+ import path5 from "node:path";
254
+ import crypto2 from "node:crypto";
255
+
256
+ // src/main/tar.ts
257
+ import { promises as fs4 } from "node:fs";
258
+ import path4 from "node:path";
259
+ import zlib from "node:zlib";
260
+ import { promisify } from "node:util";
261
+ var gzip = promisify(zlib.gzip);
262
+ var gunzip = promisify(zlib.gunzip);
263
+ var BLOCK = 512;
264
+ function header(name, size, mode, type) {
265
+ const h = Buffer.alloc(BLOCK);
266
+ if (Buffer.byteLength(name) > 100) throw new Error(`path too long for tar: ${name}`);
267
+ h.write(name, 0, "utf8");
268
+ h.write(mode.toString(8).padStart(7, "0") + "\0", 100);
269
+ h.write("0000000\0", 108);
270
+ h.write("0000000\0", 116);
271
+ h.write(size.toString(8).padStart(11, "0") + "\0", 124);
272
+ h.write("00000000000\0", 136);
273
+ h.write(" ", 148);
274
+ h.write(type, 156);
275
+ h.write("ustar\0", 257);
276
+ h.write("00", 263);
277
+ let sum = 0;
278
+ for (const b of h) sum += b;
279
+ h.write(sum.toString(8).padStart(6, "0") + "\0 ", 148);
280
+ return h;
281
+ }
282
+ async function walk(dir, base = "") {
283
+ const out = [];
284
+ for (const e of (await fs4.readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
285
+ if (e.name === "node_modules" || e.name === ".git") continue;
286
+ const abs = path4.join(dir, e.name);
287
+ const rel = base ? `${base}/${e.name}` : e.name;
288
+ if (e.isDirectory()) {
289
+ out.push({ rel, abs, dir: true });
290
+ out.push(...await walk(abs, rel));
291
+ } else if (e.isFile()) out.push({ rel, abs, dir: false });
292
+ }
293
+ return out;
294
+ }
295
+ async function createTarGz(dir, prefix = "package") {
296
+ const chunks = [];
297
+ for (const f of await walk(dir)) {
298
+ const name = `${prefix}/${f.rel}${f.dir ? "/" : ""}`;
299
+ if (f.dir) chunks.push(header(name, 0, 493, "5"));
300
+ else {
301
+ const data = await fs4.readFile(f.abs);
302
+ chunks.push(header(name, data.length, 420, "0"), data, Buffer.alloc((BLOCK - data.length % BLOCK) % BLOCK));
303
+ }
304
+ }
305
+ chunks.push(Buffer.alloc(BLOCK * 2));
306
+ return gzip(Buffer.concat(chunks));
307
+ }
308
+ async function readTarGz(buf) {
309
+ const tar = await gunzip(buf);
310
+ const entries = [];
311
+ let off = 0;
312
+ while (off + BLOCK <= tar.length) {
313
+ const h = tar.subarray(off, off + BLOCK);
314
+ if (h.every((b) => b === 0)) break;
315
+ const name = h.toString("utf8", 0, 100).replace(/\0.*$/, "");
316
+ const size = parseInt(h.toString("utf8", 124, 135).replace(/\0.*$/, "").trim() || "0", 8);
317
+ const type = String.fromCharCode(h[156] ?? 48);
318
+ off += BLOCK;
319
+ if (type === "0" || type === "\0") entries.push({ name, type: "file", data: Buffer.from(tar.subarray(off, off + size)) });
320
+ else if (type === "5") entries.push({ name, type: "dir", data: Buffer.alloc(0) });
321
+ off += Math.ceil(size / BLOCK) * BLOCK;
322
+ }
323
+ return entries;
324
+ }
325
+ async function extractTarGz(buf, dest) {
326
+ const root = path4.resolve(dest);
327
+ await fs4.mkdir(root, { recursive: true });
328
+ for (const e of await readTarGz(buf)) {
329
+ const parts = e.name.split("/").filter(Boolean);
330
+ parts.shift();
331
+ if (!parts.length) continue;
332
+ if (parts.some((p) => p === ".." || p.includes("\0")) || path4.isAbsolute(e.name)) throw new Error(`unsafe path in archive: ${e.name}`);
333
+ const target = path4.resolve(root, ...parts);
334
+ if (target !== root && !target.startsWith(root + path4.sep)) throw new Error(`unsafe path in archive: ${e.name}`);
335
+ if (e.type === "dir") await fs4.mkdir(target, { recursive: true });
336
+ else {
337
+ await fs4.mkdir(path4.dirname(target), { recursive: true });
338
+ await fs4.writeFile(target, e.data);
339
+ }
340
+ }
341
+ }
342
+
343
+ // src/main/registry.ts
344
+ async function readManifest(dir) {
345
+ const raw = JSON.parse(await fs5.readFile(path5.join(dir, MANIFEST_FILE), "utf8"));
346
+ const v = validateManifest(raw);
347
+ if (!v.manifest) throw new Error(`invalid manifest: ${v.issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
348
+ return { raw, manifest: v.manifest };
349
+ }
350
+ var toEntry = (m, location, source, sha256) => ({
351
+ id: m.id,
352
+ name: m.name,
353
+ version: m.version,
354
+ description: m.description,
355
+ author: typeof m.author === "string" ? m.author : m.author?.name,
356
+ permissions: m.permissions ?? [],
357
+ location,
358
+ source,
359
+ sha256
360
+ });
361
+ async function copyDir(src, dest) {
362
+ await fs5.mkdir(dest, { recursive: true });
363
+ for (const e of await fs5.readdir(src, { withFileTypes: true })) {
364
+ if (e.name === "node_modules" || e.name === ".git") continue;
365
+ const s = path5.join(src, e.name);
366
+ const d = path5.join(dest, e.name);
367
+ if (e.isDirectory()) await copyDir(s, d);
368
+ else if (e.isFile()) await fs5.copyFile(s, d);
369
+ }
370
+ }
371
+ var LocalRegistry = class {
372
+ constructor(dir, name = `local:${dir}`) {
373
+ this.dir = dir;
374
+ this.name = name;
375
+ }
376
+ dir;
377
+ name;
378
+ async search(query = "") {
379
+ const q = query.toLowerCase();
380
+ const out = [];
381
+ let names = [];
382
+ try {
383
+ names = await fs5.readdir(this.dir);
384
+ } catch {
385
+ return [];
386
+ }
387
+ for (const n of names.sort()) {
388
+ const root = path5.join(this.dir, n);
389
+ try {
390
+ const { manifest } = await readManifest(root);
391
+ if (!q || `${manifest.id} ${manifest.name} ${manifest.description ?? ""}`.toLowerCase().includes(q)) out.push(toEntry(manifest, root, this.name));
392
+ } catch {
393
+ }
394
+ }
395
+ return out;
396
+ }
397
+ async fetch(entry, dest) {
398
+ await copyDir(entry.location, dest);
399
+ return (await readManifest(dest)).manifest;
400
+ }
401
+ };
402
+ var HttpRegistry = class {
403
+ constructor(baseUrl, fetchImpl = fetch) {
404
+ this.baseUrl = baseUrl;
405
+ this.fetchImpl = fetchImpl;
406
+ this.name = `http:${baseUrl}`;
407
+ }
408
+ baseUrl;
409
+ fetchImpl;
410
+ name;
411
+ async search(query = "") {
412
+ const res = await this.fetchImpl(new URL("index.json", this.baseUrl.endsWith("/") ? this.baseUrl : this.baseUrl + "/"));
413
+ if (!res.ok) throw new Error(`registry responded ${res.status}`);
414
+ const j = await res.json();
415
+ const q = query.toLowerCase();
416
+ return (j.plugins ?? []).filter((p) => !q || `${p["id"]} ${p["name"]} ${p["description"] ?? ""}`.toLowerCase().includes(q)).map((p) => ({
417
+ id: String(p["id"]),
418
+ name: String(p["name"] ?? p["id"]),
419
+ version: String(p["version"]),
420
+ description: p["description"],
421
+ author: p["author"],
422
+ permissions: p["permissions"] ?? [],
423
+ location: new URL(String(p["url"]), this.baseUrl).href,
424
+ source: this.name,
425
+ sha256: p["sha256"]
426
+ }));
427
+ }
428
+ async fetch(entry, dest) {
429
+ const res = await this.fetchImpl(entry.location);
430
+ if (!res.ok) throw new Error(`download failed: ${res.status}`);
431
+ const buf = Buffer.from(await res.arrayBuffer());
432
+ if (entry.sha256 && crypto2.createHash("sha256").update(buf).digest("hex") !== entry.sha256.replace(/^sha256-/, "")) {
433
+ throw new Error("downloaded archive does not match the registry checksum");
434
+ }
435
+ await extractTarGz(buf, dest);
436
+ return (await readManifest(dest)).manifest;
437
+ }
438
+ };
439
+ var PluginInstaller = class {
440
+ constructor(pluginsDir, backupDir, registries = []) {
441
+ this.pluginsDir = pluginsDir;
442
+ this.backupDir = backupDir;
443
+ this.registries = registries;
444
+ }
445
+ pluginsDir;
446
+ backupDir;
447
+ registries;
448
+ addRegistry(r) {
449
+ this.registries.push(r);
450
+ }
451
+ async search(query) {
452
+ const all = await Promise.allSettled(this.registries.map((r) => r.search(query)));
453
+ return all.flatMap((r) => r.status === "fulfilled" ? r.value : []);
454
+ }
455
+ async installed() {
456
+ const out = [];
457
+ for (const n of await fs5.readdir(this.pluginsDir).catch(() => [])) {
458
+ try {
459
+ out.push((await readManifest(path5.join(this.pluginsDir, n))).manifest);
460
+ } catch {
461
+ }
462
+ }
463
+ return out;
464
+ }
465
+ dirOf(id) {
466
+ return path5.join(this.pluginsDir, id.replace(/[^a-z0-9.-]/gi, "_"));
467
+ }
468
+ /** Installs (or updates) `entry`. The plugin directory is replaced atomically from a staging directory. */
469
+ async install(entry) {
470
+ const registry = this.registries.find((r) => r.name === entry.source);
471
+ if (!registry) throw new Error(`unknown registry "${entry.source}"`);
472
+ const stage = path5.join(this.backupDir, ".staging", `${entry.id}-${Date.now()}`);
473
+ await fs5.mkdir(path5.dirname(stage), { recursive: true });
474
+ try {
475
+ const manifest = await registry.fetch(entry, stage);
476
+ if (manifest.id !== entry.id) throw new Error(`registry entry "${entry.id}" contains plugin "${manifest.id}"`);
477
+ const target = this.dirOf(entry.id);
478
+ let previousVersion;
479
+ try {
480
+ previousVersion = (await readManifest(target)).manifest.version;
481
+ const bk = path5.join(this.backupDir, entry.id, previousVersion);
482
+ await fs5.rm(bk, { recursive: true, force: true });
483
+ await fs5.mkdir(path5.dirname(bk), { recursive: true });
484
+ await fs5.rename(target, bk);
485
+ } catch (e) {
486
+ if (e.code && e.code !== "ENOENT") throw e;
487
+ }
488
+ await fs5.mkdir(path5.dirname(target), { recursive: true });
489
+ await fs5.rename(stage, target);
490
+ return { id: entry.id, version: manifest.version, previousVersion };
491
+ } finally {
492
+ await fs5.rm(stage, { recursive: true, force: true });
493
+ }
494
+ }
495
+ /** Removes the plugin directory (backups are kept so the user can still roll back/reinstall). */
496
+ async uninstall(id) {
497
+ const dir = this.dirOf(id);
498
+ const { manifest } = await readManifest(dir);
499
+ const bk = path5.join(this.backupDir, id, manifest.version);
500
+ await fs5.rm(bk, { recursive: true, force: true });
501
+ await fs5.mkdir(path5.dirname(bk), { recursive: true });
502
+ await fs5.rename(dir, bk);
503
+ }
504
+ async versions(id) {
505
+ const names = await fs5.readdir(path5.join(this.backupDir, id)).catch(() => []);
506
+ return names.sort(compareVersions);
507
+ }
508
+ /** Restores the newest backup (or `version`) and keeps the replaced version as a backup. */
509
+ async rollback(id, version) {
510
+ const versions = await this.versions(id);
511
+ const target = version ?? versions[versions.length - 1];
512
+ if (!target || !versions.includes(target)) throw new Error(`no backup available for "${id}"${version ? ` @ ${version}` : ""}`);
513
+ const dir = this.dirOf(id);
514
+ let current;
515
+ try {
516
+ current = (await readManifest(dir)).manifest.version;
517
+ const bk = path5.join(this.backupDir, id, current);
518
+ await fs5.rm(bk, { recursive: true, force: true });
519
+ await fs5.rename(dir, bk);
520
+ } catch (e) {
521
+ if (e.code && e.code !== "ENOENT") throw e;
522
+ }
523
+ await fs5.rename(path5.join(this.backupDir, id, target), dir);
524
+ return { id, version: target, previousVersion: current };
525
+ }
526
+ /** Entries from registries that are newer than what is installed. */
527
+ async updates() {
528
+ const installed = new Map((await this.installed()).map((m) => [m.id, m.version]));
529
+ return (await this.search()).filter((e) => installed.has(e.id) && compareVersions(e.version, installed.get(e.id)) > 0);
530
+ }
531
+ };
532
+
533
+ // src/main/framework.ts
534
+ var MainFramework = class {
535
+ constructor(options) {
536
+ this.options = options;
537
+ this.logs = new LogStore(5e3);
538
+ this.state = new JsonStateFile(path6.join(options.dataDir, "plugins-state.json"));
539
+ this.permissions = new PermissionManager({
540
+ autoGrant: !options.requireApproval,
541
+ onChange: (id, snap) => void this.state.update((s) => ({ approved: { ...s.approved, [id]: snap.granted } })).catch(() => void 0)
542
+ });
543
+ this.manager = new PluginManager({
544
+ runtime: "main",
545
+ storage: new FsStorageBackend(path6.join(options.dataDir, "storage")),
546
+ permissions: this.permissions,
547
+ logs: this.logs,
548
+ frameworkVersion: options.frameworkVersion,
549
+ stateStore: {
550
+ load: async () => (await this.state.readAll()).disabled,
551
+ save: (ids) => this.state.update({ disabled: ids })
552
+ },
553
+ onBroadcastEvent: (name, payload) => this.broadcast("flux.event." + name, payload),
554
+ ...options.manager
555
+ });
556
+ this.manager.ipc.setTransport({ broadcast: (c, p, t) => this.broadcast(c, p, t) });
557
+ this.installer = new PluginInstaller(options.installDir ?? options.pluginDirs[0], path6.join(options.dataDir, "backups"), options.registries ?? []);
558
+ this.manager.subscribe(() => {
559
+ this.revision++;
560
+ this.broadcast(MGMT.changed, { revision: this.revision });
561
+ });
562
+ this.registerManagementApi();
563
+ }
564
+ options;
565
+ manager;
566
+ logs;
567
+ installer;
568
+ permissions;
569
+ state;
570
+ broadcastListeners = /* @__PURE__ */ new Set();
571
+ watchers = [];
572
+ revision = 0;
573
+ reloadTimers = /* @__PURE__ */ new Map();
574
+ started = false;
575
+ /* --------------------------------------------------------- start / stop */
576
+ async start() {
577
+ if (this.started) return;
578
+ this.started = true;
579
+ const approved = (await this.state.readAll()).approved;
580
+ const g = globalThis;
581
+ g.__FLUXPLUGIN_SHARED__ = { ...g.__FLUXPLUGIN_SHARED__ ?? {}, "fluxplugin": core_exports };
582
+ this.permissions.restore(approved);
583
+ await this.manager.init();
584
+ await this.scan();
585
+ await this.manager.startup();
586
+ if (this.options.hotReload) this.watch();
587
+ }
588
+ discoverOpts() {
589
+ return { trustedKeys: this.options.trustedKeys, requireSignature: this.options.requireSignature, ...this.options.discover };
590
+ }
591
+ /** (Re)scans the plugin directories and registers plugins that are new. */
592
+ async scan() {
593
+ for (const dir of this.options.pluginDirs) {
594
+ for (const p of await discoverPlugins(dir, this.discoverOpts())) {
595
+ const id = typeof p.manifest["id"] === "string" ? p.manifest["id"] : "";
596
+ if (id && this.manager.has(id)) continue;
597
+ const info = this.manager.register(p.source);
598
+ if (p.problem) this.logs.logger(info.id).error(p.problem);
599
+ }
600
+ }
601
+ }
602
+ async stop() {
603
+ for (const w of this.watchers) w.close();
604
+ this.watchers = [];
605
+ for (const t of this.reloadTimers.values()) clearTimeout(t);
606
+ await this.manager.dispose();
607
+ await this.state.flush();
608
+ }
609
+ /** Watches plugin directories and reloads the affected plugin (debounced). */
610
+ watch() {
611
+ for (const dir of this.options.pluginDirs) {
612
+ try {
613
+ const w = watch(dir, { recursive: true }, (_evt, filename) => {
614
+ if (!filename) return;
615
+ const parts = String(filename).split(path6.sep);
616
+ if (parts.includes("node_modules") || parts.includes("src")) return;
617
+ const folder = parts[0];
618
+ clearTimeout(this.reloadTimers.get(folder));
619
+ this.reloadTimers.set(
620
+ folder,
621
+ setTimeout(() => void this.hotReloadFolder(path6.join(dir, folder)), 150)
622
+ );
623
+ });
624
+ this.watchers.push(w);
625
+ } catch (e) {
626
+ this.logs.logger("core").warn(`hot reload disabled for ${dir}: ${errorMessage(e)}`);
627
+ }
628
+ }
629
+ }
630
+ async hotReloadFolder(root) {
631
+ try {
632
+ const p = await readPluginDir(root, this.discoverOpts());
633
+ const id = String(p.manifest["id"] ?? "");
634
+ if (!id) return;
635
+ if (!this.manager.has(id)) {
636
+ this.manager.register(p.source);
637
+ await this.manager.activate(id).catch(() => void 0);
638
+ } else {
639
+ await this.manager.unregister(id);
640
+ this.manager.register(p.source);
641
+ await this.manager.activate(id).catch(() => void 0);
642
+ }
643
+ this.logs.logger(id).info("hot reloaded");
644
+ this.broadcast("flux.event.plugin.hotreload", { id });
645
+ } catch (e) {
646
+ this.logs.logger("core").error(`hot reload failed for ${root}: ${errorMessage(e)}`);
647
+ }
648
+ }
649
+ /* ----------------------------------------------------------- broadcasting */
650
+ onBroadcast(l) {
651
+ this.broadcastListeners.add(l);
652
+ return () => void this.broadcastListeners.delete(l);
653
+ }
654
+ broadcast(channel, payload, target) {
655
+ for (const l of [...this.broadcastListeners]) {
656
+ try {
657
+ l(channel, payload, target);
658
+ } catch {
659
+ }
660
+ }
661
+ }
662
+ /* ---------------------------------------------------------------- bootstrap */
663
+ bootstrap() {
664
+ const assetUrl = this.options.assetUrl ?? ((id, rel) => `fluxplugin://${id}/${rel.replace(/^\.\//, "")}`);
665
+ const plugins = this.manager.list().map((info) => {
666
+ const m = this.manager.manifest(info.id);
667
+ const rejected = info.state === "error" && info.error?.phase === "validate";
668
+ return {
669
+ id: info.id,
670
+ manifest: m,
671
+ enabled: info.enabled && !rejected,
672
+ rendererUrl: m.renderer && !rejected ? assetUrl(info.id, m.renderer) : void 0,
673
+ styleUrls: (m.styles ?? []).map((s) => assetUrl(info.id, s)),
674
+ granted: info.permissions.granted,
675
+ state: info.state
676
+ };
677
+ });
678
+ return { frameworkVersion: this.options.frameworkVersion ?? FRAMEWORK_VERSION, plugins, preload: this.manager.preload.manifest(), revision: this.revision };
679
+ }
680
+ /* --------------------------------------------------- renderer-facing IPC */
681
+ exposedChannels(kind) {
682
+ const out = new Set(this.options.allowedRendererChannels ?? []);
683
+ for (const e of this.manager.preload.list()) for (const m of Object.values(e.spec)) if (m.kind === kind) out.add(m.channel);
684
+ return out;
685
+ }
686
+ /**
687
+ * Handles an invoke coming from a renderer. Policy: the renderer may only call channels that a plugin explicitly
688
+ * exposed through `preload.expose`, channels the host allow-listed, and (optionally) `flux.plugins.*`.
689
+ */
690
+ async handleInvoke(req, senderId) {
691
+ try {
692
+ if (!req || typeof req.channel !== "string") throw new Error("malformed request");
693
+ const ch = req.channel;
694
+ const mgmt = ch.startsWith("flux.plugins.");
695
+ if (mgmt) {
696
+ if (this.options.allowManagement === false) throw new Error(`management API is disabled`);
697
+ } else if (!this.exposedChannels("invoke").has(ch)) {
698
+ throw new Error(`channel "${ch}" is not exposed to renderers`);
699
+ }
700
+ const caller = typeof req.caller === "string" && this.manager.has(req.caller) ? req.caller : "host";
701
+ if (caller !== "host") this.permissions.assert(caller, "ipc", `invoke("${ch}")`);
702
+ const value = await this.manager.ipc.invoke(ch, req.input, { caller, senderId });
703
+ return { ok: true, value };
704
+ } catch (e) {
705
+ return { ok: false, error: serializeError(e) };
706
+ }
707
+ }
708
+ handleSend(channel, payload, caller, senderId) {
709
+ if (typeof channel !== "string" || !this.exposedChannels("send").has(channel)) return;
710
+ const who = typeof caller === "string" && this.manager.has(caller) ? caller : "host";
711
+ if (who !== "host" && !this.permissions.has(who, "ipc")) return;
712
+ this.manager.ipc.dispatch(channel, payload, { caller: who, senderId });
713
+ }
714
+ /* -------------------------------------------------------- management API */
715
+ registerManagementApi() {
716
+ const ipc = this.manager.ipc;
717
+ const h = (name, fn) => ipc.handle(name, (i) => fn(i), "host");
718
+ const list = () => this.manager.list();
719
+ h(MGMT.list, () => list());
720
+ h(MGMT.enable, async ({ id }) => (await this.manager.enable(id).catch((e) => this.logs.logger(id).error(errorMessage(e))), list()));
721
+ h(MGMT.disable, async ({ id }) => (await this.manager.disable(id), list()));
722
+ h(MGMT.reload, async ({ id }) => (await this.manager.reload(id), list()));
723
+ h(MGMT.grant, ({ id, permission }) => (this.permissions.grant(id, permission), this.manager.info(id)));
724
+ h(MGMT.revoke, ({ id, permission }) => (this.permissions.revoke(id, permission), this.manager.info(id)));
725
+ h(MGMT.configGet, async ({ id }) => this.requireConfig(id).get());
726
+ h(MGMT.configSet, async ({ id, patch }) => this.requireConfig(id).set(patch));
727
+ h(MGMT.configReset, async ({ id }) => this.requireConfig(id).reset());
728
+ h(MGMT.configSchema, ({ id }) => this.manager.getConfig(id)?.describe() ?? null);
729
+ h(MGMT.logs, ({ id }) => this.logs.query(id ? { source: id } : {}).slice(-200));
730
+ h(MGMT.diagnostics, ({ id }) => this.manager.diagnostics(id));
731
+ h(MGMT.search, ({ query }) => this.installer.search(query));
732
+ h(MGMT.updates, () => this.installer.updates());
733
+ h(MGMT.install, async ({ entry }) => this.installAndLoad(entry));
734
+ h(MGMT.update, async ({ entry }) => this.installAndLoad(entry));
735
+ h(MGMT.uninstall, async ({ id }) => {
736
+ await this.manager.unregister(id);
737
+ await this.installer.uninstall(id);
738
+ return list();
739
+ });
740
+ h(MGMT.rollback, async ({ id, version }) => {
741
+ await this.manager.unregister(id);
742
+ const r = await this.installer.rollback(id, version);
743
+ await this.scan();
744
+ await this.manager.activate(id).catch(() => void 0);
745
+ return r;
746
+ });
747
+ }
748
+ requireConfig(id) {
749
+ const c = this.manager.getConfig(id);
750
+ if (!c) throw new Error(`plugin "${id}" has no configuration (activate it first)`);
751
+ return c;
752
+ }
753
+ async installAndLoad(entry) {
754
+ const wasActive = this.manager.has(entry.id) && this.manager.info(entry.id).state === "ready";
755
+ if (this.manager.has(entry.id)) await this.manager.unregister(entry.id);
756
+ const r = await this.installer.install(entry);
757
+ await this.scan();
758
+ if (this.manager.has(entry.id) && (wasActive || !r.previousVersion)) await this.manager.activate(entry.id).catch(() => void 0);
759
+ return r;
760
+ }
761
+ /** Channels used by transports. */
762
+ static channels = CHANNELS;
763
+ };
764
+
765
+ // src/main/adapters.ts
766
+ import http from "node:http";
767
+ import path7 from "node:path";
768
+ import { promises as fs6 } from "node:fs";
769
+ function attachElectron(fw, electron) {
770
+ const { ipcMain, webContents } = electron;
771
+ ipcMain.handle(CHANNELS.invoke, (e, req) => fw.handleInvoke(req, e.sender.id));
772
+ ipcMain.handle(CHANNELS.bootstrap, () => fw.bootstrap());
773
+ const onSend = (e, msg) => fw.handleSend(msg?.channel, msg?.payload, msg?.caller, e.sender.id);
774
+ ipcMain.on(CHANNELS.send, onSend);
775
+ const off = fw.onBroadcast((channel, payload, target) => {
776
+ for (const wc of webContents.getAllWebContents()) {
777
+ if (wc.isDestroyed()) continue;
778
+ if (target !== void 0 && wc.id !== target) continue;
779
+ wc.send(CHANNELS.event, { channel, payload });
780
+ }
781
+ });
782
+ return () => {
783
+ ipcMain.removeHandler(CHANNELS.invoke);
784
+ ipcMain.removeHandler(CHANNELS.bootstrap);
785
+ ipcMain.removeListener(CHANNELS.send, onSend);
786
+ off();
787
+ };
788
+ }
789
+ var PLUGIN_SCHEME = {
790
+ scheme: "fluxplugin",
791
+ privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, stream: true }
792
+ };
793
+ var MIME = {
794
+ ".js": "text/javascript",
795
+ ".mjs": "text/javascript",
796
+ ".css": "text/css",
797
+ ".json": "application/json",
798
+ ".svg": "image/svg+xml",
799
+ ".png": "image/png",
800
+ ".jpg": "image/jpeg",
801
+ ".woff2": "font/woff2",
802
+ ".map": "application/json"
803
+ };
804
+ async function resolvePluginAsset(fw, url) {
805
+ let u;
806
+ try {
807
+ u = new URL(url);
808
+ } catch {
809
+ return null;
810
+ }
811
+ const id = decodeURIComponent(u.hostname);
812
+ if (!fw.manager.has(id)) return null;
813
+ const root = fw.manager.info(id).root;
814
+ if (!root) return null;
815
+ const rel = decodeURIComponent(u.pathname).replace(/^\/+/, "");
816
+ const file = path7.resolve(root, rel);
817
+ if (!file.startsWith(path7.resolve(root) + path7.sep)) return null;
818
+ const m = fw.manager.manifest(id);
819
+ const main = m.main ? path7.resolve(root, m.main) : void 0;
820
+ if (file === main || path7.basename(file) === "plugin.json") return null;
821
+ const ext = path7.extname(file).toLowerCase();
822
+ if (!(ext in MIME)) return null;
823
+ try {
824
+ await fs6.access(file);
825
+ } catch {
826
+ return null;
827
+ }
828
+ return { file, mime: MIME[ext] };
829
+ }
830
+ function registerPluginProtocol(fw, protocol) {
831
+ protocol.handle(PLUGIN_SCHEME.scheme, async (req) => {
832
+ const r = await resolvePluginAsset(fw, req.url);
833
+ if (!r) return new Response("Not found", { status: 404 });
834
+ return new Response(await fs6.readFile(r.file), { headers: { "content-type": r.mime, "access-control-allow-origin": "*" } });
835
+ });
836
+ }
837
+ function attachHttp(fw, opts = {}) {
838
+ const prefix = opts.assetPrefix ?? "/@plugins";
839
+ const clients = /* @__PURE__ */ new Set();
840
+ const off = fw.onBroadcast((channel, payload) => {
841
+ const data = `data: ${JSON.stringify({ channel, payload })}
842
+
843
+ `;
844
+ for (const c of clients) c.write(data);
845
+ });
846
+ const readJson = (req) => new Promise((resolve, reject) => {
847
+ let s = "";
848
+ req.on("data", (d) => {
849
+ s += d;
850
+ if (s.length > 5e6) reject(new Error("body too large"));
851
+ });
852
+ req.on("end", () => {
853
+ try {
854
+ resolve(s ? JSON.parse(s) : {});
855
+ } catch (e) {
856
+ reject(e);
857
+ }
858
+ });
859
+ });
860
+ const json = (res, code, body) => {
861
+ res.writeHead(code, { "content-type": "application/json" });
862
+ res.end(JSON.stringify(body));
863
+ };
864
+ const server = http.createServer(async (req, res) => {
865
+ const url = new URL(req.url ?? "/", "http://localhost");
866
+ try {
867
+ if (url.pathname === "/flux/bootstrap") return json(res, 200, fw.bootstrap());
868
+ if (url.pathname === "/flux/invoke" && req.method === "POST") return json(res, 200, await fw.handleInvoke(await readJson(req), "http"));
869
+ if (url.pathname === "/flux/send" && req.method === "POST") {
870
+ const m = await readJson(req);
871
+ fw.handleSend(m.channel, m.payload, m.caller, "http");
872
+ return json(res, 200, { ok: true });
873
+ }
874
+ if (url.pathname === "/flux/events") {
875
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
876
+ res.write(": connected\n\n");
877
+ clients.add(res);
878
+ req.on("close", () => clients.delete(res));
879
+ return;
880
+ }
881
+ if (url.pathname.startsWith(prefix + "/")) {
882
+ const rest = url.pathname.slice(prefix.length + 1);
883
+ const slash = rest.indexOf("/");
884
+ const id = decodeURIComponent(rest.slice(0, slash));
885
+ const r = await resolvePluginAsset(fw, `fluxplugin://${encodeURIComponent(id)}/${rest.slice(slash + 1)}`);
886
+ if (!r) return json(res, 404, { error: "not found" });
887
+ res.writeHead(200, { "content-type": r.mime, "cache-control": "no-store" });
888
+ return res.end(await fs6.readFile(r.file));
889
+ }
890
+ if (opts.fallback && await opts.fallback(req, res)) return;
891
+ json(res, 404, { error: "not found" });
892
+ } catch (e) {
893
+ json(res, 500, { error: e.message });
894
+ }
895
+ });
896
+ return new Promise((resolve) => {
897
+ server.listen(opts.port ?? 0, opts.host ?? "127.0.0.1", () => {
898
+ const port = server.address().port;
899
+ resolve({
900
+ server,
901
+ port,
902
+ close: () => new Promise((r) => {
903
+ off();
904
+ for (const c of clients) c.end();
905
+ server.close(() => r());
906
+ })
907
+ });
908
+ });
909
+ });
910
+ }
911
+ function createLoopbackBridge(fw) {
912
+ const clone = (v) => v === void 0 ? v : JSON.parse(JSON.stringify(v));
913
+ const listeners = /* @__PURE__ */ new Map();
914
+ fw.onBroadcast((channel, payload) => {
915
+ for (const l of [...listeners.get(channel) ?? []]) l(clone(payload));
916
+ });
917
+ return {
918
+ async invoke(channel, input, caller) {
919
+ const res = await fw.handleInvoke({ channel, input: clone(input), caller }, "loopback");
920
+ if (!res.ok) throw Object.assign(new Error(res.error.message), { code: res.error.code, payload: res.error });
921
+ return clone(res.value);
922
+ },
923
+ send: (channel, payload, caller) => fw.handleSend(channel, clone(payload), caller, "loopback"),
924
+ on(channel, l) {
925
+ let set = listeners.get(channel);
926
+ if (!set) listeners.set(channel, set = /* @__PURE__ */ new Set());
927
+ set.add(l);
928
+ return () => void set.delete(l);
929
+ },
930
+ async bootstrap() {
931
+ return clone(fw.bootstrap());
932
+ }
933
+ };
934
+ }
935
+
936
+ // src/main/serve-web.ts
937
+ import path8 from "node:path";
938
+ import { promises as fs7 } from "node:fs";
939
+ var TYPES = { ".html": "text/html", ".js": "text/javascript", ".mjs": "text/javascript", ".css": "text/css", ".map": "application/json", ".svg": "image/svg+xml", ".png": "image/png" };
940
+ function serveWeb(fw, opts) {
941
+ const dist = path8.resolve(opts.distDir);
942
+ return attachHttp(fw, {
943
+ port: opts.port ?? 5173,
944
+ host: opts.host,
945
+ fallback: async (req, res) => {
946
+ const rel = !req.url || req.url === "/" ? "index.html" : decodeURIComponent(req.url.split("?")[0]).replace(/^\/+/, "");
947
+ const file = path8.resolve(dist, rel);
948
+ const type = TYPES[path8.extname(file)];
949
+ if (!type || !file.startsWith(dist + path8.sep)) return false;
950
+ try {
951
+ res.writeHead(200, { "content-type": type, "cache-control": "no-store" });
952
+ res.end(await fs7.readFile(file));
953
+ return true;
954
+ } catch {
955
+ return false;
956
+ }
957
+ }
958
+ });
959
+ }
960
+
961
+ export {
962
+ IntegrityError,
963
+ sha256File,
964
+ canonicalJson,
965
+ computeIntegrity,
966
+ generateSigningKeyPair,
967
+ signManifest,
968
+ verifyPlugin,
969
+ MANIFEST_FILE,
970
+ importFresh,
971
+ readPluginDir,
972
+ discoverPlugins,
973
+ FsStorageBackend,
974
+ JsonStateFile,
975
+ createTarGz,
976
+ readTarGz,
977
+ extractTarGz,
978
+ copyDir,
979
+ LocalRegistry,
980
+ HttpRegistry,
981
+ PluginInstaller,
982
+ MainFramework,
983
+ attachElectron,
984
+ PLUGIN_SCHEME,
985
+ resolvePluginAsset,
986
+ registerPluginProtocol,
987
+ attachHttp,
988
+ createLoopbackBridge,
989
+ serveWeb
990
+ };