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:
yeuimu
2026-08-14 05:48:56 +00:00
parent 5ed6569535
commit 07836c0165
2 changed files with 73 additions and 0 deletions
+18
View File
@@ -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);
}