Originally created by: Akarsh-Hegde
Adds a full Authorization Code + PKCE browser login for Jira, wired into the installer so a new user connects without minting an API token. Static JIRA_BASE_URL / JIRA_EMAIL / JIRA_API_TOKEN stays as a fallback; OAuth wins when a token store exists.
src/intelligence/oauth/| file | role |
|---|---|
pkce |
S256 verifier / challenge / state |
store |
daemon-writable ~/.meridian/oauth/jira.json (0600), refresh-aware — rotating refresh tokens are persisted back (unlike static tokens) |
flow |
generic loopback-redirect engine: browser → code → token exchange → refresh |
jira |
Atlassian 3LO wiring — login, refresh-before-use, and a JiraReqCtx resolver |
api.atlassian.com/ex/jira/{cloudId} with Bearer (cloud-id from accessible-resources); basic auth still hits the site URL — both via one resolve().client_id is baked into DEFAULT_CLIENT_ID (PKCE has no secret to protect); JIRA_OAUTH_CLIENT_ID overrides it./myself doctor probe works under OAuth via the read:jira-user scope; login() fails loudly if no refresh token comes back (i.e. offline_access not granted); redirect uses 127.0.0.1 (Atlassian's console rejects localhost, and it dodges the localhost→::1 bind trap).meridian setup (bundle) and ./install.sh (source) now offer "Connect Jira in your browser? [Y/n]" and run the login inline — no separate command, no extra restart. The bundle runs it inline (prebuilt binary present); source defers it to after cargo build, before the daemon starts. API token is the fallback on decline / failure / org-blocked apps. Shared helper: scripts/lib-jira-setup.sh; meridian oauth-login jira also works through the CLI wrapper.
Each user connects to their own Jira site — discovered from whoever signs in. The baked-in client id is only the app identity, not a site.
docs/jira-oauth-app.mdThe one-time Atlassian app setup: scopes (read:jira-work write:jira-work read:jira-user + runtime offline_access), the http://127.0.0.1:9123/callback callback, and the Distribution → Distributable toggle that gates all non-Meridiona users. Linked from DEFAULT_CLIENT_ID and SETUP.md, with a pre-GA checklist.
feat(tasks) (33ed1c1) — on-demand PM sync, provider filter, and a dashboard sync button (meridian tasks-sync).fix(health) (923c315) — tilde expansion in the DB-path check, accurate doctor banner title, doctor DB probe.0600 perms), authorize-URL builder, JiraReqCtx URL/auth, config-resolution (store-presence vs basic vs nothing). Bash: lib-jira-setup source-and-call smoke + bash -n on the installers. Full pre-push suite (fmt + clippy + cargo test + UI build + UI tests + security audit) green.api.atlassian.com gateway fetch). Earlier "tickets fetched" testing was actually the basic-auth fallback — no token store existed at that moment. The basic-auth path is unchanged and works.Linear OAuth (same PKCE engine) — held for a follow-up.
🤖 Generated with Claude Code
Originally posted by: adityaharishch
Code Review — PR [#213] (OAuth PKCE for Jira)
🔴 Critical Issues (2)
1.
jira_update_enabledsilently disabled for zero-config OAuth userssrc/config.rs:300JIRA_BASE_URLorJIRA_OAUTH_CLIENT_ID, but not for the OAuth token store file. Users who follow the recommended path (meridian oauth-login jirawith zero env vars) will have their OAuth fully configured for task classification but worklog posting will be silently disabled.meridian oauth-login jirawith no env vars → OAuth store created at~/.meridian/oauth/jira.json→parse_jira()detects it and returns configured → butjira_configuredgate on line 300 is false because neither env var is set →jira_update_enabledbecomes false → worklogs never post despite OAuth workingFix: Add OAuth store check to the gate:
:::rust
let jira_configured =
std::env::var("JIRA_BASE_URL").is_ok()
|| std::env::var("JIRA_OAUTH_CLIENT_ID").is_ok()
|| crate::intelligence::oauth::store::exists("jira"); // ADD THIS
2. Missing 'error' handler on spawn() in tasks sync endpoint
ui/app/api/tasks/sync/route.ts:22-36{ok: false}.Fix: Add error handler before the timeout handler:
:::typescript
child.on('error', (err) => {
clearTimeout(timer)
resolve({ ok: false, stdout, stderr:
spawn error: ${err.message}})})
🟠 High Severity (1)
3. Missing timeouts on HTTP clients in OAuth flow
src/intelligence/oauth/flow.rs:145,src/intelligence/oauth/jira.rs:88post_token()anddiscover_cloud()createreqwest::Client::new()with no timeout. If Atlassian's servers are slow,ensure_fresh()can block the main daemon poll loop indefinitely.Fix: Add timeout to both clients using the pattern from
src/health/jira.rs::::rust
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(6))
.build()?;
🟡 Medium Severity (1)
4. Race condition in
ensure_fresh()token refreshsrc/intelligence/oauth/jira.rs:151-172ensure_fresh()concurrently, both load the same refresh_token and POST to Atlassian. Since Atlassian rotates the token on each use, the second request gets a 401.tokio::sync::Mutexto serialize refresh calls.🔵 Low Severity (1)
5. Temporary token file created world-readable
src/intelligence/oauth/store.rs:81open()with mode 0o600.Summary: 2 critical bugs (silent feature disable + crash), 1 high (blocking hang), 1 medium (transient auth failure), 1 low (token exposure window).
Related
Tickets:
#213Originally posted by: adityaharishch
Inline: config.rs:300 — CRITICAL
This gate only checks env vars, missing OAuth store check. Zero-config OAuth users have:
parse_jira()→ returnsSome(detected OAuth store viastore::exists("jira"))jira_configured→ false (no env vars)jira_update_enabled→ false (silent disable)Result: Task classification works, worklog posting is silently disabled.
Fix: Add
|| crate::intelligence::oauth::store::exists("jira")to the condition.Originally posted by: adityaharishch
Inline: ui/app/api/tasks/sync/route.ts:22 — CRITICAL
Missing: No
.on('error')handler on the ChildProcess.If spawn fails (binary not found, permission denied), Node.js emits an 'error' event. Without a handler, this crashes the API handler instead of returning graceful error response.
Scenario: All candidate paths fail
access()check → fallback path doesn't exist →spawn()errors → unhandled exception crashes the route.Fix: Add error handler before timeout:
Originally posted by: adityaharishch
Inline: src/intelligence/oauth/flow.rs:145 + jira.rs:88 — HIGH
Both
post_token()anddiscover_cloud()create HTTP clients without timeouts:Issue: No timeout on HTTP requests. If Atlassian's servers are slow or unresponsive,
ensure_fresh()(called during token refresh) can block the daemon's main poll loop indefinitely.Context:
src/health/jira.rscorrectly usesClient::builder().timeout(Duration::from_secs(6)).Fix: Reuse the timeout pattern in both functions:
Originally posted by: adityaharishch
Inline: src/intelligence/oauth/jira.rs:151 — MEDIUM
Race condition: No locking on the load-refresh-save sequence.
Scenario: PM sync and health check both call
ensure_fresh()concurrently → both load samerefresh_token→ both POST to Atlassian → both get 401 (token rotated by first request) → second caller persists invalid token → subsequent auth fails.Impact: Self-heals on next poll, but causes transient auth-failed warnings during normal operation.
Fix: Serialize token refreshes using
tokio::sync::Mutexor similar.Originally posted by: adityaharishch
Inline: src/intelligence/oauth/store.rs:81 — LOW
Issue: File is world-readable for the microseconds between
write()andset_permissions().Race window: Other users on shared systems can read the Jira OAuth tokens during this brief window.
Fix: Either:
open(O_CREAT | O_EXCL)with mode 0o600mkdir(0o700)first, then writeThis is low severity (narrow window, home directory context), but worth fixing while touching this code.
Originally posted by: adityaharishch
Review Summary
Reviewed: PR [#213] (OAuth PKCE for Jira) — 35 files, ~2500 LOC
Findings: 5 issues found (2 critical, 1 high, 1 medium, 1 low)
Critical path: The two critical issues both affect the recommended zero-config OAuth path:
Recommendation: Fix the two critical issues before merging. The high and medium severity issues are important but not merge-blockers (timeouts can be added in follow-up, concurrency guard is edge case).
Related
Tickets:
#213Originally posted by: Akarsh-Hegde
✅ All review issues resolved
Addressed all 5 feedback items from the code review:
jira_update_enabledsilently falsestore::exists("jira")check to thejira_configuredgate inconfig.rs:300spawn()in tasks/sync API crashes on missing binary.on('error')listener before timeout handler inroute.ts:36post_token(),discover_cloud()) had no timeout, could block daemon indefinitelyflow.rs:145andjira.rs:92ensure_fresh()calls cause race condition → lost refresh tokens → 401 errorstokio::sync::Mutexto serialize all token refreshes injira.rs:155OpenOptions::mode(0o600)to create file with correct perms upfront instore.rs:74Verification
The fixes are pushed and ready for review.
Originally posted by: adityaharishch
✅ Verification Complete — All Issues Fixed
Reviewed the fixes in commit 6b8be49. All 5 findings are correctly addressed:
✅ CRITICAL [#1]: config.rs:300 — Zero-config OAuth disabled
Fix: Added
|| crate::intelligence::oauth::store::exists("jira")to the jira_configured gate✅ CRITICAL [#2]: tasks/sync/route.ts:36 — Missing error handler
Fix: Added
child.on('error', (err) => { ... })before timeout handler✅ HIGH: flow.rs:145 + jira.rs — Missing HTTP timeout
Fix: Added
Client::builder().timeout(Duration::from_secs(6)).build()?to both✅ MEDIUM: jira.rs:155 — Concurrency race in ensure_fresh()
Fix: Added
let _guard = refresh_lock().lock().await;to serialize refresh calls✅ LOW: store.rs:74 — Temp file world-readable race
Fix: Changed from write-then-chmod to OpenOptions with mode(0o600) upfront
Verification:
Ready to merge — all critical and high-priority issues resolved with comprehensive test coverage.
Related
Tickets:
#1Tickets:
#2Ticket changed by: adityaharishch
Originally posted by: adityaharishch
🎉 This PR is included in version 1.35.0 🎉
The release is available on:
v1.35.0Your semantic-release bot 📦🚀