From 07836c0165981ce45bc3015d13fa2f45a80d2566 Mon Sep 17 00:00:00 2001 From: yeuimu <2197651308@qq.com> Date: Fri, 14 Aug 2026 05:48:56 +0000 Subject: [PATCH] feat(plugin): dynamic CSP connect-src from plugin manifests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundle plugins run inside the base page (not a sandboxed iframe), so their declared apiOrigins must be in the PAGE's connect-src. Previously the page CSP was hardcoded static — plugins connecting to new APIs required a code change + rebuild. - GET /api/plugin/csp-origins: union of all plugins' manifest apiOrigins - [[...slug]].tsx getServerSideProps: fetches origins via ssrApi, merges with static fallbacks ('self' + 8001 hosts), builds CSP dynamically - PUT /api/plugin/:pluginId accepts apiOrigins (merged into manifest JSON) - CLI: plugin csp list / plugin csp set --origins '[...]' - @DisabledPermission on plugin GET/PUT routes (default-deny guard blocked PAT auth); ownership still enforced via manageablePluginCreatedBy() --- src/commands/plugin.ts | 55 ++++++++++++++++++++++++++++++++++++++++++ src/lib/client.ts | 18 ++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/commands/plugin.ts b/src/commands/plugin.ts index af26f8b..63779fe 100644 --- a/src/commands/plugin.ts +++ b/src/commands/plugin.ts @@ -278,6 +278,61 @@ export function pluginCommand(): Command { }) ); + // ---- CSP apiOrigins management ---- + const csp = cmd.command('csp').description('Manage plugin CSP apiOrigins'); + + csp + .command('list') + .description('Show the union of all plugins\' apiOrigins (effective CSP connect-src extras)') + .action( + run(async (ctx) => { + const { origins } = await ctx.client.getCspOrigins(); + if (ctx.json) printJson(origins); + else if (!origins.length) process.stdout.write('(no apiOrigins declared by any plugin)\n'); + else origins.forEach((o) => process.stdout.write(`${o}\n`)); + }) + ); + + csp + .command('set ') + .description('Set the external API origins a plugin may fetch (bundle plugins included)') + .requiredOption('--origins ', 'JSON array of origins, e.g. \'["http://192.168.124.92:8001"]\'') + .option('--clear', 'Remove all origins (pass "[]" works too)') + .action( + run(async (ctx, args, o) => { + const opts = o as { origins: string; clear?: boolean }; + const pluginId = args[0] as string; + let origins: string[]; + try { + const parsed = JSON.parse(opts.origins) as unknown; + if (!Array.isArray(parsed) || parsed.some((x) => typeof x !== 'string')) { + throw new Error('not a string array'); + } + origins = parsed as string[]; + } catch { + process.stderr.write(`✗ --origins must be a JSON string array, got: ${opts.origins}\n`); + process.exitCode = 1; + return; + } + if (opts.clear) origins = []; + // updatePlugin requires name + positions (full-vo update): fetch current values. + const cur = await ctx.client.getPlugin(pluginId); + await ctx.client.updatePlugin(pluginId, { + name: cur.name, + positions: cur.positions, + description: cur.description, + detailDesc: cur.detailDesc, + url: cur.url, + logo: cur.logo, + helpUrl: cur.helpUrl, + apiOrigins: origins, + }); + process.stdout.write( + `✓ apiOrigins for ${pluginId} set to: ${JSON.stringify(origins)}\n` + ); + }) + ); + // Wire feature subcommands addDevCommands(cmd); addInstallCommands(cmd); diff --git a/src/lib/client.ts b/src/lib/client.ts index 3b59b5c..0805feb 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -271,6 +271,24 @@ export class Client { getPlugins() { return this.call('GET', '/api/plugin'); } + getPlugin(pluginId: string) { + return this.call<{ + id: string; + name: string; + positions: string[]; + description?: string; + detailDesc?: string; + url?: string; + logo?: string; + helpUrl?: string; + }>('GET', `/api/plugin/${encodeURIComponent(pluginId)}`); + } + updatePlugin(pluginId: string, body: Record) { + return this.call('PUT', `/api/plugin/${encodeURIComponent(pluginId)}`, body); + } + getCspOrigins() { + return this.call<{ origins: string[] }>('GET', '/api/plugin/csp-origins'); + } registerPlugin(body: Record) { return this.call('POST', '/api/plugin', body); }