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.
- package/LICENSE +9 -0
- package/README.md +50 -0
- package/dist/browser/chunks/chunk-Q75AOCXD.js +2544 -0
- package/dist/browser/chunks/chunk-ZZ25CRMR.js +759 -0
- package/dist/browser/index.js +138 -0
- package/dist/browser/manager-ui.js +165 -0
- package/dist/browser/react.js +73 -0
- package/dist/cli.js +1635 -0
- package/dist/node/chunks/chunk-BZ7NZF7I.js +990 -0
- package/dist/node/chunks/chunk-XPLGAEBH.js +2623 -0
- package/dist/node/cli-api.js +843 -0
- package/dist/node/index.js +138 -0
- package/dist/node/main.js +61 -0
- package/dist/node/preload.js +56 -0
- package/dist/types/cli/bin.d.ts +1 -0
- package/dist/types/cli/build.d.ts +7 -0
- package/dist/types/cli/cli.d.ts +2 -0
- package/dist/types/cli/config.d.ts +9 -0
- package/dist/types/cli/host-templates.d.ts +6 -0
- package/dist/types/cli/host.d.ts +16 -0
- package/dist/types/cli/index.d.ts +6 -0
- package/dist/types/cli/shared-modules.d.ts +8 -0
- package/dist/types/cli/templates.d.ts +10 -0
- package/dist/types/core/api.d.ts +11 -0
- package/dist/types/core/common.d.ts +69 -0
- package/dist/types/core/deps.d.ts +34 -0
- package/dist/types/core/events.d.ts +58 -0
- package/dist/types/core/hooks.d.ts +55 -0
- package/dist/types/core/index.d.ts +18 -0
- package/dist/types/core/ipc.d.ts +148 -0
- package/dist/types/core/manager.d.ts +155 -0
- package/dist/types/core/manifest.d.ts +88 -0
- package/dist/types/core/permissions.d.ts +45 -0
- package/dist/types/core/plugin.d.ts +214 -0
- package/dist/types/core/protocol.d.ts +86 -0
- package/dist/types/core/registry.d.ts +88 -0
- package/dist/types/core/routes.d.ts +105 -0
- package/dist/types/core/schema.d.ts +99 -0
- package/dist/types/core/semver.d.ts +16 -0
- package/dist/types/core/slots.d.ts +75 -0
- package/dist/types/core/storage.d.ts +55 -0
- package/dist/types/core/styles.d.ts +34 -0
- package/dist/types/main/adapters.d.ts +81 -0
- package/dist/types/main/discover.d.ts +21 -0
- package/dist/types/main/framework.d.ts +78 -0
- package/dist/types/main/fs-storage.d.ts +33 -0
- package/dist/types/main/index.d.ts +8 -0
- package/dist/types/main/registry.d.ts +75 -0
- package/dist/types/main/security.d.ts +24 -0
- package/dist/types/main/serve-web.d.ts +14 -0
- package/dist/types/main/tar.d.ts +10 -0
- package/dist/types/manager-ui/index.d.ts +7 -0
- package/dist/types/preload/index.d.ts +27 -0
- package/dist/types/react/base-css.d.ts +2 -0
- package/dist/types/react/components.d.ts +89 -0
- package/dist/types/react/http-bridge.d.ts +10 -0
- package/dist/types/react/index.d.ts +6 -0
- package/dist/types/react/palette.d.ts +10 -0
- package/dist/types/react/router.d.ts +47 -0
- package/dist/types/react/runtime.d.ts +99 -0
- package/docs/ARCHITECTURE.md +31 -0
- package/docs/PLUGIN-AUTHORS.md +11 -0
- package/docs/RELEASE.md +20 -0
- package/docs/SECURITY.md +18 -0
- package/docs/screenshots/command-palette.png +0 -0
- package/docs/screenshots/dashboard.png +0 -0
- package/docs/screenshots/plugin-details.png +0 -0
- package/docs/screenshots/plugin-manager.png +0 -0
- package/docs/screenshots/stats.png +0 -0
- package/package.json +112 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { LogStore, PluginManager, type Bootstrap, type InvokeRequest, type InvokeResponse, type PluginManagerOptions } from "../core/index.js";
|
|
2
|
+
import { type DiscoverOptions } from "./discover.js";
|
|
3
|
+
import { PluginInstaller, type PluginRegistry } from "./registry.js";
|
|
4
|
+
export interface MainFrameworkOptions {
|
|
5
|
+
/** Directories scanned for plugins (each sub-directory with a plugin.json). */
|
|
6
|
+
pluginDirs: string[];
|
|
7
|
+
/** Where plugin storage/config/state lives (typically `app.getPath("userData")/plugins-data`). */
|
|
8
|
+
dataDir: string;
|
|
9
|
+
/** Where installs/uninstalls happen. Defaults to the first of `pluginDirs`. */
|
|
10
|
+
installDir?: string;
|
|
11
|
+
registries?: PluginRegistry[];
|
|
12
|
+
/** Third-party plugins start with no permissions until the user approves them. Default: false (all declared granted). */
|
|
13
|
+
requireApproval?: boolean;
|
|
14
|
+
/** Reject plugins without a valid signature from one of these keys. */
|
|
15
|
+
trustedKeys?: string[];
|
|
16
|
+
requireSignature?: boolean;
|
|
17
|
+
/** Re-load plugins when files change on disk. Default false (enable in development). */
|
|
18
|
+
hotReload?: boolean;
|
|
19
|
+
/** Builds the URL used by the renderer to import `<pluginId>/<relative file>` (custom protocol or dev server). */
|
|
20
|
+
assetUrl?: (pluginId: string, relativePath: string) => string;
|
|
21
|
+
/** Channels the renderer may call in addition to those exposed by plugins. */
|
|
22
|
+
allowedRendererChannels?: string[];
|
|
23
|
+
/** Whether renderer code may use `flux.plugins.*` management channels. Default true (the host UI needs them). */
|
|
24
|
+
allowManagement?: boolean;
|
|
25
|
+
frameworkVersion?: string;
|
|
26
|
+
manager?: Partial<PluginManagerOptions>;
|
|
27
|
+
/** Replaces discovery loader (tests). */
|
|
28
|
+
discover?: DiscoverOptions;
|
|
29
|
+
}
|
|
30
|
+
type BroadcastListener = (channel: string, payload: unknown, target?: number | string) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Everything the main process needs, with no dependency on Electron. Adapters (`attachElectron`, `attachHttp`)
|
|
33
|
+
* connect it to a transport. This keeps the logic testable and lets the same framework run under
|
|
34
|
+
* Electron, a dev HTTP server, or an integration test.
|
|
35
|
+
*/
|
|
36
|
+
export declare class MainFramework {
|
|
37
|
+
readonly options: MainFrameworkOptions;
|
|
38
|
+
readonly manager: PluginManager;
|
|
39
|
+
readonly logs: LogStore;
|
|
40
|
+
readonly installer: PluginInstaller;
|
|
41
|
+
private readonly permissions;
|
|
42
|
+
private readonly state;
|
|
43
|
+
private readonly broadcastListeners;
|
|
44
|
+
private watchers;
|
|
45
|
+
private revision;
|
|
46
|
+
private reloadTimers;
|
|
47
|
+
private started;
|
|
48
|
+
constructor(options: MainFrameworkOptions);
|
|
49
|
+
start(): Promise<void>;
|
|
50
|
+
private discoverOpts;
|
|
51
|
+
/** (Re)scans the plugin directories and registers plugins that are new. */
|
|
52
|
+
scan(): Promise<void>;
|
|
53
|
+
stop(): Promise<void>;
|
|
54
|
+
/** Watches plugin directories and reloads the affected plugin (debounced). */
|
|
55
|
+
private watch;
|
|
56
|
+
private hotReloadFolder;
|
|
57
|
+
onBroadcast(l: BroadcastListener): () => void;
|
|
58
|
+
broadcast(channel: string, payload: unknown, target?: number | string): void;
|
|
59
|
+
bootstrap(): Bootstrap;
|
|
60
|
+
private exposedChannels;
|
|
61
|
+
/**
|
|
62
|
+
* Handles an invoke coming from a renderer. Policy: the renderer may only call channels that a plugin explicitly
|
|
63
|
+
* exposed through `preload.expose`, channels the host allow-listed, and (optionally) `flux.plugins.*`.
|
|
64
|
+
*/
|
|
65
|
+
handleInvoke(req: InvokeRequest, senderId?: number | string): Promise<InvokeResponse>;
|
|
66
|
+
handleSend(channel: string, payload: unknown, caller?: string, senderId?: number | string): void;
|
|
67
|
+
private registerManagementApi;
|
|
68
|
+
private requireConfig;
|
|
69
|
+
private installAndLoad;
|
|
70
|
+
/** Channels used by transports. */
|
|
71
|
+
static readonly channels: {
|
|
72
|
+
readonly invoke: "flux:invoke";
|
|
73
|
+
readonly send: "flux:send";
|
|
74
|
+
readonly event: "flux:event";
|
|
75
|
+
readonly bootstrap: "flux:bootstrap";
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { StorageBackend } from "../core/index.js";
|
|
2
|
+
/** One JSON file per plugin namespace, written atomically (tmp + rename). */
|
|
3
|
+
export declare class FsStorageBackend implements StorageBackend {
|
|
4
|
+
private readonly dir;
|
|
5
|
+
constructor(dir: string);
|
|
6
|
+
private file;
|
|
7
|
+
read(ns: string): Promise<Record<string, unknown>>;
|
|
8
|
+
write(ns: string, data: Record<string, unknown>): Promise<void>;
|
|
9
|
+
remove(ns: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
/** Persists the list of disabled plugins and approved permissions. Updates are serialized (no lost writes). */
|
|
12
|
+
export declare class JsonStateFile {
|
|
13
|
+
private readonly file;
|
|
14
|
+
private queue;
|
|
15
|
+
constructor(file: string);
|
|
16
|
+
readAll(): Promise<{
|
|
17
|
+
disabled: string[];
|
|
18
|
+
approved: Record<string, string[]>;
|
|
19
|
+
}>;
|
|
20
|
+
/** Merges `patch(current)` into the file. Concurrent calls run one after the other. */
|
|
21
|
+
update(patch: Partial<{
|
|
22
|
+
disabled: string[];
|
|
23
|
+
approved: Record<string, string[]>;
|
|
24
|
+
}> | ((cur: {
|
|
25
|
+
disabled: string[];
|
|
26
|
+
approved: Record<string, string[]>;
|
|
27
|
+
}) => Partial<{
|
|
28
|
+
disabled: string[];
|
|
29
|
+
approved: Record<string, string[]>;
|
|
30
|
+
}>)): Promise<void>;
|
|
31
|
+
/** Resolves once every queued write has hit the disk. */
|
|
32
|
+
flush(): Promise<void>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type PluginManifest } from "../core/index.js";
|
|
2
|
+
import { createTarGz } from "./tar.js";
|
|
3
|
+
export interface RegistryEntry {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
author?: string;
|
|
9
|
+
permissions: string[];
|
|
10
|
+
/** Opaque location understood by the registry that produced the entry. */
|
|
11
|
+
location: string;
|
|
12
|
+
source: string;
|
|
13
|
+
/** sha256 hex of the archive when the registry knows it. */
|
|
14
|
+
sha256?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Abstraction over where plugins come from. Implement it for npm, GitHub releases, a private HTTP registry,
|
|
18
|
+
* or a marketplace. The framework only needs `search` and `fetch` (a directory with a plugin.json).
|
|
19
|
+
*/
|
|
20
|
+
export interface PluginRegistry {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
search(query?: string): Promise<RegistryEntry[]>;
|
|
23
|
+
/** Materializes a plugin into `destDir` (which does not exist yet). Returns its manifest. */
|
|
24
|
+
fetch(entry: RegistryEntry, destDir: string): Promise<PluginManifest>;
|
|
25
|
+
}
|
|
26
|
+
export declare function copyDir(src: string, dest: string): Promise<void>;
|
|
27
|
+
/** A directory whose sub-directories are plugins (e.g. a shared network folder or a repo checkout). */
|
|
28
|
+
export declare class LocalRegistry implements PluginRegistry {
|
|
29
|
+
private readonly dir;
|
|
30
|
+
readonly name: string;
|
|
31
|
+
constructor(dir: string, name?: string);
|
|
32
|
+
search(query?: string): Promise<RegistryEntry[]>;
|
|
33
|
+
fetch(entry: RegistryEntry, dest: string): Promise<PluginManifest>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* HTTP registry: GET `<baseUrl>/index.json` → `{ plugins: [{ id, name, version, description, author, permissions, url, sha256 }] }`
|
|
37
|
+
* where `url` points at a `.tgz` produced by `fluxplugin package`. Downloads are verified against `sha256` when given.
|
|
38
|
+
*/
|
|
39
|
+
export declare class HttpRegistry implements PluginRegistry {
|
|
40
|
+
private readonly baseUrl;
|
|
41
|
+
private readonly fetchImpl;
|
|
42
|
+
readonly name: string;
|
|
43
|
+
constructor(baseUrl: string, fetchImpl?: typeof fetch);
|
|
44
|
+
search(query?: string): Promise<RegistryEntry[]>;
|
|
45
|
+
fetch(entry: RegistryEntry, dest: string): Promise<PluginManifest>;
|
|
46
|
+
}
|
|
47
|
+
export interface InstallResult {
|
|
48
|
+
id: string;
|
|
49
|
+
version: string;
|
|
50
|
+
previousVersion?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Installs plugins into a directory, keeping the previous version as a backup so `rollback` works.
|
|
54
|
+
* Layout: `<pluginsDir>/<id>/` (active) and `<backupDir>/<id>/<version>/` (previous versions).
|
|
55
|
+
*/
|
|
56
|
+
export declare class PluginInstaller {
|
|
57
|
+
private readonly pluginsDir;
|
|
58
|
+
private readonly backupDir;
|
|
59
|
+
private readonly registries;
|
|
60
|
+
constructor(pluginsDir: string, backupDir: string, registries?: PluginRegistry[]);
|
|
61
|
+
addRegistry(r: PluginRegistry): void;
|
|
62
|
+
search(query?: string): Promise<RegistryEntry[]>;
|
|
63
|
+
installed(): Promise<PluginManifest[]>;
|
|
64
|
+
private dirOf;
|
|
65
|
+
/** Installs (or updates) `entry`. The plugin directory is replaced atomically from a staging directory. */
|
|
66
|
+
install(entry: RegistryEntry): Promise<InstallResult>;
|
|
67
|
+
/** Removes the plugin directory (backups are kept so the user can still roll back/reinstall). */
|
|
68
|
+
uninstall(id: string): Promise<void>;
|
|
69
|
+
versions(id: string): Promise<string[]>;
|
|
70
|
+
/** Restores the newest backup (or `version`) and keeps the replaced version as a backup. */
|
|
71
|
+
rollback(id: string, version?: string): Promise<InstallResult>;
|
|
72
|
+
/** Entries from registries that are newer than what is installed. */
|
|
73
|
+
updates(): Promise<RegistryEntry[]>;
|
|
74
|
+
}
|
|
75
|
+
export { createTarGz };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PluginManifest } from "../core/index.js";
|
|
2
|
+
export declare class IntegrityError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare const sha256File: (file: string) => Promise<string>;
|
|
6
|
+
/** Deterministic JSON (sorted keys) so signatures do not depend on key order. */
|
|
7
|
+
export declare function canonicalJson(v: unknown): string;
|
|
8
|
+
/** Fills `integrity` for every entry file of a manifest. Used by `fluxplugin package`. */
|
|
9
|
+
export declare function computeIntegrity(root: string, manifest: Pick<PluginManifest, "main" | "preload" | "renderer" | "styles">): Promise<Record<string, string>>;
|
|
10
|
+
export declare function generateSigningKeyPair(): {
|
|
11
|
+
publicKey: string;
|
|
12
|
+
privateKey: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function signManifest(manifest: Record<string, unknown>, privateKeyB64: string): Record<string, unknown>;
|
|
15
|
+
export interface VerifyOptions {
|
|
16
|
+
/** Verify the hashes listed in `manifest.integrity` (default true). */
|
|
17
|
+
verifyIntegrity?: boolean;
|
|
18
|
+
/** Public keys (base64 SPKI DER) allowed to sign plugins. */
|
|
19
|
+
trustedKeys?: string[];
|
|
20
|
+
/** Refuse plugins without a valid signature from a trusted key. Default false. */
|
|
21
|
+
requireSignature?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Throws `IntegrityError` when a check fails. Returns warnings otherwise. */
|
|
24
|
+
export declare function verifyPlugin(root: string, raw: Record<string, unknown>, manifest: PluginManifest, opts?: VerifyOptions): Promise<string[]>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MainFramework } from "./framework.js";
|
|
2
|
+
/**
|
|
3
|
+
* Runs the app in a plain browser: serves the built renderer (`distDir`) plus the framework wire protocol over HTTP/SSE.
|
|
4
|
+
* Intended for development, headless E2E tests and demos; binds to loopback by default.
|
|
5
|
+
*/
|
|
6
|
+
export declare function serveWeb(fw: MainFramework, opts: {
|
|
7
|
+
distDir: string;
|
|
8
|
+
port?: number;
|
|
9
|
+
host?: string;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
server: import("http").Server;
|
|
12
|
+
port: number;
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Creates a .tgz (ustar) of a directory. Entries are placed under `package/` like npm does. */
|
|
2
|
+
export declare function createTarGz(dir: string, prefix?: string): Promise<Buffer>;
|
|
3
|
+
export interface TarEntry {
|
|
4
|
+
name: string;
|
|
5
|
+
type: "file" | "dir";
|
|
6
|
+
data: Buffer;
|
|
7
|
+
}
|
|
8
|
+
export declare function readTarGz(buf: Buffer): Promise<TarEntry[]>;
|
|
9
|
+
/** Extracts into `dest`, stripping the first path component. Rejects traversal and absolute paths. */
|
|
10
|
+
export declare function extractTarGz(buf: Buffer, dest: string): Promise<void>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/** Auto-generated settings form from a plugin's config schema (`ctx.config.describe()`). */
|
|
3
|
+
export declare function ConfigForm({ id }: {
|
|
4
|
+
id: string;
|
|
5
|
+
}): React.ReactElement | null;
|
|
6
|
+
/** Full plugin manager screen. Extensible through the `manager.plugin.actions` / `manager.plugin.details` slots. */
|
|
7
|
+
export declare function PluginManagerUI(): React.ReactElement;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type FluxBridge, type IpcErrorPayload } from "../core/index.js";
|
|
2
|
+
/** Structural types for Electron's `contextBridge` / `ipcRenderer` — no `electron` import, works in sandboxed preloads. */
|
|
3
|
+
export interface IpcRendererLike {
|
|
4
|
+
invoke(channel: string, ...args: unknown[]): Promise<any>;
|
|
5
|
+
send(channel: string, ...args: unknown[]): void;
|
|
6
|
+
on(channel: string, listener: (event: unknown, ...args: any[]) => void): unknown;
|
|
7
|
+
removeListener(channel: string, listener: (...args: any[]) => void): unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface ContextBridgeLike {
|
|
10
|
+
exposeInMainWorld(key: string, api: unknown): void;
|
|
11
|
+
}
|
|
12
|
+
export declare class IpcCallError extends Error {
|
|
13
|
+
readonly payload: IpcErrorPayload;
|
|
14
|
+
constructor(payload: IpcErrorPayload);
|
|
15
|
+
get code(): string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Creates the bridge object. It is deliberately tiny and generic: it knows nothing about plugins.
|
|
19
|
+
* Per-plugin APIs (`window.plugins.analytics.getStats()`) are generated *in the renderer* from the preload
|
|
20
|
+
* manifest (see `fluxplugin/react` → `installWindowPlugins`), so plugins can be enabled/disabled at runtime —
|
|
21
|
+
* `contextBridge` can only expose objects once, at preload time.
|
|
22
|
+
*
|
|
23
|
+
* Nothing from Node, `ipcRenderer` itself, or `event` objects ever reaches the page: only the five functions below.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createBridge(ipcRenderer: IpcRendererLike): FluxBridge;
|
|
26
|
+
/** Call from your preload script: `exposeFluxBridge(contextBridge, ipcRenderer)`. */
|
|
27
|
+
export declare function exposeFluxBridge(contextBridge: ContextBridgeLike, ipcRenderer: IpcRendererLike, key?: string): FluxBridge;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Minimal CSS for the framework's own components. Uses CSS variables so hosts can theme it; Tailwind is not required. */
|
|
2
|
+
export declare const baseCss = "\n:root{--flux-bg:#fff;--flux-fg:#0f172a;--flux-muted:#64748b;--flux-border:#e2e8f0;--flux-accent:#4f46e5;--flux-accent-fg:#fff;--flux-surface:#f8fafc;--flux-danger:#dc2626;--flux-ok:#16a34a;--flux-warn:#d97706;--flux-radius:10px}\n@media (prefers-color-scheme: dark){:root:not([data-theme=\"light\"]){--flux-bg:#0b1020;--flux-fg:#e5e7eb;--flux-muted:#94a3b8;--flux-border:#1f2a44;--flux-surface:#111936}}\n.fluxplugin-error{padding:8px 12px;border:1px solid var(--flux-danger);border-radius:var(--flux-radius);color:var(--flux-danger);font-size:13px}\n.flux-breadcrumbs{font-size:13px;color:var(--flux-muted)}.flux-breadcrumbs a{color:inherit}\n.flux-not-found,.flux-forbidden,.flux-loading{padding:24px;color:var(--flux-muted)}\n.flux-palette-backdrop{position:fixed;inset:0;background:rgba(2,6,23,.45);display:flex;justify-content:center;align-items:flex-start;padding-top:14vh;z-index:9999}\n.flux-palette{width:min(560px,92vw);background:var(--flux-bg);color:var(--flux-fg);border:1px solid var(--flux-border);border-radius:14px;box-shadow:0 24px 60px rgba(0,0,0,.35);overflow:hidden}\n.flux-palette-input{width:100%;box-sizing:border-box;padding:14px 16px;border:0;border-bottom:1px solid var(--flux-border);background:transparent;color:inherit;font-size:15px;outline:none}\n.flux-palette-list{list-style:none;margin:0;padding:6px;max-height:340px;overflow:auto}\n.flux-palette-item{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:9px 12px;border-radius:8px;cursor:pointer;font-size:14px}\n.flux-palette-item.is-active{background:var(--flux-accent);color:var(--flux-accent-fg)}\n.flux-palette-cat{opacity:.65}.flux-palette-empty{padding:14px;color:var(--flux-muted);font-size:14px}\n.flux-palette kbd{font:11px ui-monospace,monospace;padding:2px 6px;border:1px solid currentColor;border-radius:5px;opacity:.7}\n";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React, { Component, type ErrorInfo, type ReactNode } from "react";
|
|
2
|
+
import { type IpcContract, type PluginConfig, type PluginInfo } from "../core/index.js";
|
|
3
|
+
import { RendererFramework } from "./runtime.js";
|
|
4
|
+
export declare function PluginProvider({ framework, children }: {
|
|
5
|
+
framework: RendererFramework;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}): React.ReactElement;
|
|
8
|
+
export declare function usePluginFramework(): RendererFramework;
|
|
9
|
+
interface BoundaryProps {
|
|
10
|
+
owner: string;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
fallback?: (error: Error, reset: () => void) => ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/** Isolates a plugin's UI: a throwing component renders a small fallback and is reported to the runtime. */
|
|
15
|
+
export declare class PluginErrorBoundary extends Component<BoundaryProps & {
|
|
16
|
+
framework: RendererFramework;
|
|
17
|
+
}, {
|
|
18
|
+
error: Error | null;
|
|
19
|
+
}> {
|
|
20
|
+
state: {
|
|
21
|
+
error: Error | null;
|
|
22
|
+
};
|
|
23
|
+
static getDerivedStateFromError(error: Error): {
|
|
24
|
+
error: Error;
|
|
25
|
+
};
|
|
26
|
+
componentDidCatch(error: Error, _info: ErrorInfo): void;
|
|
27
|
+
reset: () => void;
|
|
28
|
+
render(): ReactNode;
|
|
29
|
+
}
|
|
30
|
+
/** Wraps plugin-provided UI: error boundary + `data-fluxplugin` attribute (used by scoped styles). */
|
|
31
|
+
export declare function PluginBoundary({ owner, children }: {
|
|
32
|
+
owner: string;
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
}): React.ReactElement;
|
|
35
|
+
export interface PluginSlotProps {
|
|
36
|
+
name: string;
|
|
37
|
+
/** Host default content: kept, wrapped, or replaced depending on plugin contributions. */
|
|
38
|
+
children?: ReactNode;
|
|
39
|
+
/** Props handed to every contributed component. */
|
|
40
|
+
props?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Declares an extension point in the host UI.
|
|
44
|
+
* `before`/`prepend` contributions render above the default content, `after`/`append` below it and
|
|
45
|
+
* a `replace` contribution takes its place.
|
|
46
|
+
*/
|
|
47
|
+
export declare function PluginSlot({ name, children, props }: PluginSlotProps): React.ReactElement;
|
|
48
|
+
/** Data contributions to a declarative extension point (`ui.extend("dashboard", { toolbar: [...] })`). */
|
|
49
|
+
export declare function useExtensionItems<T = unknown>(point: string, key: string): T[];
|
|
50
|
+
export declare function usePlugin(id: string): PluginInfo | undefined;
|
|
51
|
+
export declare function usePlugins(): PluginInfo[];
|
|
52
|
+
/** Reactive per-plugin state (same store as `ctx.state` inside the plugin). */
|
|
53
|
+
export declare function usePluginState<T>(pluginId: string, key: string, initial: T): [T, (v: T | ((prev: T) => T)) => void];
|
|
54
|
+
/**
|
|
55
|
+
* Subscribes to an event. Looks at the renderer event bus *and* at pushes from the main process
|
|
56
|
+
* (`ctx.ipc.send(channel, payload)` and broadcast events). Returns the latest payload.
|
|
57
|
+
*/
|
|
58
|
+
export declare function usePluginEvent<T = unknown>(name: string, onEvent?: (payload: T) => void): T | undefined;
|
|
59
|
+
export declare function usePluginCommand<R = unknown>(id: string): {
|
|
60
|
+
run: (...args: unknown[]) => Promise<R>;
|
|
61
|
+
available: boolean;
|
|
62
|
+
title?: string;
|
|
63
|
+
};
|
|
64
|
+
export declare function usePluginService<T = unknown>(name: string): T | undefined;
|
|
65
|
+
export interface InvokeState<O> {
|
|
66
|
+
data: O | undefined;
|
|
67
|
+
error: Error | undefined;
|
|
68
|
+
loading: boolean;
|
|
69
|
+
}
|
|
70
|
+
/** Calls an IPC endpoint (by name or shared contract) with loading/error state. */
|
|
71
|
+
export declare function usePluginIPC<I = unknown, O = unknown>(endpoint: string | IpcContract<I, O>, opts?: {
|
|
72
|
+
caller?: string;
|
|
73
|
+
}): InvokeState<O> & {
|
|
74
|
+
call: (input?: I) => Promise<O>;
|
|
75
|
+
};
|
|
76
|
+
/** Current route match: params, breadcrumbs and route meta. */
|
|
77
|
+
export { usePluginRoute } from "./router.js";
|
|
78
|
+
export declare function usePluginConfig<T extends Record<string, any> = Record<string, any>>(pluginId: string): {
|
|
79
|
+
config: T | undefined;
|
|
80
|
+
loading: boolean;
|
|
81
|
+
error: Error | undefined;
|
|
82
|
+
set: PluginConfig<T>["set"];
|
|
83
|
+
reset: PluginConfig<T>["reset"];
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Mirrors the style registry into the document: `<style>` for inline css, `<link>` for hrefs.
|
|
87
|
+
* Disabling/reloading a plugin removes or replaces its styles because the registry entries disappear.
|
|
88
|
+
*/
|
|
89
|
+
export declare function StyleHost(): null;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FluxBridge } from "../core/index.js";
|
|
2
|
+
export declare class HttpBridgeError extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
constructor(message: string, code: string);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Bridge over HTTP + Server-Sent Events, for running the renderer in a plain browser against `attachHttp()`
|
|
8
|
+
* (development, headless E2E tests, screenshots). In Electron the preload bridge is used instead.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createHttpBridge(baseUrl?: string): FluxBridge;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/** Global keybindings: runs the command bound to the pressed shortcut. `Mod` = Ctrl (Win/Linux) or Cmd (macOS). */
|
|
3
|
+
export declare function useKeybindings(opts?: {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
}): void;
|
|
6
|
+
/** Searchable command palette (opens with Mod+K, or controlled through `open`). */
|
|
7
|
+
export declare function CommandPalette({ open, onClose }: {
|
|
8
|
+
open?: boolean;
|
|
9
|
+
onClose?: () => void;
|
|
10
|
+
}): React.ReactElement | null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { type ComponentType, type ReactNode } from "react";
|
|
2
|
+
import { type Breadcrumb, type RouteMatch, type RouteParams } from "../core/index.js";
|
|
3
|
+
export declare function navigate(to: string, opts?: {
|
|
4
|
+
replace?: boolean;
|
|
5
|
+
}): void;
|
|
6
|
+
export declare function useLocation(): string;
|
|
7
|
+
export declare function useNavigate(): typeof navigate;
|
|
8
|
+
export declare function Link({ to, children, ...rest }: {
|
|
9
|
+
to: string;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
} & React.AnchorHTMLAttributes<HTMLAnchorElement>): React.ReactElement;
|
|
12
|
+
interface RouteState {
|
|
13
|
+
match: RouteMatch<any> | undefined;
|
|
14
|
+
params: RouteParams;
|
|
15
|
+
breadcrumbs: Breadcrumb[];
|
|
16
|
+
path: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Current route (params, breadcrumbs, match). Works anywhere under `PluginProvider` — e.g. a header rendering
|
|
20
|
+
* breadcrumbs outside `<PluginRoutes>` — because it derives from the location and the route registry.
|
|
21
|
+
*/
|
|
22
|
+
export declare function usePluginRoute(): RouteState;
|
|
23
|
+
export declare const useParams: () => RouteParams;
|
|
24
|
+
export declare function Breadcrumbs({ separator }: {
|
|
25
|
+
separator?: ReactNode;
|
|
26
|
+
}): React.ReactElement | null;
|
|
27
|
+
export interface PluginRoutesProps {
|
|
28
|
+
notFound?: ReactNode;
|
|
29
|
+
forbidden?: (reason: string) => ReactNode;
|
|
30
|
+
loading?: ReactNode;
|
|
31
|
+
/** Layout used when a route does not name one. */
|
|
32
|
+
defaultLayout?: ComponentType<{
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Renders the route matching the current location from the dynamic route registry.
|
|
38
|
+
* Plugins can add routes at any time; the router re-renders automatically. Routes owned by lazily-activated
|
|
39
|
+
* plugins (`onRoute:` activation events) trigger activation on first navigation.
|
|
40
|
+
*/
|
|
41
|
+
export declare function PluginRoutes({ notFound, forbidden, loading, defaultLayout: DefaultLayout }: PluginRoutesProps): React.ReactElement;
|
|
42
|
+
/** Menu entries for a location with active-route highlighting (sidebar, toolbar...). */
|
|
43
|
+
export declare function useMenu(location: import("../core/index.js").MenuLocation): {
|
|
44
|
+
items: import("../core/registry.js").MenuItemDefinition[];
|
|
45
|
+
run: (item: import("../core/index.js").MenuItemDefinition) => Promise<unknown> | undefined;
|
|
46
|
+
};
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { PluginManager, type Bootstrap, type FluxBridge, type IpcContract, type IpcErrorPayload, type Permission, type PluginConfig, type PluginInfo, type PluginManagerOptions, type PreloadApiSpec, type RegistryEntryLike, type RendererPluginDescriptor } from "../core/index.js";
|
|
2
|
+
export declare class RemoteError extends Error {
|
|
3
|
+
readonly payload: IpcErrorPayload;
|
|
4
|
+
constructor(payload: IpcErrorPayload);
|
|
5
|
+
get code(): string;
|
|
6
|
+
}
|
|
7
|
+
/** Typed client for the `flux.plugins.*` management API served by the main process. */
|
|
8
|
+
export declare class PluginsClient {
|
|
9
|
+
private readonly bridge;
|
|
10
|
+
constructor(bridge: FluxBridge);
|
|
11
|
+
private call;
|
|
12
|
+
list: () => Promise<PluginInfo[]>;
|
|
13
|
+
enable: (id: string) => Promise<PluginInfo[]>;
|
|
14
|
+
disable: (id: string) => Promise<PluginInfo[]>;
|
|
15
|
+
reload: (id: string) => Promise<PluginInfo[]>;
|
|
16
|
+
grant: (id: string, permission: Permission) => Promise<PluginInfo>;
|
|
17
|
+
revoke: (id: string, permission: Permission) => Promise<PluginInfo>;
|
|
18
|
+
uninstall: (id: string) => Promise<PluginInfo[]>;
|
|
19
|
+
search: (query?: string) => Promise<RegistryEntryLike[]>;
|
|
20
|
+
updates: () => Promise<RegistryEntryLike[]>;
|
|
21
|
+
install: (entry: RegistryEntryLike) => Promise<{
|
|
22
|
+
id: string;
|
|
23
|
+
version: string;
|
|
24
|
+
}>;
|
|
25
|
+
update: (entry: RegistryEntryLike) => Promise<{
|
|
26
|
+
id: string;
|
|
27
|
+
version: string;
|
|
28
|
+
}>;
|
|
29
|
+
rollback: (id: string, version?: string) => Promise<{
|
|
30
|
+
id: string;
|
|
31
|
+
version: string;
|
|
32
|
+
}>;
|
|
33
|
+
configGet: <T = Record<string, unknown>>(id: string) => Promise<T>;
|
|
34
|
+
configSet: <T = Record<string, unknown>>(id: string, patch: Partial<T>) => Promise<T>;
|
|
35
|
+
configReset: <T = Record<string, unknown>>(id: string) => Promise<T>;
|
|
36
|
+
configSchema: (id: string) => Promise<import("../core/schema.js").SchemaDescription | null>;
|
|
37
|
+
logs: (id?: string) => Promise<import("../core/common.js").LogEntry[]>;
|
|
38
|
+
diagnostics: (id: string) => Promise<{
|
|
39
|
+
info: PluginInfo;
|
|
40
|
+
logs: import("../core/index.js").LogEntry[];
|
|
41
|
+
contributions: Record<string, number>;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
export interface RendererFrameworkOptions {
|
|
45
|
+
bridge: FluxBridge;
|
|
46
|
+
/**
|
|
47
|
+
* How to import a plugin's renderer entry. Defaults to a dynamic `import(url)`.
|
|
48
|
+
* Hosts using a bundler can map ids to static imports (e.g. `import.meta.glob`) for dev.
|
|
49
|
+
*/
|
|
50
|
+
importPlugin?: (url: string, descriptor: RendererPluginDescriptor) => Promise<unknown>;
|
|
51
|
+
/** Called when a plugin component keeps crashing; defaults to disabling it in main + renderer. */
|
|
52
|
+
maxRenderErrors?: number;
|
|
53
|
+
manager?: Partial<PluginManagerOptions>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The renderer half of the framework. The main process stays the source of truth for *which* plugins exist, are
|
|
57
|
+
* enabled and which permissions were granted; the renderer mirrors that state (`sync`) and runs the renderer
|
|
58
|
+
* entries inside its own `PluginManager` (runtime = "renderer").
|
|
59
|
+
*/
|
|
60
|
+
export declare class RendererFramework {
|
|
61
|
+
private readonly opts;
|
|
62
|
+
readonly bridge: FluxBridge;
|
|
63
|
+
readonly client: PluginsClient;
|
|
64
|
+
readonly manager: PluginManager;
|
|
65
|
+
private readonly permissions;
|
|
66
|
+
private revision;
|
|
67
|
+
private bootstrap;
|
|
68
|
+
private syncing;
|
|
69
|
+
private renderErrors;
|
|
70
|
+
private unsubscribers;
|
|
71
|
+
private storeListeners;
|
|
72
|
+
private _storeVersion;
|
|
73
|
+
constructor(opts: RendererFrameworkOptions);
|
|
74
|
+
/** `useSyncExternalStore` plumbing for the bootstrap-derived view (plugin list etc.). */
|
|
75
|
+
get storeVersion(): number;
|
|
76
|
+
subscribe: (l: () => void) => (() => void);
|
|
77
|
+
private touch;
|
|
78
|
+
get currentBootstrap(): Bootstrap | undefined;
|
|
79
|
+
/** Fetches the bootstrap, activates enabled plugins and starts listening for changes. */
|
|
80
|
+
start(): Promise<void>;
|
|
81
|
+
stop(): void;
|
|
82
|
+
private hotReload;
|
|
83
|
+
private cacheBust;
|
|
84
|
+
/** Mirrors main-process state into the renderer manager. Calls are serialized. */
|
|
85
|
+
sync(force?: boolean): Promise<void>;
|
|
86
|
+
private reconcile;
|
|
87
|
+
private load;
|
|
88
|
+
/** Called by plugin error boundaries. Repeated render failures disable the plugin. */
|
|
89
|
+
reportRenderError(pluginId: string, error: unknown): void;
|
|
90
|
+
/** Builds `window.plugins.<ns>.<member>` from the preload manifest. Rebuilt on every sync. */
|
|
91
|
+
private installWindowPlugins;
|
|
92
|
+
/** Config handle usable from React (mirrors ctx.config through the management API). */
|
|
93
|
+
configOf<T extends Record<string, any>>(id: string): Pick<PluginConfig<T>, "get" | "set" | "reset">;
|
|
94
|
+
}
|
|
95
|
+
/** Generates the object behind `window.plugins` from a preload manifest. Functions only; no references to Node. */
|
|
96
|
+
export declare function buildPluginApis(bridge: FluxBridge, spec: Record<string, PreloadApiSpec>): Record<string, Record<string, (...args: any[]) => any>>;
|
|
97
|
+
/** Shares singletons (React, the runtime...) with plugin bundles, which import them from `globalThis.__FLUXPLUGIN_SHARED__`. */
|
|
98
|
+
export declare function installSharedModules(modules: Record<string, unknown>): void;
|
|
99
|
+
export type { IpcContract };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
**Single source of truth = main process.** `MainFramework` discovers/validates plugins, persists enabled state and permission
|
|
4
|
+
grants, owns IPC handlers. The renderer mirrors that via `flux:bootstrap` and runs renderer entries in its own `PluginManager`
|
|
5
|
+
(`runtime: "renderer"`). Same `fluxplugin` code runs on both sides.
|
|
6
|
+
|
|
7
|
+
## Lifecycle (per plugin)
|
|
8
|
+
`discovered → validated → loaded → initialized → activated → ready → deactivated → unloaded` (+ `disabled`, `error`).
|
|
9
|
+
Every phase has a timeout. A failure rolls back everything the plugin registered (ownership sweep), is logged, and never
|
|
10
|
+
reaches other plugins. 3 failures in 60 s auto-disable the plugin (persisted). Dependencies activate first and deactivate last;
|
|
11
|
+
cycles, missing deps and version mismatches block activation with a precise reason.
|
|
12
|
+
|
|
13
|
+
## Lazy activation
|
|
14
|
+
`activationEvents`: `onStartup`, `*`, `onCommand:<id>`, `onRoute:<pattern>`, `onEvent:<pattern>`, `onService:<name>`,
|
|
15
|
+
`onSlot:<name>`, `onIPC:<name>`. Statically declared `contributes.commands/menus` appear (as proxies) before the plugin loads.
|
|
16
|
+
|
|
17
|
+
## Extension surface (`PluginContext`)
|
|
18
|
+
events (typed, wildcard, priority, cancel) · hooks (`validate/before/around/transform/after/filter`) · commands (palette,
|
|
19
|
+
keybindings) · routes (params, lazy, layouts, guards, breadcrumbs, auto menu) · menus · UI slots (`before/after/replace/
|
|
20
|
+
append/prepend`) · declarative extension points (host-schema-validated data) · services · IPC (schema-validated, shared
|
|
21
|
+
contracts) · preload APIs (data-only, → `window.plugins.<ns>`) · storage · config (schema, defaults, migrations) · state ·
|
|
22
|
+
styles (removed on disable) · host APIs (`host.provide`, permission-gated).
|
|
23
|
+
|
|
24
|
+
## Why `window.plugins` is built in the renderer
|
|
25
|
+
`contextBridge` exposes objects once at preload time; plugins come and go at runtime. The preload exposes only `window.__fluxplugin`
|
|
26
|
+
(5 functions); the renderer builds the frozen `window.plugins` from the preload manifest on every change.
|
|
27
|
+
|
|
28
|
+
## Plugin bundles
|
|
29
|
+
`fluxplugin build` (esbuild) rewrites `react`, `@epf/*` imports to `globalThis.__FLUXPLUGIN_SHARED__` so there is exactly one
|
|
30
|
+
React/runtime instance. Hosts call `installSharedModules({...})` (see `examples/host/src/renderer.tsx`).
|
|
31
|
+
Renderer entries load via `fluxplugin://<id>/…` (Electron) or `/@plugins/<id>/…` (web mode); `main`/`plugin.json` are never served.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Plugin authors
|
|
2
|
+
|
|
3
|
+
1. `fluxplugin create my-plugin -t basic|dashboard|backend`
|
|
4
|
+
2. Edit `plugin.json` (id, version, `activationEvents`, `permissions`, `dependencies`, `engines`) and `src/main.ts`, `src/renderer.tsx`, `src/styles/*.css`.
|
|
5
|
+
3. `fluxplugin dev` → host hot-reloads. `validate` → `package --sign-key key` → `<id>-<version>.tgz`.
|
|
6
|
+
4. Install: `fluxplugin install my.tgz --dir plugins`, via the Plugin Manager UI, or any `PluginRegistry`.
|
|
7
|
+
|
|
8
|
+
Rules: register everything through `ctx`/declarative fields (auto-cleaned); clean timers in `ctx.lifecycle.onDeactivate`;
|
|
9
|
+
declare every permission you use; version with semver and declare `engines["fluxplugin"]`.
|
|
10
|
+
Hooks: `before.<op>` / `after.<op>` / `around.<op>` / `transform.<op>` / `validate.<op>` / `filter.<name>`.
|
|
11
|
+
Global typing: `declare module "fluxplugin" { interface PluginApis { stats: {...} } }` then `pluginApi("stats")`.
|
package/docs/RELEASE.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Publishing & verifying a release
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
pnpm install
|
|
5
|
+
pnpm typecheck && pnpm test # 121 tests
|
|
6
|
+
pnpm build # dist/ (ESM, shared chunks) + dist/types (.d.ts)
|
|
7
|
+
npm pack # fluxplugin-1.0.0.tgz — inspect with `tar tzf`
|
|
8
|
+
|
|
9
|
+
# Verify like a user, before publishing (offline-safe: installs the tarball):
|
|
10
|
+
export ELECTRON_SKIP_BINARY_DOWNLOAD=1
|
|
11
|
+
mkdir /tmp/t && cd /tmp/t && npm init -y && npm i /path/to/fluxplugin-1.0.0.tgz
|
|
12
|
+
FLUXPLUGIN_SPEC=file:/path/to/fluxplugin-1.0.0.tgz npx fluxplugin init my-app
|
|
13
|
+
cd my-app && npm install && npm run build && npm run dev:web
|
|
14
|
+
|
|
15
|
+
# Publish (both packages; `fluxplugin` first):
|
|
16
|
+
npm publish # in the repo root → fluxplugin
|
|
17
|
+
cd create-fluxplugin && npm publish # → create-fluxplugin (enables `npm create fluxplugin@latest`)
|
|
18
|
+
```
|
|
19
|
+
Check the names are free on npm first (`npm view fluxplugin`); if not, use a scope (`@you/fluxplugin`) and
|
|
20
|
+
search/replace the package name and `SHARED_MODULES` ids in `src/cli/shared-modules.ts`.
|
package/docs/SECURITY.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Security model (honest version)
|
|
2
|
+
|
|
3
|
+
**What is enforced**
|
|
4
|
+
* Renderer ↔ main: sandboxed preload exposes only `window.__fluxplugin`. The renderer may call **only** channels a plugin explicitly
|
|
5
|
+
exposed via `preload.expose`, host allow-listed channels, and `flux.plugins.*` (disable with `allowManagement:false`).
|
|
6
|
+
* IPC input/output validated by schema; stack traces never cross the boundary; `flux.*` names are reserved.
|
|
7
|
+
* Permissions are declared in the manifest, granted by the host (`requireApproval:true` → nothing until the user approves),
|
|
8
|
+
checked on every scoped API, persisted, and revocable live. Host APIs can be permission-gated.
|
|
9
|
+
* Integrity: sha256 of entry files in `integrity`; ed25519 manifest signatures with trusted keys (`trustedKeys`, `requireSignature`).
|
|
10
|
+
* Archives: path-traversal-safe extraction; HTTP registry checksums verified; asset protocol rejects traversal, manifest and main entry.
|
|
11
|
+
* Crash isolation: timeouts, rollback, error boundaries, auto-disable.
|
|
12
|
+
|
|
13
|
+
**What is NOT enforced — do not rely on it**
|
|
14
|
+
* Plugin `main` code runs in the Electron main process with full Node access. Permissions are an API contract, **not a sandbox**:
|
|
15
|
+
a malicious plugin can `require("fs")`. Only install plugins you trust; use signatures + `requireSignature`.
|
|
16
|
+
* Renderer plugin code shares the host page's JS realm; it can reach `window.__fluxplugin` and the management API.
|
|
17
|
+
For untrusted plugins you need iframe/utility-process isolation (not implemented).
|
|
18
|
+
* `@scope` scoped styles need Chromium ≥ 118.
|
|
Binary file
|