* feat(plugin): add generic request capability to PluginAPI * feat(plugin): gate PluginAPI.request behind manifest allowedHosts PluginAPI.request is otherwise reachable to any host the shared SSRF filter does not block. Add a manifest-declared, host-enforced allowlist so a plugin's outbound reach is explicit and reviewable at install. - PluginManifest.allowedHosts: exact hostnames the plugin may reach. - PluginBridgeService enforces it before the shared HTTP/SSRF layer: exact host, case-insensitive, trailing-dot tolerant, port-agnostic; userinfo tricks resolved via URL parsing; fail-closed (empty/omitted allowedHosts disables request entirely). - validatePluginManifest rejects malformed allowedHosts at install. Companion tests: bridge enforcement (reject non-declared host, fail-closed on undefined/empty, case-insensitive + trailing-dot, userinfo-trick blocked) and manifest validation. * style(plugin): fix prettier formatting flagged by CI in request API The generic-request commit slipped past checkFile on three files; CI prettier flagged the multi-line request() signature and a long spec assertion string. Formatting only, no behavior change. * feat(plugin): require "http" permission for PluginAPI.request Network egress becomes an explicit, opt-in capability (like nodeExecution): request now requires "http" in the manifest permissions in addition to a matching allowedHosts entry. Missing either is fail-closed. Enforced host-side (before the shared SSRF layer) on both the iframe and Function-sandbox paths; the "http" permission gets a human-readable line in the plugin security info. Tests: fail-closed without the "http" permission. * feat(plugin): surface plugin network reach in the plugin-management UI Render the manifest allowedHosts as a chip-set beside permissions/hooks (with a count in the collapsible title), so a plugin's outbound network reach is reviewable in-app instead of only in the raw manifest. Tests: allowedHosts shown in the collapsible title, omitted when none. * feat(plugin): block redirects on PluginAPI.request (SSRF-via-redirect) Only the initial request URL is allowlist/SSRF-checked, but HttpClient/XHR auto-follows 3xx — so a declared host could 302 to a private/metadata IP and return internal content to the plugin. Execute the request path via fetch with redirect:"error" so any redirect is refused instead of chased. - New PluginHttpHelperOpts.blockRedirects (default false); PluginBridge sets it for request. Issue-provider HTTP is untouched (still HttpClient). - fetch executor preserves the HttpClient contract callers depend on: query params, timeout via AbortController (covering the body read, not just headers), text/json parsing, and non-2xx rejecting with .status + .error. - Only plain objects/arrays are JSON-serialized; FormData/Blob/URLSearchParams/ ArrayBuffer(View) pass through, and Content-Type: application/json is added only for the JSON case (HttpClient parity). - Web/desktop only: on native, Capacitor patches fetch and ignores redirect/signal, so native falls back to HttpClient (documented limitation). - Blocks all redirects incl. benign same-host ones; manual per-hop re-validation is not possible on the web (cross-origin Location is opaque). Documented. Tests: redirect refused, .status/.error parity, SSRF still pre-checked, body pass-through, params + responseType:text, real-timer timeout, and native falls back to HttpClient. * fix(plugin): gate allowedHosts UI on the "http" capability The plugin-management panel showed a "Network access" section (and title count) whenever allowedHosts was non-empty, regardless of permissions — advertising reach for a capability the bridge rejects without the "http" permission. Gate the section, the chip loop, and the title count on a getNetworkReachHosts() helper (hosts only when permissions includes "http"). Addresses review on #8721. * docs(plugin): note _requestNoRedirect bypasses NetworkRetryInterceptorService The fetch path skips Angular HTTP_INTERCEPTORS, so PluginAPI.request GETs lose the single status-0 retry the HttpClient paths keep. Inherent to redirect:"error" (XHR/HttpClient cannot block redirects). Flagged in review on #8721. |
||
|---|---|---|
| .. | ||
| src | ||
| .gitignore | ||
| .npmignore | ||
| DEVELOPMENT.md | ||
| package-lock.json | ||
| package.json | ||
| publish.sh | ||
| PUBLISHING.md | ||
| README.md | ||
| tsconfig.json | ||
@super-productivity/plugin-api
Official TypeScript definitions for developing Super Productivity plugins.
Installation
npm install @super-productivity/plugin-api
Usage
TypeScript Plugin Development
import type {
PluginAPI,
PluginManifest,
PluginHooks,
} from '@super-productivity/plugin-api';
// Your plugin code with full type support
PluginAPI.registerHook(PluginHooks.TASK_COMPLETE, (taskData) => {
console.log('Task completed!', taskData);
PluginAPI.showSnack({
msg: 'Task completed successfully!',
type: 'SUCCESS',
ico: 'celebration',
});
});
// Register a header button
PluginAPI.registerHeaderButton({
label: 'My Plugin',
icon: 'extension',
onClick: () => {
PluginAPI.showIndexHtmlAsView();
},
});
// Register a keyboard shortcut
PluginAPI.registerShortcut({
id: 'my_shortcut',
label: 'My Custom Shortcut',
onExec: () => {
PluginAPI.showSnack({
msg: 'Shortcut executed!',
type: 'SUCCESS',
});
},
});
Plugin Manifest
{
"name": "My Awesome Plugin",
"id": "my-awesome-plugin",
"manifestVersion": 1,
"version": "1.0.0",
"minSupVersion": "13.0.0",
"description": "An awesome plugin for Super Productivity",
"hooks": ["taskComplete", "taskUpdate"],
"permissions": ["showSnack", "getTasks", "addTask", "showIndexHtmlAsView"],
"iFrame": true,
"uiKit": true,
"icon": "icon.svg"
}
Available Types
Core Types
PluginAPI- Main plugin API interfacePluginManifest- Plugin configurationPluginHooks- Available hook typesPluginBaseCfg- Runtime configuration
Data Types
TaskData- Task informationProjectData- Project informationTagData- Tag information
UI Types
DialogCfg- Dialog configurationDialogResult- Dialog return valueSnackCfg- Notification configurationPluginMenuEntryCfg- Menu entry configurationPluginShortcutCfg- Keyboard shortcut configuration
Plugin Development Guide
1. Available Hooks
enum PluginHooks {
TASK_COMPLETE = 'taskComplete',
TASK_UPDATE = 'taskUpdate',
TASK_DELETE = 'taskDelete',
FINISH_DAY = 'finishDay',
LANGUAGE_CHANGE = 'languageChange',
PERSISTED_DATA_CHANGED = 'persistedDataChanged',
ACTION = 'action',
}
PERSISTED_DATA_CHANGED fires on any persistent-data change to this
plugin after the host has finished its initial boot load — including
remote sync deliveries and bulk imports. Handler receives no payload;
re-call loadSyncedData(key?) for any key your plugin tracks to get
fresh data (scoped to the calling plugin). Contract: call
loadSyncedData() on plugin init for the initial state; then use this
hook for subsequent changes. There is no replay-on-register, no
per-key discrimination in the event, and no guaranteed ordering across
rapid changes. Handlers must be idempotent.
2. Required Permissions
Add these to your manifest.json based on what your plugin needs:
showSnack- Show notificationsnotify- System notificationsshowIndexHtmlAsView- Display plugin UIopenDialog- Show dialogsgetTasks- Read tasksgetArchivedTasks- Read archived tasksgetCurrentContextTasks- Read current context tasksgetSelectedTask- Read the task selected in the task detail panelgetFocusedTask- Read the currently focused task row, if anyaddTask- Create tasksgetAllProjects- Read projectsaddProject- Create projectsgetAllTags- Read tagsaddTag- Create tagspersistDataSynced- Persist plugin datagetAppState- Read-only snapshot of application state
3. Plugin Structure
my-plugin/
├── manifest.json
├── plugin.js
├── index.html (optional, if iFrame: true)
└── icon.svg (optional)
4. Example Plugin
// plugin.js
console.log('My Plugin initializing...', PluginAPI);
// Register hook for task completion
PluginAPI.registerHook(PluginAPI.Hooks.TASK_COMPLETE, function (taskData) {
console.log('Task completed!', taskData);
PluginAPI.showSnack({
msg: '🎉 Task completed!',
type: 'SUCCESS',
ico: 'celebration',
});
});
// Register header button
PluginAPI.registerHeaderButton({
label: 'My Plugin',
icon: 'dashboard',
onClick: function () {
PluginAPI.showIndexHtmlAsView();
},
});
// Read full app state
const state = await PluginAPI.getAppState();
License
MIT - See the main Super Productivity repository for details.
Contributing
Please contribute to the main Super Productivity repository.