* 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.
* fix(plugin-oauth): surface real error and propagate state on local errors
Clicking "Connect Google Account" could show the generic
"Authentication failed!" snack with no detail. Two issues caused this:
1. The catch in connectOAuth swallowed the actual error. Log it and
include the message in the snack so failures are diagnosable.
2. When the Electron main process emitted a local OAuth error
(invalid_auth_url, failed_to_open_browser), the IPC payload had no
state, and handleRedirectError silently dropped any callback whose
state did not match. Echo the state from the auth URL on the main
side, and treat missing-state errors as trusted local failures on
the renderer side (they are not CSRF-relevant). This rejects the
pending flow immediately instead of waiting for the 5-minute timeout.
* fix(plugin-oauth): apply review feedback on connect-OAuth UX
- Split the try block in connectOAuth so a failure of _loadDynamicOptions
no longer surfaces as an "Authentication failed" snack on top of the
success snack. The OAuth connection itself succeeded; loadOptions has
its own per-field error reporting.
- Sanitize the surfaced error: collapse whitespace and cap to 200 chars
so a long HttpErrorResponse message doesn't blow up the snack.
- Log the message field instead of the raw Error, per CLAUDE.md
anti-pattern #11 (log history is exportable).
- Use undefined instead of null for the state echo in the main process
to match the renderer/preload signatures.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Google's Desktop OAuth client requires loopback redirect URIs
(http://127.0.0.1:<port>) and blocks embedded webviews. This replaces
the custom URI scheme + BrowserWindow approach with a temporary loopback
HTTP server and shell.openExternal for the system browser.
- Add PLUGIN_OAUTH_PREPARE IPC to start loopback server and return port
- Open auth URL in system browser instead of Electron BrowserWindow
- Make getRedirectUri() async to support IPC port retrieval
- Validate OAuth config before starting loopback server to prevent leaks
- Force 200 status on OPTIONS preflight responses in CORS bypass
Add OAuth support for plugins with PKCE, token persistence in local-only
IndexedDB, and Electron/web redirect handling. Extend two-way sync with
dueDay, dueWithTime, and timeEstimate field mappings including mutually
exclusive field clearing. Support remote issue deletion on task delete
and remote deletion detection during polling.
Add Google Calendar plugin (disabled from bundled builds pending legal
review) as a development reference for the plugin OAuth and two-way
sync APIs.
Additional fixes:
- Restore accidentally deleted focus-mode translation keys
- Remove full Task[] from deleteTasks action to prevent op-log bloat
- Add dueDay/deadlineDay format validation guards (#6908)
- Fix Android reminder alarm cancel intent matching
- Validate dueDay format to prevent false overdue from corrupted data
- Fix PKCE test expectation after utility consolidation