super-productivity/packages/plugin-api
oon arfiandwi 5ea0570f39
feat(plugins): generic OAuth hooks for issue-provider plugins (#8546)
* feat(plugins): enable OAuth-based issue-provider plugins

Generic, provider-agnostic plugin-framework hooks so an issue-provider plugin
that needs an exact OAuth redirect can work without any built-in code:

- OAuthFlowConfig.redirectUri: a plugin may declare an exact pre-registered
  callback; the host uses it for both the authorize request and token exchange.
- The Electron loopback honors a plugin-requested fixed port and rejects with a
  clear message when that port is already in use.
- Apply user-supplied clientId/clientSecret/redirectUri overrides onto a
  plugin's oauthConfig (bring-your-own OAuth app).
- Fix: merging a partial pluginConfig update no longer drops omitted keys and
  deep-merges nested objects (e.g. twoWaySync).

Split out of the Basecamp community-plugin work per PR #8507 feedback; contains
no provider-specific code.

* fix(plugins): address #8546 review

- validate OAuthFlowConfig.redirectUri per platform (loopback / same-origin /
  app scheme) and fail fast instead of hanging; restrict desktop loopback to 127.0.0.1
- warn when a client secret is dropped on web/native (bring-your-own credentials)
- validate the IPC loopback port to [1024,65535], register the error handler before
  listen(), and close the failed server
- merge pluginConfig via generic recursion instead of a hardcoded twoWaySync case
- nits: named token-store imports; fix stale prepareRedirectUri comment
- tests: redirectUri validation, the web client-secret warning, and generic merge

* fix(plugins): address #8546 round-2 review

- loopback error handler calls cleanupServer() so a post-listen runtime error
  doesn't leave the server ref / 5-min timer dangling
- share OAUTH_LOOPBACK_PORT_{MIN,MAX} between the renderer and Electron main so
  the bounds never drift; reject out-of-range (incl. 0/80/443) redirect ports early
- drop _getElectronLoopbackPort and parse the already-validated redirectUri once
- document that a bring-your-own clientSecret syncs via pluginConfig (override boundary)
- skip __proto__/constructor/prototype keys in the pluginConfig merge (defense-in-depth)
- test: prototype-pollution guard

* fix(plugins): address #8546 round-3 review

- reject native redirectUri overrides outright (closes CodeQL
  js/incomplete-url-scheme-check) via a pure, per-platform validateOAuthRedirectUri
  util (electron loopback / native reject / web same-origin)
- gate bring-your-own OAuth credentials to the desktop loopback flow and warn
  (instead of silently dropping clientId) when set on web/native
- namespace BYO under pluginConfig.oauthOverrides (was flat keys); document the
  convention on OAuthFlowConfig (public plugin API)
- shallow top-level pluginConfig merge: drop the deep recursion + proto guard;
  nested objects (e.g. twoWaySync) are replaced wholesale, matching callers
- pin the web redirect to /assets/oauth-callback.html via a shared constant so a
  same-origin wrong-path URI fails fast; note the desktop 127.0.0.1-only rule
- companion tests for each

* fix(plugins): strip desktop redirectUri on web/native OAuth flows

A plugin-declared redirectUri is the desktop loopback override; keeping it on
the web/native branches made a web/native-capable plugin throw at connect time
(the loopback URI fails web/native redirectUri validation). Strip it on those
branches so prepareRedirectUri falls through to the platform default. Document
redirectUri as desktop-only in the plugin API and fix a misleading test name.

* refactor(plugins): extract resolveEffectiveOAuthConfig and harden native fallthrough

Move the platform client/secret/redirectUri selection out of the bridge into a
pure, parameterized util so every branch is unit-testable (the IS_* platform
consts are module-level and cannot be mocked in karma). Also strip clientSecret
and redirectUri on the native fall-through — a native platform where the plugin
ships no matching client id — keeping both strictly desktop-only.

Optional hardening on top of the redirectUri fix; safe to drop independently.
2026-06-30 14:11:00 +02:00
..
src feat(plugins): generic OAuth hooks for issue-provider plugins (#8546) 2026-06-30 14:11:00 +02:00
.gitignore build(plugin-api): stop tracking generated source map 2026-05-11 14:38:52 +02:00
.npmignore feat(plugin-api): create foundational plugin API package 2025-06-27 18:13:19 +02:00
DEVELOPMENT.md fix(plugins): return dialog result #5239 (#8106) 2026-06-08 12:14:08 +02:00
package-lock.json feat(plugin-api): publish TypeScript definitions package to npm 2025-06-29 15:32:51 +02:00
package.json build: update links to match our new organization 2026-01-05 14:45:06 +01:00
publish.sh feat(plugin-api): create foundational plugin API package 2025-06-27 18:13:19 +02:00
PUBLISHING.md feat(plugin-api): create foundational plugin API package 2025-06-27 18:13:19 +02:00
README.md fix(plugins): expose focused task API to iframe plugins (#8291) 2026-06-15 14:57:34 +02:00
tsconfig.json feat(plugins): update plugin infrastructure and cleanup 2025-07-10 15:06:48 +02:00

@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 interface
  • PluginManifest - Plugin configuration
  • PluginHooks - Available hook types
  • PluginBaseCfg - Runtime configuration

Data Types

  • TaskData - Task information
  • ProjectData - Project information
  • TagData - Tag information

UI Types

  • DialogCfg - Dialog configuration
  • DialogResult - Dialog return value
  • SnackCfg - Notification configuration
  • PluginMenuEntryCfg - Menu entry configuration
  • PluginShortcutCfg - 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 notifications
  • notify - System notifications
  • showIndexHtmlAsView - Display plugin UI
  • openDialog - Show dialogs
  • getTasks - Read tasks
  • getArchivedTasks - Read archived tasks
  • getCurrentContextTasks - Read current context tasks
  • getSelectedTask - Read the task selected in the task detail panel
  • getFocusedTask - Read the currently focused task row, if any
  • addTask - Create tasks
  • getAllProjects - Read projects
  • addProject - Create projects
  • getAllTags - Read tags
  • addTag - Create tags
  • persistDataSynced - Persist plugin data
  • getAppState - 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.