Skip to content

fix(webhooks): resolve GitHub installation_id for OAuth tenant link - #606

Open
yuvrxj-afk wants to merge 1 commit into
mainfrom
fix/webhook-tenant-link
Open

fix(webhooks): resolve GitHub installation_id for OAuth tenant link#606
yuvrxj-afk wants to merge 1 commit into
mainfrom
fix/webhook-tenant-link

Conversation

@yuvrxj-afk

@yuvrxj-afk yuvrxj-afk commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Problem

GitHub returns installation_id in neither the OAuth token body nor the callback query — the login/oauth/authorize callback is just ?code=&state=. Webhook tenant-link resolution had nothing to key on, returned null, and inbound GitHub webhooks never routed to a tenant. Slack and Linear work because their identity is in the token body.

Change

  • Propagate OAuth callback query params into tenant-link resolution via mergeOAuthProviderData (token body wins on collision), threaded through the tunnel payload, so query-only identity like installation_id can route.
  • GitHub resolver falls back to GET /user/installations with the access token when installation_id is absent, using the first installation.

Tests

  • packages/corsair/tests/oauth-callback-params.test.ts — 3 passing
  • packages/github/webhooks/oauth-tenant-link.test.ts — 4 passing

Pairs with corsairdev/hub fix/webhook-tenant-link (hub forwards the callback params and resolves installation_id for managed connections). Both are additive; the GitHub webhook path needs both.

Summary by CodeRabbit

  • New Features

    • OAuth integrations now support provider callback parameters, including GitHub installation details and Slack workspace information.
    • Provider data is combined consistently, with token values taking precedence when information overlaps.
    • GitHub integration can automatically discover the first available installation when installation details aren’t included.
  • Bug Fixes

    • Improved handling of missing, invalid, or unavailable GitHub installation data.
    • OAuth tenant linking now fails safely when provider requests or responses are unsuccessful.

GitHub sends installation_id in neither the token body nor the callback
query, so tenant-link resolution returned null and GitHub webhooks never
routed to a tenant. Merge OAuth callback query params under the token body
(token wins) and fall back to GET /user/installations in the GitHub resolver.
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
www Ready Ready Preview Aug 4, 2026 7:54pm

Request Review

@github-actions github-actions Bot added core Changes in packages/corsair plugin Changes inside a plugin package labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Plugin PR scorecard — packages/github

Check Status Notes
R1 — Scope: plugin files only Out of scope: packages/corsair/oauth/index.ts, packages/corsair/tests/oauth-callback-params.test.ts, packages/corsair/tunnel/index.ts
R2 — Tests with assertions
R3 — Description Description section is empty or placeholder
R3 — Linked issue / claim ⚠️ No "Fixes #…" or claim link — add one if this PR has a claim or issue
R4 — Demo video / recording Required in "Screenshots / Demos" before a maintainer reviews

Rules: PLUGIN_PR_RULES.md · re-runs on every push

@github-actions github-actions Bot added the gate:failed Plugin PR gate checks failing label Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

OAuth callback parameters now pass through the tunnel and OAuth processing flow. Token data takes precedence over callback parameters. GitHub tenant-link resolution now falls back to the GitHub installations API when no installation ID is present.

Changes

OAuth provider data handling

Layer / File(s) Summary
Callback parameter propagation
packages/corsair/oauth/index.ts, packages/corsair/tunnel/index.ts
OAuth callback options and tunnel payloads accept provider parameters. OAuth processing merges them with token data while preserving token values on collisions.
GitHub installation fallback
packages/github/webhooks/oauth-tenant-link.ts
The resolver returns a promise and queries /user/installations when token data lacks an installation ID. Invalid or failed responses return null.
OAuth resolution validation
packages/corsair/tests/oauth-callback-params.test.ts, packages/github/webhooks/oauth-tenant-link.test.ts
Tests cover callback propagation, precedence, Slack resolution, GitHub fallback behavior, empty results, missing tokens, and fetch errors.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OAuthCallbackTunnel
  participant processOAuthCallback
  participant mergeOAuthProviderData
  participant resolveGithubOAuthWebhookTenantLink
  participant GitHubAPI
  OAuthCallbackTunnel->>processOAuthCallback: callbackParams
  processOAuthCallback->>mergeOAuthProviderData: tokens and callbackParams
  mergeOAuthProviderData->>resolveGithubOAuthWebhookTenantLink: merged provider data
  resolveGithubOAuthWebhookTenantLink->>GitHubAPI: GET /user/installations when needed
  GitHubAPI-->>resolveGithubOAuthWebhookTenantLink: installation data or error
  resolveGithubOAuthWebhookTenantLink-->>processOAuthCallback: tenant link or null
Loading

Possibly related PRs

Suggested labels: bot:round-1

Suggested reviewers: devjain32

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: resolving GitHub installation_id for OAuth webhook tenant linking.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/webhook-tenant-link

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR propagates OAuth callback query parameters into webhook tenant-link resolution and adds a GitHub API fallback when installation_id is unavailable.

  • Merges callback parameters with token data while preserving token-body precedence.
  • Threads callbackParams through signed OAuth callback tunnel deliveries.
  • Resolves GitHub installation IDs directly or through /user/installations and adds focused tests.

Confidence Score: 4/5

The PR should not merge until GitHub installation selection is unambiguous and callback parameters are propagated through every supported OAuth completion path.

Selecting the first accessible GitHub installation can persist the wrong routing identity, while browser-delivery and management callbacks still discard the query-only installation ID that this change is intended to preserve.

Files Needing Attention: packages/github/webhooks/oauth-tenant-link.ts and packages/corsair/oauth/index.ts

Important Files Changed

Filename Overview
packages/corsair/oauth/index.ts Adds callback-parameter merging before tenant-link resolution, but existing non-tunnel callback entry points do not propagate the new input.
packages/corsair/tunnel/index.ts Extends signed OAuth callback tunnel payloads and forwards callbackParams to the callback processor.
packages/github/webhooks/oauth-tenant-link.ts Adds an API fallback that can select the wrong installation when the token grants access to multiple installations.
packages/corsair/tests/oauth-callback-params.test.ts Covers merge precedence and resolver behavior but not all public callback entry points.
packages/github/webhooks/oauth-tenant-link.test.ts Covers direct and fallback resolution, but only with a single returned installation.

Sequence Diagram

sequenceDiagram
  participant Provider as GitHub
  participant Hub
  participant SDK as Corsair SDK
  participant API as GitHub API
  participant DB
  Provider->>Hub: OAuth callback (code, query params)
  Hub->>SDK: Signed oauth.callback payload
  SDK->>Provider: Exchange code for access token
  alt installation_id present
    SDK->>SDK: Merge callback params with token data
  else installation_id absent
    SDK->>API: GET /user/installations
    API-->>SDK: Accessible installations
    SDK->>SDK: Select first installation
  end
  SDK->>DB: Persist installation_id → tenant link
Loading

Reviews (1): Last reviewed commit: "fix(webhooks): resolve GitHub installati..." | Re-trigger Greptile

const body = (await res.json()) as {
installations?: Array<{ id?: unknown }>;
};
return toExternalId(body.installations?.[0]?.id) ?? null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Fallback selects unrelated installation

If an OAuth token can access multiple GitHub App installations and no installation_id was forwarded, this fallback persists installations[0] without identifying which installation belongs to the connection, causing webhooks from the intended installation to miss the tenant routing link.

Knowledge Base Used: OAuth Helpers, Dev Tunnel, and Webhook Tenant Routing

plugin?: string;
tenantId?: string;
// Provider callback params (e.g. GitHub's installation_id) absent from the token body.
callbackParams?: Record<string, string>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Callback identity remains path-dependent

When GitHub OAuth completes through the existing browser-delivery or management entry point, those callers do not supply the new callbackParams, so a query-only installation_id is lost and the tenant remains dependent on the ambiguous first-installation fallback for webhook routing.

Knowledge Base Used: OAuth Helpers, Dev Tunnel, and Webhook Tenant Routing

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/github/webhooks/oauth-tenant-link.ts`:
- Around line 22-28: Update the fetch call in the OAuth tenant-link flow to use
an AbortController with a bounded timeout, passing its signal to the GitHub API
request. Preserve the existing null fallback on failure and clear the timeout
timer in a finally block.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: df5094ea-7113-4881-8714-410ad2c200bb

📥 Commits

Reviewing files that changed from the base of the PR and between 6f29d95 and c8f88a4.

📒 Files selected for processing (5)
  • packages/corsair/oauth/index.ts
  • packages/corsair/tests/oauth-callback-params.test.ts
  • packages/corsair/tunnel/index.ts
  • packages/github/webhooks/oauth-tenant-link.test.ts
  • packages/github/webhooks/oauth-tenant-link.ts

Comment on lines +22 to +28
const res = await fetch('https://api.github.com/user/installations', {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

file="packages/github/webhooks/oauth-tenant-link.ts"
printf '%s\n' '--- file map ---'
ast-grep outline "$file" --view expanded || true
printf '%s\n' '--- target file ---'
cat -n "$file"
printf '%s\n' '--- related symbols ---'
rg -n -C 5 'resolveGithubOAuthWebhookTenantLink|processOAuthCallback|fetchInstallationId' packages .github

Repository: corsairdev/corsair

Length of output: 23598


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- OAuth callback implementation ---'
sed -n '230,380p' packages/corsair/oauth/index.ts
printf '%s\n' '--- GitHub resolver tests ---'
cat -n packages/github/webhooks/oauth-tenant-link.test.ts
printf '%s\n' '--- timeout and fetch controls ---'
rg -n -C 4 'AbortController|AbortSignal|setTimeout|request timeout|timeout|fetch\(' packages/corsair packages/github | head -n 300

Repository: corsairdev/corsair

Length of output: 29730


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- remaining processOAuthCallback flow ---'
sed -n '360,470p' packages/corsair/oauth/index.ts
printf '%s\n' '--- webhook resolver dispatch ---'
rg -n -C 8 'function resolveOAuthWebhookTenantLink|resolveOAuthWebhookTenantLink\(' packages/corsair
printf '%s\n' '--- request entry points and response timeout configuration ---'
rg -n -C 6 'handle.*Request|processCorsair|setTimeout|AbortSignal|timeoutMs|requestTimeout|REQUEST_TIMEOUT' packages/corsair/tunnel packages/corsair/hub packages/corsair/oauth packages/corsair/core
printf '%s\n' '--- standalone source invariant probe ---'
python3 - <<'PY'
from pathlib import Path

p = Path("packages/github/webhooks/oauth-tenant-link.ts")
s = p.read_text()
fetch_start = s.index("const res = await fetch(")
fetch_end = s.index("\n\t\t});", fetch_start) + len("\n\t\t});")
fetch_block = s[fetch_start:fetch_end]
print("fetch_has_signal:", "signal:" in fetch_block)
print("fetch_is_awaited:", "const res = await fetch(" in fetch_block)
print("resolver_awaits_fetch_helper:", "fromToken ?? (await fetchInstallationId" in s)
print("helper_catches_fetch_failure:", "catch {" in s and "return null;" in s[s.index("catch {"):])
PY

Repository: corsairdev/corsair

Length of output: 36864


Bound the GitHub API request.

Add an abort signal with a bounded timeout. Preserve the null fallback and clear the timer in finally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/github/webhooks/oauth-tenant-link.ts` around lines 22 - 28, Update
the fetch call in the OAuth tenant-link flow to use an AbortController with a
bounded timeout, passing its signal to the GitHub API request. Preserve the
existing null fallback on failure and clear the timeout timer in a finally
block.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Changes in packages/corsair gate:failed Plugin PR gate checks failing plugin Changes inside a plugin package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant