Originally created by: Akarsh-Hegde
Centralized PM integrations system supporting all 5 trackers (Jira, Linear, GitHub, Trello, Azure DevOps) working perfectly from both the setup wizard and dashboard.
meridian-oauth — config-free OAuth/token engine reused by both daemon and traysave_integration_token command eliminates "run meridian config edit" dead-end<ConnectTrackers> component driven by ui/lib/integrations.ts SSOT🤖 Generated with Claude Code
Added support for managing provider credentials and refreshing connections from the UI.
Bug Fixes
Tickets: #341
Tickets: #342
Tickets: #343
Tickets: #347
Tickets: #351
Originally posted by: coderabbitai[bot]
✨ Finishing Touches
🧪 Generate unit tests (beta)
- [ ] Create PR with unit tests - [ ] Commit unit tests in branch `feat/in-process-oauth`Comment
@coderabbitai helpto get the list of available commands.Originally posted by: Akarsh-Hegde
Code Review — 10-angle automated analysis
15 findings (10 bugs, 1 cleanup across
meridian-oauth,tray/src-tauri/src/commands/integrations.rs,ui/components/IntegrationConnect.tsx)🔴 Critical / High bugs
1.
jira.rs:219—ensure_freshdiscards rotated refresh token on save failure → permanent lockoutAtlassian rotates the refresh token on every use. If
flow::refresh()succeeds butstore::save(&t)fails (disk full, permissions), the new token is discarded and the store retains the now-invalidated old token. Every subsequent call gets a 401 — permanent lockout until re-auth. Fix: write new token to temp file and rename-into-place before returning success.2.
jira.rs:28—refresh_lock()is per-process — daemon + tray race consumes same Atlassian refresh tokenThe
OnceLock<Mutex<()>>is a per-process static. Daemon background sync and a tray user action can both callensure_fresh()simultaneously, both read the expired store, both POST to Atlassian — the second POST 401s because the first call already rotated and consumed the token. Fix: serialize across processes with a file lock on the OAuth store path.3.
flow.rs:39—TokenResponse.expires_inmissing#[serde(default)]— login fails hard if Atlassian omits the fieldexpires_inis a required serde field. Atlassian can omit it for certain grant types or partial error responses.serde_json::from_strthen returns a parse error, failing the entire login.#[serde(default)]degrades gracefully:expires_at = now, triggering an immediate refresh on next use rather than a hard failure.4.
integrations.rs:328—save_integration_tokenclears OAuth JSON but not the error sentinel — stale sentinel corruptsget_oauth_statusIf Jira OAuth fails (sentinel written), then user connects via API token, the sentinel is never cleared.
get_oauth_statusstill returns the old error and the dashboard shows a broken state even though the token is working. Fix: havesave_integration_tokendelete the error sentinel for the provider when it writes a successful token.5.
jira.rs:157— Jira and Trello share fixed loopback port 9123 — concurrent flows fail with EADDRINUSEBoth providers call
flow::run_authcode_flowon port 9123. If user clicks Connect Jira then Connect Trello (or double-clicks), the secondTcpListener::bindreturnsEADDRINUSE, the spawned task writes the error sentinel. Fix: bind port 0 and derive the redirect URI from the OS-assigned port; add a per-providerAtomicBoolin-flight guard.6.
integrations.rs:594— inner task_ => Ok(())catch-all silently no-ops new providersIf a developer adds a new provider to
OAUTH_PROVIDERSbut forgets to update the innermatch, the spawned task returnsOk(())without running any login. The user seesstarted=truebut no token is ever written. Fix: replace catch-all withbail!("unhandled provider: {}", task_provider).7.
integrations.rs:1296—forward_oauth_envcallsstd::env::set_varon a Tokio worker thread — UB under concurrent accessset_var/getenvare not thread-safe on POSIX. Concurrentgetenv(reqwest proxy detection, a second OAuth flow) races withset_var. Rust 1.93+ warns onset_varin multithreaded programs. Fix: read env keys inside the spawned async block afterspawn, or pass values as captured variables rather than mutating the global env.8.
jira.rs:194— Empty OAuth client secret accepted silently until mid-flowWhen
MERIDIAN_JIRA_OAUTH_CLIENT_SECRETis not set at build time,DEFAULT_CLIENT_SECRET = "". The Connect button shows, the user clicks it, and only then do they get a cryptic error. CLAUDE.md: "Validate all input at system boundaries." Fix: disable/hide the Jira OAuth button or log a startup warning when the compiled-in secret is empty.🟡 Medium bugs
9.
integrations.rs:1077— blockingstd::fsI/O inside async Tauri commandupsert_envcallsstd::fs::read_to_string/std::fs::writesynchronously on Tokio's worker thread insidesave_integration_token. On a slow disk this starves the poll loop, health refresh, and active-session updates. Fix:tokio::fsequivalents ortokio::task::spawn_blocking.10.
IntegrationConnect.tsx:1995—pollRef.currentassigned afterawait— interval leaks on unmountpollRef.current = idis assigned only afterawait mutate(...)resolves. If the component unmounts while that await is in flight, the cleanupuseEffectruns before the assignment —clearIntervalis never called and the 2-second poll fires indefinitely. Fix: assign the interval id before the first await.11.
IntegrationConnect.tsx:2030—tracker.token!non-null assertion throws if method is undefinedconst method = tracker.token!will throw a runtimeTypeErrorifTokenSetupis ever rendered for a tracker that has notokenmethod in the config. Fix: null-check or optional chaining.🟠 Cleanup / conventions
12.
integrations.rs:80— hand-rolledparse_env/upsert_envdiverge from dotenvyThe daemon loads
~/.meridian/.envviadotenvy::dotenv_override()which handles edge cases (export prefix, backslash continuation, quoted values). The tray's hand-rolled parser doesn't — a value likeKEY="foo bar"is read differently by the two processes. Fix: usedotenvyin both, or ensure the hand-rolled parser handles all cases dotenvy does.13.
jira.rs:275—discover_clouderror message on non-2xx is generic — actual Atlassian error is swallowedOn 401/403,
resp.text()is called but the body is not included in the returned error message. Fix:bail!("accessible-resources → {status}: {text}").14.
jira.rs:281—discover_cloudsilently picks the first cloud site — wrong instance for multi-org usersWhen an Atlassian account has multiple accessible sites, the first is taken silently (only a warning logged). A user in multiple orgs may be connected to the wrong Jira instance with no recourse. Future: surface site selection in the setup wizard.
15.
integrations.rs:238—strip_env_keysmissing trailing newline — corrupts nextupsert_envappendkept.join("\n")produces no trailing newline. The nextupsert_envcall appends\nKEY=valueonto the last retained key line, producingLAST_KEY=valuenewKEY=newvalue— corrupting the env file. Fix:kept.join("\n") + "\n"(orkept.join("\n") + if kept.is_empty() { "" } else { "\n" }).Findings [#3], 4, 6, 7, 9, 10, 11, 13, 15 are being addressed in a follow-up commit on this branch. Findings [#1], 2, 5, 8, 12, 14 are architectural — raised for awareness; fixes scoped separately.
Related
Tickets:
#1Tickets:
#3Originally posted by: Akarsh-Hegde
Review follow-up — fixes applied (commit 897b099)
The following findings from the automated review have been addressed:
TokenResponse.expires_inmissing#[serde(default)]— hard login failure if Atlassian omits field#[serde(default)]; default 0 → immediate refresh on next use instead of a parse errorsave_integration_tokendidn't clear OAuth error sentinel → stale error shown after token connectsave_integration_tokenalongside the OAuth JSON removal_ => Ok(())catch-all in inner OAuth task match silently no-ops new providersbail!("unhandled OAuth provider: {task_provider}")OAuthSetuppoll interval leaks on unmount (component unmounts duringawait mutate, cleanup runs beforepollRefis assigned)mountedRefguard — if unmounted during the await,setIntervalis never calledtracker.token!non-null assertion inTokenSetupthrowsTypeErrorif rendered for a tracker without a token methodNot addressed in this PR (architectural, need separate scope):
ensure_freshloses rotated token on save failure → atomic write pattern neededrefresh_lock()is per-process → cross-process file lock needed to prevent daemon+tray token raceforward_oauth_envcallsset_varon Tokio worker thread → pass env values explicitly (requiresmeridian_oauthAPI change)std::fsinupsert_envinside async context →tokio::fsorspawn_blockingparse_env/upsert_envdiverge fromdotenvy→ unifyFinding #13 was a false positive —
discover_cloudalready includes the response body in itsbail!message.PR is ready for review.
Ticket changed by: Akarsh-Hegde