Skip to content
Merged
Show file tree
Hide file tree
Changes from 142 commits
Commits
Show all changes
171 commits
Select commit Hold shift + click to select a range
c2d45c0
feat(github): enable shared web agent installations
pandemicsyn Sep 8, 2026
bb189ad
fix(github): harden shared installation races
pandemicsyn Sep 8, 2026
9e78435
fix(github): serialize shared callback cleanup
pandemicsyn Sep 8, 2026
a3e2582
fix(integrations): fence provider state mutations
pandemicsyn Sep 8, 2026
9cde1c7
fix(integrations): serialize provider state sessions
pandemicsyn Sep 8, 2026
7c7f87c
fix(integrations): restore provider session consistency
pandemicsyn Sep 8, 2026
a7c3c79
Revert "fix(integrations): restore provider session consistency"
pandemicsyn Sep 8, 2026
66e2100
Revert "fix(integrations): serialize provider state sessions"
pandemicsyn Sep 8, 2026
4e43c9b
Revert "fix(integrations): fence provider state mutations"
pandemicsyn Sep 8, 2026
602cc9e
feat(integrations): reserve provider OAuth attempts
pandemicsyn Sep 8, 2026
6d96f55
fix(integrations): lock OAuth reservations by owner row
pandemicsyn Sep 8, 2026
5c2801a
fix(integrations): fence provider OAuth state purpose
pandemicsyn Sep 8, 2026
eec4f26
test(integrations): harden OAuth reservation races
pandemicsyn Sep 8, 2026
da98aa3
test(integrations): prove OAuth owner-row blocking
pandemicsyn Sep 8, 2026
b533c8c
fix(github): serialize verified owner cardinality
pandemicsyn Sep 9, 2026
1d4ad35
test(integrations): include OAuth state issue time
pandemicsyn Sep 9, 2026
d67f75b
chore(db): regenerate shared GitHub migration
pandemicsyn Sep 9, 2026
1d2f970
fix(github): close shared runtime isolation gaps
pandemicsyn Sep 9, 2026
b4b1dfa
fix(github): fence exclusive lifecycle retries
pandemicsyn Sep 9, 2026
50c41b1
fix(github): close legacy authorization edges
pandemicsyn Sep 9, 2026
3a4208f
fix(github): fence direct token legacy paths
pandemicsyn Sep 9, 2026
a518526
fix(github): retry in-progress lifecycle receipts
pandemicsyn Sep 9, 2026
a26013b
refactor(github): simplify lifecycle cleanup
pandemicsyn Sep 10, 2026
fe7651d
fix(github): align repository refresh lock order
pandemicsyn Sep 10, 2026
454bb92
test(github): prove lifecycle lock overlap
pandemicsyn Sep 10, 2026
3fe432d
test(github): match exact advisory lock query
pandemicsyn Sep 10, 2026
884f872
fix(github): bind bot links to tenant association
pandemicsyn Sep 10, 2026
af1734d
fix(github): constrain legacy bot-link fallback
pandemicsyn Sep 10, 2026
57f5e26
feat(slack): fence shared installation activation
pandemicsyn Sep 11, 2026
233a4d0
feat(slack): delegate with exact GitHub connection
pandemicsyn Sep 11, 2026
b9aeab1
fix(slack): make installation activation recoverable
pandemicsyn Sep 11, 2026
27d5944
fix(slack): resolve all-access GitHub repositories
pandemicsyn Sep 11, 2026
300d02a
fix(slack): retire expired installation generations
pandemicsyn Sep 11, 2026
296df67
test(slack): reject unhealthy all-access GitHub links
pandemicsyn Sep 11, 2026
1d9b194
fix(slack): preserve cleanup reservations until revoke
pandemicsyn Sep 11, 2026
0636b8f
fix(slack): clean verified legacy token metadata
pandemicsyn Sep 11, 2026
4357a03
fix(slack): retain cleanup identity barriers
pandemicsyn Sep 11, 2026
8db09b0
fix(slack): bridge Enterprise workspace identities
pandemicsyn Sep 11, 2026
439013f
fix(slack): order Enterprise identity aliases
pandemicsyn Sep 11, 2026
41538c3
fix(slack): fence alias and cleanup ordering
pandemicsyn Sep 11, 2026
89ac167
fix(slack): persist cleanup and alias progress
pandemicsyn Sep 11, 2026
4eab6e0
fix(slack): defer shared Enterprise Grid support
pandemicsyn Sep 11, 2026
31ac813
fix(slack): isolate shared workspace activation
pandemicsyn Sep 11, 2026
3bf72f0
fix(slack): fence workspace sharing conversion
pandemicsyn Sep 11, 2026
b567d09
fix(slack): validate workspace conversion state
pandemicsyn Sep 11, 2026
185270d
fix(slack): route shared and cleanup states exactly
pandemicsyn Sep 12, 2026
f386ae0
fix(slack): preserve unshared runtime boundaries
pandemicsyn Sep 12, 2026
fd6c560
fix(slack): enforce runtime health boundaries
pandemicsyn Sep 12, 2026
d8eecc6
fix(slack): reserve successful unshared workspaces
pandemicsyn Sep 12, 2026
31e4964
fix(slack): finalize SDK effects with generation CAS
pandemicsyn Sep 12, 2026
8ac1656
fix(slack): separate workspace and Enterprise cleanup
pandemicsyn Sep 12, 2026
bfe544e
fix(slack): terminalize legacy cleanup safely
pandemicsyn Sep 12, 2026
6d67a1e
fix(slack): release terminal cleanup tombstones
pandemicsyn Sep 12, 2026
9bfcd8d
fix(slack): isolate webhook maintenance failures
pandemicsyn Sep 12, 2026
bf4f256
fix(slack): exclude Enterprise installs from backfill
pandemicsyn Sep 12, 2026
06f0792
fix(slack): validate Enterprise IDs and reset health
pandemicsyn Sep 12, 2026
0440374
fix(slack): make Enterprise activation success-only
pandemicsyn Sep 12, 2026
0d98e23
fix(slack): reconstruct Enterprise runtime state
pandemicsyn Sep 12, 2026
365a247
Revert "fix(slack): reconstruct Enterprise runtime state"
pandemicsyn Sep 12, 2026
a9f3db9
Revert "fix(slack): make Enterprise activation success-only"
pandemicsyn Sep 12, 2026
53c6893
Revert "fix(slack): validate Enterprise IDs and reset health"
pandemicsyn Sep 12, 2026
7677da4
Revert "fix(slack): exclude Enterprise installs from backfill"
pandemicsyn Sep 12, 2026
12ff9d3
Revert "fix(slack): isolate webhook maintenance failures"
pandemicsyn Sep 12, 2026
f2213c8
Revert "fix(slack): release terminal cleanup tombstones"
pandemicsyn Sep 12, 2026
0bf885a
Revert "fix(slack): terminalize legacy cleanup safely"
pandemicsyn Sep 12, 2026
a4765e4
Revert "fix(slack): separate workspace and Enterprise cleanup"
pandemicsyn Sep 12, 2026
316a94b
Revert "fix(slack): finalize SDK effects with generation CAS"
pandemicsyn Sep 12, 2026
86e22b0
Revert "fix(slack): reserve successful unshared workspaces"
pandemicsyn Sep 12, 2026
96b12da
Revert "fix(slack): enforce runtime health boundaries"
pandemicsyn Sep 12, 2026
030bf07
Revert "fix(slack): preserve unshared runtime boundaries"
pandemicsyn Sep 12, 2026
16bf7d0
Revert "fix(slack): route shared and cleanup states exactly"
pandemicsyn Sep 12, 2026
e2b28a7
Revert "fix(slack): validate workspace conversion state"
pandemicsyn Sep 12, 2026
68449c5
Revert "fix(slack): fence workspace sharing conversion"
pandemicsyn Sep 12, 2026
c351966
Revert "fix(slack): isolate shared workspace activation"
pandemicsyn Sep 12, 2026
72d2f0f
Revert "fix(slack): defer shared Enterprise Grid support"
pandemicsyn Sep 12, 2026
8eca867
Revert "fix(slack): persist cleanup and alias progress"
pandemicsyn Sep 12, 2026
efd7202
Revert "fix(slack): fence alias and cleanup ordering"
pandemicsyn Sep 12, 2026
b6048b6
Revert "fix(slack): order Enterprise identity aliases"
pandemicsyn Sep 12, 2026
dc43dcc
Revert "fix(slack): bridge Enterprise workspace identities"
pandemicsyn Sep 12, 2026
e0b32fa
Revert "fix(slack): retain cleanup identity barriers"
pandemicsyn Sep 12, 2026
5993f0c
Revert "fix(slack): clean verified legacy token metadata"
pandemicsyn Sep 12, 2026
73856a5
Revert "fix(slack): preserve cleanup reservations until revoke"
pandemicsyn Sep 12, 2026
fb1b7ce
Revert "test(slack): reject unhealthy all-access GitHub links"
pandemicsyn Sep 12, 2026
4fd2f6d
Revert "fix(slack): retire expired installation generations"
pandemicsyn Sep 12, 2026
8c22ca7
Revert "fix(slack): resolve all-access GitHub repositories"
pandemicsyn Sep 12, 2026
d2bcb9e
Revert "fix(slack): make installation activation recoverable"
pandemicsyn Sep 12, 2026
6d824dc
Revert "feat(slack): fence shared installation activation"
pandemicsyn Sep 12, 2026
d617c5c
test(slack): verify minimal shared delegation
pandemicsyn Sep 12, 2026
3ea54c5
test(slack): preserve unsupported provider fences
pandemicsyn Sep 12, 2026
527ce8a
test(slack): tighten minimal sharing fixtures
pandemicsyn Sep 12, 2026
7a78158
fix(slack): validate cached GitHub repositories
pandemicsyn Sep 13, 2026
d4d2e74
fix(github): stop a locally disconnected connection from blocking new…
pandemicsyn Sep 12, 2026
45c3347
fix(organizations): keep onboarding Source Control in sync with GitHub
pandemicsyn Sep 12, 2026
7a24fc0
fix(github): record authorization provenance on every connect path
pandemicsyn Sep 12, 2026
0dc8214
fix(github): also exclude disconnected rows from the legacy cardinali…
pandemicsyn Sep 12, 2026
5892b8f
fix(github): terminalize active review work on disconnect and uninstall
pandemicsyn Sep 12, 2026
cfb515c
test(github): add remaining P2 coverage from PR4 review
pandemicsyn Sep 12, 2026
fee30e4
fix(github): settle the review ledger only after disconnect/uninstall…
pandemicsyn Sep 12, 2026
bd0c8fd
feat(github): converge Connect GitHub and Connect existing into one flow
pandemicsyn Sep 12, 2026
64595f6
fix(github): distinguish a stale connection attempt from a transient …
pandemicsyn Sep 12, 2026
bedc0d9
fix(github): actually send code_verifier through the OAuth token exch…
pandemicsyn Sep 13, 2026
e08d1f7
fix(github): do not require sharing admission for a disconnected prio…
pandemicsyn Sep 13, 2026
95bf47b
refactor(github): reduce duplication in provenance and settle-contrac…
pandemicsyn Sep 13, 2026
8c47e1d
fix(github): picker action races, error-state polish, and effect churn
pandemicsyn Sep 13, 2026
9b11a35
fix(github): reject a non-JSON OAuth token response with a clear error
pandemicsyn Sep 13, 2026
3f4eb4b
docs(github): correct inaccurate rationale in disconnect review-cance…
pandemicsyn Sep 13, 2026
aa3c843
refactor(github): make the picker's error state a discriminated union
pandemicsyn Sep 13, 2026
62a1005
refactor(github): move latest-ref update into an effect, not the rend…
pandemicsyn Sep 13, 2026
ec51eee
fix(db): reconcile orphans before validating github FK
pandemicsyn Sep 14, 2026
1158178
fix(github): only record webhook receipt on 2xx dispatch
pandemicsyn Sep 14, 2026
731e775
fix(integrations): supersede unexpired pending OAuth attempts
pandemicsyn Sep 14, 2026
242eb98
fix(cloud-agent): dedupe exact-duplicate repository entries
pandemicsyn Sep 14, 2026
829cb62
fix(github): stop locking an incumbent personal owner out of their ow…
pandemicsyn Sep 14, 2026
296112d
Merge branch 'main' into feat/github-connections-pr2
pandemicsyn Sep 14, 2026
0e1c8b1
Merge branch 'feat/github-connections-pr2' into feat/github-connectio…
pandemicsyn Sep 14, 2026
704539d
Merge branch 'feat/github-connections-pr3' into feat/github-connectio…
pandemicsyn Sep 14, 2026
50b7505
fix(slack): accept adapter-written repository cache
pandemicsyn Sep 14, 2026
7d94647
Merge branch 'feat/github-connections-pr3' into feat/github-connectio…
pandemicsyn Sep 14, 2026
09238be
Merge branch 'main' into feat/github-connections-pr2
pandemicsyn Sep 14, 2026
015fd21
Merge branch 'feat/github-connections-pr2' into feat/github-connectio…
pandemicsyn Sep 14, 2026
023d206
Merge branch 'feat/github-connections-pr3' into feat/github-connectio…
pandemicsyn Sep 14, 2026
ca18bfa
fix(github): stop an unbound legacy association from uninstalling a s…
pandemicsyn Sep 14, 2026
805e161
fix(github): harden shared delivery and repository validation
pandemicsyn Sep 14, 2026
12b0e56
fix(cloud-agent): repair personal GitHub session start and stale refresh
pandemicsyn Sep 14, 2026
866465c
Merge branch 'main' into feat/github-connections-pr2
pandemicsyn Sep 14, 2026
85e48a8
fix(github): reclaim stale installation delivery claims
pandemicsyn Sep 14, 2026
4a7c23c
Merge remote-tracking branch 'origin/feat/github-connections-pr2' int…
pandemicsyn Sep 14, 2026
5e72145
fix(github): enforce webhook delivery-claim window
pandemicsyn Sep 14, 2026
1415194
Merge branch 'feat/github-connections-pr3' into feat/github-connectio…
pandemicsyn Sep 14, 2026
9710652
Merge branch 'main' into feat/github-connections-pr2
pandemicsyn Sep 14, 2026
5776bde
Merge branch 'feat/github-connections-pr2' into feat/github-connectio…
pandemicsyn Sep 14, 2026
6822faa
Merge branch 'feat/github-connections-pr3' into feat/github-connectio…
pandemicsyn Sep 14, 2026
6b2e740
fix(db): make github FK migration crash-safe and guard orphan reconci…
pandemicsyn Sep 14, 2026
3dcd749
fix(db): make concurrent index builds in 0245 retry-safe
pandemicsyn Sep 14, 2026
79025d2
Merge PR2 into PR3 and renumber receipt migration to 0247
pandemicsyn Sep 14, 2026
4418b33
Merge branch 'feat/github-connections-pr3' into feat/github-connectio…
pandemicsyn Sep 14, 2026
1465240
Merge branch 'main' into feat/github-connections-pr2
pandemicsyn Sep 14, 2026
a332616
Merge branch 'feat/github-connections-pr2' into feat/github-connectio…
pandemicsyn Sep 14, 2026
5493ba0
Merge branch 'feat/github-connections-pr3' into feat/github-connectio…
pandemicsyn Sep 14, 2026
169563f
Merge branch 'main' into feat/github-connections-pr2
pandemicsyn Sep 15, 2026
616f690
Merge branch 'feat/github-connections-pr2' into feat/github-connectio…
pandemicsyn Sep 15, 2026
b78f528
Merge branch 'feat/github-connections-pr3' into feat/github-connectio…
pandemicsyn Sep 15, 2026
174ad4f
fix(github): serialize pending-install completion with sharing admission
pandemicsyn Sep 15, 2026
de0dad3
fix(github): apply the pending-completion transaction to the right query
pandemicsyn Sep 15, 2026
301d4b4
fix(github): never route a webhook through a disconnected tenant asso…
pandemicsyn Sep 15, 2026
15ae070
fix(github): let an exact shared association read its repository inve…
pandemicsyn Sep 15, 2026
baffc31
chore(github): apply formatter output to the security-fix files
pandemicsyn Sep 15, 2026
f53e068
fix(github): treat an empty exact-association id as the exclusive-onl…
pandemicsyn Sep 15, 2026
81628ee
Merge branch 'main' into feat/github-connections-pr2
pandemicsyn Sep 15, 2026
ea8d5ed
fix(github): restore exact-id shared runtime authorization lost in th…
pandemicsyn Sep 15, 2026
9e7a7ce
Merge branch 'feat/github-connections-pr2' into feat/github-connectio…
pandemicsyn Sep 15, 2026
0d5df58
Merge branch 'feat/github-connections-pr3' into feat/github-connectio…
pandemicsyn Sep 15, 2026
bf403eb
fix(github): let installation.unsuspend clear a suspended association
pandemicsyn Sep 15, 2026
1d850d2
fix(github): keep the all-repositories inventory sync best-effort
pandemicsyn Sep 15, 2026
a2fb46e
Merge branch 'main' into feat/github-connections-pr2
pandemicsyn Sep 15, 2026
3ae05d4
Merge branch 'feat/github-connections-pr2' into feat/github-connectio…
pandemicsyn Sep 15, 2026
4ac21f1
Merge branch 'feat/github-connections-pr3' into feat/github-connectio…
pandemicsyn Sep 15, 2026
0d9c303
fix(github): serialize the legacy install bind against pending comple…
pandemicsyn Sep 15, 2026
6b96bb9
fix(github): apply lifecycle events to the connected tenant, not an a…
pandemicsyn Sep 15, 2026
40f70c2
chore(github): apply formatter output to the lifecycle regression test
pandemicsyn Sep 15, 2026
35146ba
fix(github): resolve the connected association for installation.unsus…
pandemicsyn Sep 15, 2026
7ca4831
fix(github): gate the legacy bind guard and compare owners NULL-safely
pandemicsyn Sep 15, 2026
e8f59cf
fix(github): make the legacy install write, guard, and bind one trans…
pandemicsyn Sep 15, 2026
9051173
fix(github): stop the suspend route from dropping a lifecycle delivery
pandemicsyn Sep 15, 2026
f352b0c
Merge branch 'main' into feat/github-connections-pr2
pandemicsyn Sep 15, 2026
0f65542
Merge branch 'feat/github-connections-pr2' into feat/github-connectio…
pandemicsyn Sep 15, 2026
c569bda
Merge branch 'feat/github-connections-pr3' into feat/github-connectio…
pandemicsyn Sep 15, 2026
d6b6b79
Merge branch 'main' into feat/github-connections-pr2
pandemicsyn Sep 16, 2026
6a6e30a
Merge PR2 into PR3: adopt renumbered migrations 0245-0249
pandemicsyn Sep 16, 2026
1586448
Merge branch 'feat/github-connections-pr3' into feat/github-connectio…
pandemicsyn Sep 16, 2026
65790fd
Merge remote-tracking branch 'origin/main' into feat/github-connectio…
pandemicsyn Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ Manage shared web env var additions and rotations with `pnpm web:env set <VARIAB
- `GITHUB_LITE_APP_PRIVATE_KEY` - Private key for the lite GitHub App. `[SECRET]`
- `GITHUB_LITE_APP_CLIENT_ID` - OAuth Client ID for the lite GitHub App install/login flow. [PUBLIC]
- `GITHUB_MULTIPLE_INSTALLATION_ORGANIZATION_IDS` - Comma-separated Kilo organization UUIDs allowed to connect multiple GitHub App installations. Unset or empty disables multiple installations for all organizations. [SERVER]
- `GITHUB_SHARED_INSTALLATION_ORGANIZATION_IDS` - Comma-separated destination Kilo organization UUIDs allowed to create an association to a GitHub App installation already associated elsewhere. Unset or empty disables new shared associations without revoking existing ones. [SERVER]
- `GITHUB_CONNECTION_MANAGEMENT_ENABLED` - Set to exact `true` to admit new existing-installation connection management and local disconnect. Unset or any other value keeps new management admission disabled without changing incumbent GitHub integration workflows. [SERVER]
- Keep disabled for at least one OAuth state TTL (10 minutes) after deploying reservation-aware callbacks so purpose-less states issued by the previous version can complete.
- During the migration-to-app promotion window, old pending-install callbacks may fail against the replaced pending indexes. Keep the window brief, monitor deploy health, and retry the GitHub connection after promotion completes.
- `PER_REPO_SETTINGS` - Set to exactly `true` to reveal the Repository Customizations UI (per-installation default AI model / PR review mode, plus per-repository overrides) on the GitHub integration settings pages, for both personal accounts and organizations. Defaults to disabled so the feature can ship dark. [SERVER]
- `GITHUB_ADMIN_STATS_TOKEN` - Token for admin GitHub API stats lookups; used in `apps/web/src/scripts/backfill-pr-author-github-ids.ts`. `[SECRET]`
- `GITHUB_CLI_PAT` - GitHub personal access token for `gh` CLI operations inside contractors; used in `services/gastown/container/src/process-manager.ts`. `[SECRET]`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ describe('GET /api/integrations/bitbucket/connect', () => {
owner: `org_${ORGANIZATION_ID}`,
userId: USER_ID,
returnTo: `/organizations/${ORGANIZATION_ID}/integrations/bitbucket`,
issuedAt: expect.any(Number),
});
expect(mockedEnsureOrganizationAccess).toHaveBeenCalledWith(
{ user: expect.objectContaining({ id: USER_ID }) },
Expand Down
177 changes: 149 additions & 28 deletions apps/web/src/app/api/integrations/github/callback/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { failureResult } from '@/lib/maybe-result';
import { consumeInstallState } from '@/lib/integrations/github/install-state';
import {
bindGitHubIntegrationToCanonicalInstallation,
connectVerifiedGitHubInstallation,
observeGitHubInstallationLifecycle,
} from '@/lib/integrations/db/github-installations';
import type * as InstallStateModule from '@/lib/integrations/github/install-state';
Expand All @@ -24,6 +25,7 @@ import { eq } from 'drizzle-orm';
import { randomUUID } from 'node:crypto';
import type { Owner } from '@/lib/integrations/core/types';
import {
findGitHubBotLinkIntegrations,
findIntegrationByInstallationId,
findIntegrationByInstallationIdForOwner,
upsertPlatformIntegrationForOwner,
Expand Down Expand Up @@ -85,6 +87,7 @@ jest.mock('@/routers/organizations/utils', () => ({
}));
jest.mock('@/lib/integrations/db/platform-integrations', () => ({
createPendingIntegration: jest.fn(),
findGitHubBotLinkIntegrations: jest.fn(),
findIntegrationByInstallationId: jest.fn(),
findIntegrationByInstallationIdForOwner: jest.fn(),
findPendingInstallationByRequesterId: jest.fn(),
Expand Down Expand Up @@ -113,6 +116,7 @@ const mockedExchangeGitHubOAuthCode = jest.mocked(exchangeGitHubOAuthCode);
const mockedLinkKiloUser = jest.mocked(linkKiloUser);
const mockedBot = jest.mocked(bot);
const mockedFindIntegrationByInstallationId = jest.mocked(findIntegrationByInstallationId);
const mockedFindGitHubBotLinkIntegrations = jest.mocked(findGitHubBotLinkIntegrations);
const mockedFindIntegrationByInstallationIdForOwner = jest.mocked(
findIntegrationByInstallationIdForOwner
);
Expand All @@ -129,6 +133,7 @@ const mockedCaptureMessage = jest.mocked(captureMessage);
const mockedEnsureOrganizationAccess = jest.mocked(ensureOrganizationAccess);
const mockedAssertUserAdministersInstallation = jest.mocked(assertUserAdministersInstallation);
const mockedObserveGitHubInstallationLifecycle = jest.mocked(observeGitHubInstallationLifecycle);
const mockedConnectVerifiedGitHubInstallation = jest.mocked(connectVerifiedGitHubInstallation);
const mockedBindGitHubIntegrationToCanonicalInstallation = jest.mocked(
bindGitHubIntegrationToCanonicalInstallation
);
Expand Down Expand Up @@ -160,6 +165,18 @@ beforeEach(() => {
github_app_type: 'standard',
}) as never
);
mockedFindGitHubBotLinkIntegrations.mockImplementation(
async () =>
[
{
id: '00000000-0000-4000-8000-000000000099',
owned_by_user_id: writtenOwner.type === 'user' ? writtenOwner.id : null,
owned_by_organization_id: writtenOwner.type === 'org' ? writtenOwner.id : null,
platform_installation_id: INSTALLATION_ID,
github_app_type: 'standard',
},
] as never
);
mockedFindIntegrationByInstallationIdForOwner.mockImplementation(
async () =>
({
Expand Down Expand Up @@ -197,6 +214,8 @@ function expectRedirectLocation(response: Response, expectedPathWithQuery: strin
describe('GET /api/integrations/github/callback bot link flow', () => {
beforeEach(() => {
jest.clearAllMocks();
process.env.GITHUB_CONNECTION_MANAGEMENT_ENABLED = 'false';
process.env.GITHUB_SHARED_INSTALLATION_ORGANIZATION_IDS = '';

mockedGetUserFromAuth.mockResolvedValue({
user: { id: USER_ID },
Expand All @@ -206,6 +225,7 @@ describe('GET /api/integrations/github/callback bot link flow', () => {
userId: USER_ID,
installationId: INSTALLATION_ID,
callbackPath: '/github/link',
platformIntegrationId: '00000000-0000-4000-8000-000000000099',
});
mockedExchangeGitHubOAuthCode.mockResolvedValue({
id: GITHUB_USER_ID,
Expand All @@ -218,6 +238,15 @@ describe('GET /api/integrations/github/callback bot link flow', () => {
github_app_type: 'standard',
metadata: null,
} as never);
mockedFindGitHubBotLinkIntegrations.mockResolvedValue([
{
id: '00000000-0000-4000-8000-000000000099',
owned_by_organization_id: 'org_1',
owned_by_user_id: null,
platform_installation_id: INSTALLATION_ID,
github_app_type: 'standard',
} as never,
]);
mockedIsOrganizationMember.mockResolvedValue(true);
});

Expand Down Expand Up @@ -281,11 +310,11 @@ describe('GET /api/integrations/github/callback bot link flow', () => {
await expect(response.text()).resolves.toContain(
'not a member of the organization that owns this GitHub integration'
);
expect(mockedFindIntegrationByInstallationId).toHaveBeenCalledWith(
'github',
INSTALLATION_ID,
'standard'
);
expect(mockedFindGitHubBotLinkIntegrations).toHaveBeenCalledWith({
installationId: INSTALLATION_ID,
appType: 'standard',
platformIntegrationId: '00000000-0000-4000-8000-000000000099',
});
expect(mockedExchangeGitHubOAuthCode).not.toHaveBeenCalled();
expect(mockedLinkKiloUser).not.toHaveBeenCalled();
});
Expand All @@ -299,11 +328,11 @@ describe('GET /api/integrations/github/callback bot link flow', () => {
expect(response.status).toBe(200);
await expect(response.text()).resolves.toContain('GitHub account octocat has been linked');
expect(mockedExchangeGitHubOAuthCode).toHaveBeenCalledWith('abc', 'standard');
expect(mockedFindIntegrationByInstallationId).toHaveBeenCalledWith(
'github',
INSTALLATION_ID,
'standard'
);
expect(mockedFindGitHubBotLinkIntegrations).toHaveBeenCalledWith({
installationId: INSTALLATION_ID,
appType: 'standard',
platformIntegrationId: '00000000-0000-4000-8000-000000000099',
});
expect(mockedIsOrganizationMember).toHaveBeenCalledWith('org_1', USER_ID);
expect(mockedBot.initialize).toHaveBeenCalled();
expect(mockedLinkKiloUser).toHaveBeenCalledWith(
Expand All @@ -318,27 +347,80 @@ describe('GET /api/integrations/github/callback bot link flow', () => {
);
});

test('allows one healthy legacy no-ID state while management and sharing are disabled', async () => {
mockedVerifyGitHubBotLinkState.mockReturnValue({
userId: USER_ID,
installationId: INSTALLATION_ID,
callbackPath: '/github/link',
});
const { GET } = await import('./route');
const response = await GET(
makeRequest('/api/integrations/github/callback?code=abc&state=signed') as never
);
expect(response.status).toBe(200);
expect(mockedFindGitHubBotLinkIntegrations).toHaveBeenCalledWith({
installationId: INSTALLATION_ID,
appType: 'standard',
platformIntegrationId: undefined,
});
});

test('rejects a no-ID state after connection management is enabled', async () => {
process.env.GITHUB_CONNECTION_MANAGEMENT_ENABLED = 'true';
mockedVerifyGitHubBotLinkState.mockReturnValue({
userId: USER_ID,
installationId: INSTALLATION_ID,
callbackPath: '/github/link',
});
const { GET } = await import('./route');
const response = await GET(
makeRequest('/api/integrations/github/callback?code=abc&state=signed') as never
);
expect(response.status).toBe(404);
expect(mockedExchangeGitHubOAuthCode).not.toHaveBeenCalled();
});

test('rejects a no-ID state after shared-installation admission is enabled', async () => {
process.env.GITHUB_SHARED_INSTALLATION_ORGANIZATION_IDS =
'00000000-0000-4000-8000-000000000001';
mockedVerifyGitHubBotLinkState.mockReturnValue({
userId: USER_ID,
installationId: INSTALLATION_ID,
callbackPath: '/github/link',
});
const { GET } = await import('./route');
const response = await GET(
makeRequest('/api/integrations/github/callback?code=abc&state=signed') as never
);
expect(response.status).toBe(404);
expect(mockedExchangeGitHubOAuthCode).not.toHaveBeenCalled();
});

test('routes a Lite bot-link callback through the Lite app identity', async () => {
mockedVerifyGitHubBotLinkState.mockReturnValue({
userId: USER_ID,
installationId: INSTALLATION_ID,
callbackPath: '/github/link',
githubAppType: 'lite',
platformIntegrationId: '00000000-0000-4000-8000-000000000099',
});
mockedFindIntegrationByInstallationId.mockResolvedValue({
owned_by_organization_id: 'org_1',
github_app_type: 'lite',
} as never);
mockedFindGitHubBotLinkIntegrations.mockResolvedValue([
{
id: '00000000-0000-4000-8000-000000000099',
owned_by_organization_id: 'org_1',
github_app_type: 'lite',
} as never,
]);
const { GET } = await import('./route');
const response = await GET(
makeRequest('/api/integrations/github/callback?code=abc&state=signed') as never
);
expect(response.status).toBe(200);
expect(mockedFindIntegrationByInstallationId).toHaveBeenCalledWith(
'github',
INSTALLATION_ID,
'lite'
);
expect(mockedFindGitHubBotLinkIntegrations).toHaveBeenCalledWith({
installationId: INSTALLATION_ID,
appType: 'lite',
platformIntegrationId: '00000000-0000-4000-8000-000000000099',
});
expect(mockedExchangeGitHubOAuthCode).toHaveBeenCalledWith('abc', 'lite');
expect(mockedLinkKiloUser).toHaveBeenCalledWith(
mockState,
Expand All @@ -347,18 +429,16 @@ describe('GET /api/integrations/github/callback bot link flow', () => {
);
});

test("exchanges the OAuth code against the integration's github_app_type", async () => {
mockedFindIntegrationByInstallationId.mockResolvedValue({
owned_by_organization_id: 'org_1',
owned_by_user_id: null,
github_app_type: 'lite',
metadata: null,
} as never);
test('rejects an integration whose app type does not match signed state', async () => {
mockedFindGitHubBotLinkIntegrations.mockResolvedValue([]);

const { GET } = await import('./route');
await GET(makeRequest('/api/integrations/github/callback?code=abc&state=signed') as never);
const response = await GET(
makeRequest('/api/integrations/github/callback?code=abc&state=signed') as never
);

expect(mockedExchangeGitHubOAuthCode).toHaveBeenCalledWith('abc', 'lite');
expect(response.status).toBe(404);
expect(mockedExchangeGitHubOAuthCode).not.toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -1139,6 +1219,10 @@ describe('GET /api/integrations/github/callback plaintext state rejection', () =
describe('GET /api/integrations/github/callback admin proof', () => {
beforeEach(() => {
jest.clearAllMocks();
// Most tests in this describe exercise the legacy access-list admin
// check; the connection-management-enabled test below opts back in
// explicitly so it does not leak into the tests that follow it.
process.env.GITHUB_CONNECTION_MANAGEMENT_ENABLED = 'false';

mockedGetUserFromAuth.mockResolvedValue({
user: {
Expand Down Expand Up @@ -1229,6 +1313,43 @@ describe('GET /api/integrations/github/callback admin proof', () => {
installationId: INSTALLATION_ID,
});
expect(mockedUpsertPlatformIntegrationForOwner).toHaveBeenCalled();
// Even on the legacy access-list admin check path, the OAuth code
// exchange resolved a real GitHub identity — it must still be recorded
// as authorization provenance rather than left null.
expect(mockedUpsertPlatformIntegrationForOwner).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
kiloUserId: USER_ID,
githubUserId: GITHUB_USER_ID,
})
);
});

test('routes a management-enabled install through the verified writer with both identities', async () => {
process.env.GITHUB_CONNECTION_MANAGEMENT_ENABLED = 'true';
mockedConnectVerifiedGitHubInstallation.mockResolvedValue({
ok: true,
integrationId: '00000000-0000-4000-8000-0000000000aa',
});

const { GET } = await import('./route');
const response = await GET(
makeRequest(
`/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${INSTALL_STATE_TOKEN}&code=abc`
) as never
);

expect(response.status).toBe(307);
expect(mockedVerifyGitHubInstallationAuthorization).toHaveBeenCalled();
expect(mockedAssertUserAdministersInstallation).not.toHaveBeenCalled();
expect(mockedUpsertPlatformIntegrationForOwner).not.toHaveBeenCalled();
expect(mockedConnectVerifiedGitHubInstallation).toHaveBeenCalledWith(
{ type: 'user', id: USER_ID },
expect.objectContaining({
kiloUserId: USER_ID,
githubUserId: GITHUB_USER_ID,
})
);
});

test('rejects an install when admin check returns false', async () => {
Expand Down
Loading