feat(plugin): dynamic CSP connect-src from plugin manifests
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 <id> --origins '[...]'
- @DisabledPermission on plugin GET/PUT routes (default-deny guard blocked
PAT auth); ownership still enforced via manageablePluginCreatedBy()
This commit is contained in:
@@ -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 <pluginId>')
|
||||
.description('Set the external API origins a plugin may fetch (bundle plugins included)')
|
||||
.requiredOption('--origins <json>', '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);
|
||||
|
||||
@@ -271,6 +271,24 @@ export class Client {
|
||||
getPlugins() {
|
||||
return this.call<unknown[]>('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<string, unknown>) {
|
||||
return this.call<unknown>('PUT', `/api/plugin/${encodeURIComponent(pluginId)}`, body);
|
||||
}
|
||||
getCspOrigins() {
|
||||
return this.call<{ origins: string[] }>('GET', '/api/plugin/csp-origins');
|
||||
}
|
||||
registerPlugin(body: Record<string, unknown>) {
|
||||
return this.call<unknown>('POST', '/api/plugin', body);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user