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
|
// Wire feature subcommands
|
||||||
addDevCommands(cmd);
|
addDevCommands(cmd);
|
||||||
addInstallCommands(cmd);
|
addInstallCommands(cmd);
|
||||||
|
|||||||
@@ -271,6 +271,24 @@ export class Client {
|
|||||||
getPlugins() {
|
getPlugins() {
|
||||||
return this.call<unknown[]>('GET', '/api/plugin');
|
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>) {
|
registerPlugin(body: Record<string, unknown>) {
|
||||||
return this.call<unknown>('POST', '/api/plugin', body);
|
return this.call<unknown>('POST', '/api/plugin', body);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user