diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md index c10bd79434..233a9e15c0 100644 --- a/ENVIRONMENT.md +++ b/ENVIRONMENT.md @@ -153,6 +153,13 @@ Manage shared web env var additions and rotations with `pnpm web:env set vi.fn()); +const platformMock = vi.hoisted(() => ({ OS: 'ios' })); +const mintInstallStateMock = vi.hoisted(() => vi.fn()); +const openAuthSessionMock = vi.hoisted(() => vi.fn()); +const openBrowserMock = vi.hoisted(() => vi.fn()); + +vi.mock('react-native', () => ({ + Alert: { alert: alertMock }, + Platform: platformMock, +})); + +vi.mock('expo-web-browser', () => ({ + openAuthSessionAsync: openAuthSessionMock, + openBrowserAsync: openBrowserMock, +})); + +vi.mock('@/lib/config', () => ({ + WEB_BASE_URL: 'https://web.test', +})); + +vi.mock('@/lib/trpc', () => ({ + trpcClient: { + githubApps: { + mintInstallState: { mutate: mintInstallStateMock }, + }, + }, +})); + +vi.mock('@/components/agents/session-list-screen', () => ({ + AgentSessionListScreen: () => null, +})); + +type AlertButton = { text: string; onPress?: () => void }; + +const noOp = () => undefined; + +function mountRoute() { + const ref: { current: TestRenderer.ReactTestRenderer | undefined } = { current: undefined }; + act(() => { + ref.current = TestRenderer.create(createElement(AgentSessionList)); + }); + if (!ref.current) { + throw new Error('route did not render'); + } + return ref.current; +} + +async function flushMicrotasks() { + await act(async () => { + await new Promise(resolve => { + setTimeout(resolve, 0); + }); + }); +} + +function lastAlertButtons(): AlertButton[] | undefined { + return alertMock.mock.calls.at(-1)?.[2] as AlertButton[] | undefined; +} + +describe('buildGitHubInstallOutcomeAlert (C13 outcome states)', () => { + it('returns null for the empty state (no return outcome)', () => { + expect(buildGitHubInstallOutcomeAlert(null, () => undefined)).toBeNull(); + }); + + it('happy state: connected message and Continue, no recovery action', () => { + expect(buildGitHubInstallOutcomeAlert({ kind: 'success' }, () => undefined)).toEqual({ + title: 'GitHub App installed', + message: 'Your repositories are now connected.', + buttons: [{ text: 'Continue' }], + }); + }); + + it('pending state: admin approval message and Done', () => { + expect(buildGitHubInstallOutcomeAlert({ kind: 'pending' }, () => undefined)).toEqual({ + title: 'Awaiting admin approval', + message: 'An organization admin must approve the installation request.', + buttons: [{ text: 'Done' }], + }); + }); + + it('retryable state: Try again wires the recovery callback', () => { + const alert = buildGitHubInstallOutcomeAlert( + { kind: 'error', code: 'installation_failed' }, + noOp + ); + expect(alert?.title).toBe('Installation did not complete'); + expect(alert?.buttons).toHaveLength(1); + expect(alert?.buttons[0]?.text).toBe('Try again'); + expect(alert?.buttons[0]?.onPress).toBe(noOp); + }); + + it('non-retryable states: show the reason and Back, never retry', () => { + for (const code of [ + 'install_state_user_mismatch', + 'not_installation_admin', + 'installation_already_claimed', + ] as const) { + const alert = buildGitHubInstallOutcomeAlert({ kind: 'error', code }, () => undefined); + expect(alert?.buttons).toEqual([{ text: 'Back' }]); + expect(alert?.buttons[0]?.onPress).toBeUndefined(); + } + expect( + buildGitHubInstallOutcomeAlert( + { kind: 'error', code: 'install_state_user_mismatch' }, + () => undefined + )?.title + ).toBe('Account mismatch'); + expect( + buildGitHubInstallOutcomeAlert( + { kind: 'error', code: 'not_installation_admin' }, + () => undefined + )?.title + ).toBe('Cannot complete installation'); + }); +}); + +describe('Agents tab return-outcome rendering', () => { + beforeEach(() => { + alertMock.mockReset(); + mintInstallStateMock.mockReset(); + mintInstallStateMock.mockResolvedValue({ token: 'fresh-token' }); + openAuthSessionMock.mockReset(); + openAuthSessionMock.mockResolvedValue(undefined); + openBrowserMock.mockReset(); + openBrowserMock.mockResolvedValue(undefined); + platformMock.OS = 'ios'; + setGitHubInstallReturnOutcome(null); + }); + + it('empty state: renders the agent list without an outcome alert', () => { + const renderer = mountRoute(); + expect(alertMock).not.toHaveBeenCalled(); + act(() => { + renderer.unmount(); + }); + }); + + it('happy state: shows the connected alert on mount and consumes the outcome', () => { + setGitHubInstallReturnOutcome({ kind: 'success' }); + const renderer = mountRoute(); + + expect(alertMock).toHaveBeenCalledTimes(1); + expect(alertMock).toHaveBeenCalledWith( + 'GitHub App installed', + 'Your repositories are now connected.', + [{ text: 'Continue' }] + ); + expect(getGitHubInstallReturnOutcome()).toBeNull(); + act(() => { + renderer.unmount(); + }); + }); + + it('retryable state: pressing Try again mints a fresh token and reopens the flow', async () => { + setGitHubInstallReturnOutcome({ kind: 'error', code: 'installation_failed' }); + const renderer = mountRoute(); + + const tryAgain = lastAlertButtons()?.find(button => button.text === 'Try again'); + expect(tryAgain?.onPress).toBeDefined(); + + act(() => { + tryAgain?.onPress?.(); + }); + await flushMicrotasks(); + + expect(mintInstallStateMock).toHaveBeenCalledWith({ returnTo: '/cloud/sessions' }); + expect(openAuthSessionMock).toHaveBeenCalledWith( + 'https://web.test/github-app?installState=fresh-token&fromApp=1' + ); + expect(openBrowserMock).not.toHaveBeenCalled(); + act(() => { + renderer.unmount(); + }); + }); + + it('retryable state: org-scoped outcome retries with the original organizationId', async () => { + setGitHubInstallReturnOutcome({ + kind: 'error', + code: 'installation_failed', + organizationId: 'org-123', + }); + const renderer = mountRoute(); + + const tryAgain = lastAlertButtons()?.find(button => button.text === 'Try again'); + expect(tryAgain?.onPress).toBeDefined(); + + act(() => { + tryAgain?.onPress?.(); + }); + await flushMicrotasks(); + + expect(mintInstallStateMock).toHaveBeenCalledWith({ + organizationId: 'org-123', + returnTo: '/cloud/sessions', + }); + expect(openAuthSessionMock).toHaveBeenCalledWith( + 'https://web.test/github-app?organizationId=org-123&installState=fresh-token&fromApp=1' + ); + act(() => { + renderer.unmount(); + }); + }); + + it('retryable state: mint failure keeps a working Try again in the failure alert', async () => { + setGitHubInstallReturnOutcome({ kind: 'error', code: 'installation_failed' }); + const renderer = mountRoute(); + + mintInstallStateMock.mockRejectedValueOnce(new Error('network')); + const tryAgain = lastAlertButtons()?.find(button => button.text === 'Try again'); + act(() => { + tryAgain?.onPress?.(); + }); + await flushMicrotasks(); + + const failureButtons = lastAlertButtons(); + expect(failureButtons?.[0]?.text).toBe('Try again'); + expect(failureButtons?.[0]?.onPress).toBeDefined(); + + // Pressing the failure alert retry re-mints and reopens the flow. + act(() => { + failureButtons?.[0]?.onPress?.(); + }); + await flushMicrotasks(); + expect(mintInstallStateMock).toHaveBeenCalledTimes(2); + expect(openAuthSessionMock).toHaveBeenCalledWith( + 'https://web.test/github-app?installState=fresh-token&fromApp=1' + ); + act(() => { + renderer.unmount(); + }); + }); + + it('pending state: shows the awaiting-approval alert with Done on mount', () => { + setGitHubInstallReturnOutcome({ kind: 'pending' }); + const renderer = mountRoute(); + + expect(alertMock).toHaveBeenCalledTimes(1); + expect(alertMock).toHaveBeenCalledWith( + 'Awaiting admin approval', + 'An organization admin must approve the installation request.', + [{ text: 'Done' }] + ); + expect(getGitHubInstallReturnOutcome()).toBeNull(); + act(() => { + renderer.unmount(); + }); + }); + + it('non-retryable state: shows the reason with Back and never offers retry', () => { + setGitHubInstallReturnOutcome({ kind: 'error', code: 'not_installation_admin' }); + const renderer = mountRoute(); + + expect(alertMock).toHaveBeenCalledTimes(1); + expect(alertMock).toHaveBeenCalledWith( + 'Cannot complete installation', + 'Only a GitHub admin of that account can connect it. Ask an organization admin to install Kilo.', + [{ text: 'Back' }] + ); + const backButton = lastAlertButtons()?.[0]; + expect(backButton?.text).toBe('Back'); + expect(backButton?.onPress).toBeUndefined(); + expect(getGitHubInstallReturnOutcome()).toBeNull(); + act(() => { + renderer.unmount(); + }); + }); + + it('retryable state on Android: reopening uses the browser launcher', async () => { + setGitHubInstallReturnOutcome({ kind: 'error', code: 'installation_failed' }); + platformMock.OS = 'android'; + const renderer = mountRoute(); + + const tryAgain = lastAlertButtons()?.find(button => button.text === 'Try again'); + + act(() => { + tryAgain?.onPress?.(); + }); + await flushMicrotasks(); + + expect(openBrowserMock).toHaveBeenCalledWith( + 'https://web.test/github-app?installState=fresh-token&fromApp=1' + ); + expect(openAuthSessionMock).not.toHaveBeenCalled(); + act(() => { + renderer.unmount(); + }); + }); +}); diff --git a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx index 09653107c0..4046bf808e 100644 --- a/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx +++ b/apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx @@ -1,5 +1,141 @@ +import { useCallback, useEffect } from 'react'; +import * as WebBrowser from 'expo-web-browser'; +import { Alert, Platform } from 'react-native'; + import { AgentSessionListScreen } from '@/components/agents/session-list-screen'; +import { getGitHubIntegrationUrl } from '@/lib/agent-github-integration'; +import { WEB_BASE_URL } from '@/lib/config'; +import { + getGitHubInstallReturnOutcome, + type GitHubInstallReturnOutcome, + subscribeToGitHubInstallReturnOutcome, +} from '@/lib/github-install-return'; +import { trpcClient } from '@/lib/trpc'; + +export type GitHubInstallOutcomeAlertButton = { + text: string; + onPress?: () => void; +}; + +export type GitHubInstallOutcomeAlert = { + title: string; + message: string; + buttons: GitHubInstallOutcomeAlertButton[]; +}; + +/** + * C13 plan outcome states for the agents tab: happy, retryable unhappy, + * non-retryable unhappy, and empty (no outcome → null, no alert). + * The retryable state wires `onRetry` onto the `Try again` button so the + * alert is a real recovery action, not an inert dismiss. + */ +export function buildGitHubInstallOutcomeAlert( + result: GitHubInstallReturnOutcome, + onRetry: () => void +): GitHubInstallOutcomeAlert | null { + if (!result) { + return null; + } + + switch (result.kind) { + case 'success': { + return { + title: 'GitHub App installed', + message: 'Your repositories are now connected.', + buttons: [{ text: 'Continue' }], + }; + } + case 'pending': { + return { + title: 'Awaiting admin approval', + message: 'An organization admin must approve the installation request.', + buttons: [{ text: 'Done' }], + }; + } + case 'error': { + if (result.code === 'install_state_user_mismatch') { + return { + title: 'Account mismatch', + message: + 'This connection was started from the Kilo App signed in as a different account. Sign in to the web with that account, or start again from the app.', + buttons: [{ text: 'Back' }], + }; + } + if (result.code === 'not_installation_admin') { + return { + title: 'Cannot complete installation', + message: + 'Only a GitHub admin of that account can connect it. Ask an organization admin to install Kilo.', + buttons: [{ text: 'Back' }], + }; + } + if (result.code === 'installation_already_claimed') { + return { + title: 'Cannot complete installation', + message: + 'That GitHub installation is already connected to another Kilo account. Disconnect it there first.', + buttons: [{ text: 'Back' }], + }; + } + return { + title: 'Installation did not complete', + message: 'The GitHub App installation was not completed. Try again from the app.', + buttons: [{ text: 'Try again', onPress: onRetry }], + }; + } + default: { + return null; + } + } +} + +/** + * Recovery for a retryable installation outcome: the consumed C1 install + * state token is single-use, so mint a fresh one and reopen the web install + * flow. The flow returns to /cloud/sessions so the outcome can reach this + * tab again. + * + * When the original outcome was org-scoped, the retry mints and reopens the + * web flow with the same organizationId so the GitHub install stays on the + * original organization owner. + */ +async function retryGitHubInstall(organizationId?: string): Promise { + try { + const { token } = await trpcClient.githubApps.mintInstallState.mutate( + organizationId + ? { organizationId, returnTo: '/cloud/sessions' } + : { returnTo: '/cloud/sessions' } + ); + const url = getGitHubIntegrationUrl(WEB_BASE_URL, organizationId, token); + await (Platform.OS === 'android' + ? WebBrowser.openBrowserAsync(url) + : WebBrowser.openAuthSessionAsync(url)); + } catch { + // Mint or browser failure is retryable: keep a working retry action so + // the user can recover without restarting the whole install flow. + Alert.alert( + 'Could not open GitHub', + 'We could not start the GitHub App setup. Please try again.', + [{ text: 'Try again', onPress: () => void retryGitHubInstall(organizationId) }] + ); + } +} export default function AgentSessionList() { + const consumeReturnOutcome = useCallback(() => { + const result = getGitHubInstallReturnOutcome(); + const alert = buildGitHubInstallOutcomeAlert(result, () => { + void retryGitHubInstall(result?.organizationId); + }); + if (alert) { + Alert.alert(alert.title, alert.message, alert.buttons); + } + }, []); + + useEffect(() => { + consumeReturnOutcome(); + return subscribeToGitHubInstallReturnOutcome(consumeReturnOutcome); + }, [consumeReturnOutcome]); + return ; } diff --git a/apps/mobile/src/app/(app)/(tabs)/(3_profile)/code-reviewer/[scope]/[platform]/(edit)/repos.tsx b/apps/mobile/src/app/(app)/(tabs)/(3_profile)/code-reviewer/[scope]/[platform]/(edit)/repos.tsx index ff5f2114d7..b7e3175999 100644 --- a/apps/mobile/src/app/(app)/(tabs)/(3_profile)/code-reviewer/[scope]/[platform]/(edit)/repos.tsx +++ b/apps/mobile/src/app/(app)/(tabs)/(3_profile)/code-reviewer/[scope]/[platform]/(edit)/repos.tsx @@ -1,6 +1,7 @@ import { useLocalSearchParams } from 'expo-router'; import { FolderGit2 } from 'lucide-react-native'; import { View } from 'react-native'; +import { toast } from 'sonner-native'; import { EmptyState } from '@/components/empty-state'; import { QueryError } from '@/components/query-error'; @@ -15,6 +16,7 @@ import { getGitHubIntegrationUrl } from '@/lib/agent-github-integration'; import { PLATFORM_CAPABILITIES, type ReviewerPlatform } from '@/lib/code-reviewer-config'; import { WEB_BASE_URL } from '@/lib/config'; import { openExternalUrl } from '@/lib/external-link'; +import { trpcClient } from '@/lib/trpc'; import { PERSONAL_SCOPE, useBitbucketReadiness, @@ -81,11 +83,11 @@ export default function ReposRoute() { const confirmedEmpty = !reposLoading && !reposError && !bitbucketNotReady && repoRows.length === 0; const orgScope = scope === PERSONAL_SCOPE ? undefined : scope; - const manageRepoAccessUrlByPlatform: Partial> = { - github: getGitHubIntegrationUrl(WEB_BASE_URL, orgScope), - gitlab: getGitLabIntegrationUrl(WEB_BASE_URL, orgScope), + const manageRepoAccessLabelByPlatform: Partial> = { + github: 'repository access', + gitlab: 'repository access', }; - const manageRepoAccessUrl = manageRepoAccessUrlByPlatform[platform]; + const manageRepoAccessLabel = manageRepoAccessLabelByPlatform[platform]; const emptyStateCopyByPlatform: Record = { github: { @@ -184,11 +186,32 @@ export default function ReposRoute() { title={emptyStateCopy.title} description={emptyStateCopy.description} action={ - manageRepoAccessUrl ? ( + manageRepoAccessLabel ? ( )} diff --git a/apps/web/src/app/api/auth/native/admission-challenge/route.ts b/apps/web/src/app/api/auth/native/admission-challenge/route.ts new file mode 100644 index 0000000000..9dfcd0c717 --- /dev/null +++ b/apps/web/src/app/api/auth/native/admission-challenge/route.ts @@ -0,0 +1,42 @@ +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; +import * as z from 'zod'; +import { issueAdmissionChallenge, ChallengeRateLimitError } from '@/lib/auth/native-admission'; + +/** + * POST /api/auth/native/admission-challenge + * + * Issues a server-side attestation challenge for the mobile client. + * + * Request body: + * { platform: 'ios' | 'android' } + * + * Response: + * 200 { challenge: string, expiresIn: number } + * 400 invalid request + * 429 rate limited + */ +export async function POST(request: NextRequest) { + const body = await request.json().catch(() => undefined); + const validation = z.object({ platform: z.enum(['ios', 'android']) }).safeParse(body); + + if (!validation.success) { + return NextResponse.json({ error: 'INVALID_REQUEST' }, { status: 400 }); + } + + // Extract client IP for rate limiting + const ipAddress = + request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || + request.headers.get('x-real-ip') || + 'unknown'; + + try { + const result = await issueAdmissionChallenge(request, ipAddress); + return NextResponse.json(result, { status: 200 }); + } catch (error) { + if (error instanceof ChallengeRateLimitError) { + return NextResponse.json({ error: 'TOO_MANY_CHALLENGES' }, { status: 429 }); + } + throw error; + } +} diff --git a/apps/web/src/app/api/auth/native/exchange/route.test.ts b/apps/web/src/app/api/auth/native/exchange/route.test.ts new file mode 100644 index 0000000000..fd54eba5b0 --- /dev/null +++ b/apps/web/src/app/api/auth/native/exchange/route.test.ts @@ -0,0 +1,79 @@ +import { NextRequest } from 'next/server'; + +jest.mock('@/lib/user/server'); +jest.mock('@/lib/auth/device-sessions'); + +import { POST } from './route'; +import { getUserFromAuth } from '@/lib/user/server'; +import { createDeviceSession, issueSessionCredentials } from '@/lib/auth/device-sessions'; +import type { User } from '@kilocode/db/schema'; + +const mockGetUserFromAuth = jest.mocked(getUserFromAuth); +const mockCreateDeviceSession = jest.mocked(createDeviceSession); +const mockIssueSessionCredentials = jest.mocked(issueSessionCredentials); + +const fakeUser = { id: 'user-1', api_token_pepper: 'pepper' } as User; + +describe('POST /api/auth/native/exchange', () => { + const createRequest = () => + new NextRequest('http://localhost:3000/api/auth/native/exchange', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: 'Bearer old-token' }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns 200 with short-lived pair when authenticated', async () => { + mockGetUserFromAuth.mockResolvedValue({ + user: fakeUser, + authFailedResponse: null, + }); + mockCreateDeviceSession.mockResolvedValue('session-1'); + mockIssueSessionCredentials.mockResolvedValue({ + token: 'short-jwt', + refreshToken: 'refresh-abc', + expiresIn: 3600, + }); + + const response = await POST(createRequest()); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + token: 'short-jwt', + refreshToken: 'refresh-abc', + expiresIn: 3600, + }); + expect(mockCreateDeviceSession).toHaveBeenCalledWith({ + userId: fakeUser.id, + userAgent: undefined, + }); + expect(mockIssueSessionCredentials).toHaveBeenCalledWith(fakeUser, 'session-1'); + }); + + it('returns the auth failed response when token is invalid', async () => { + mockGetUserFromAuth.mockResolvedValue({ + user: null, + authFailedResponse: new Response(JSON.stringify({ error: 'Invalid token' }), { + status: 401, + headers: { 'content-type': 'application/json' }, + }) as any, + }); + + const response = await POST(createRequest()); + expect(response.status).toBe(401); + }); + + it('does not call createDeviceSession when auth fails', async () => { + mockGetUserFromAuth.mockResolvedValue({ + user: null, + authFailedResponse: new Response(JSON.stringify({ error: 'Invalid token' }), { + status: 401, + headers: { 'content-type': 'application/json' }, + }) as any, + }); + + await POST(createRequest()); + expect(mockCreateDeviceSession).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/app/api/auth/native/exchange/route.ts b/apps/web/src/app/api/auth/native/exchange/route.ts new file mode 100644 index 0000000000..556e44a76c --- /dev/null +++ b/apps/web/src/app/api/auth/native/exchange/route.ts @@ -0,0 +1,47 @@ +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; +import { getUserFromAuth } from '@/lib/user/server'; +import { createDeviceSession, issueSessionCredentials } from '@/lib/auth/device-sessions'; + +/** + * Token exchange endpoint. Authenticates with the existing long-lived bearer + * token, creates a device session, and returns a short-lived token pair. + * + * The old token is NOT invalidated. If the client crashes mid-exchange, it + * must still be able to start over with the same long-lived token. The old + * token dies of natural expiry. + * + * Response contract (frozen — mobile client is built against it): + * 200 { token, refreshToken, expiresIn } + * 401 — invalid or expired existing token + * 403 — blocked user + */ +export async function POST(request: NextRequest) { + const auth = await getUserFromAuth({ + adminOnly: false, + }); + + if (auth.authFailedResponse) { + return auth.authFailedResponse; + } + + if (!auth.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const sessionId = await createDeviceSession({ + userId: auth.user.id, + userAgent: request.headers.get('user-agent') ?? undefined, + }); + + const pair = await issueSessionCredentials(auth.user, sessionId); + + return NextResponse.json( + { + token: pair.token, + refreshToken: pair.refreshToken, + expiresIn: pair.expiresIn, + }, + { status: 200 } + ); +} diff --git a/apps/web/src/app/api/auth/native/otp/route.test.ts b/apps/web/src/app/api/auth/native/otp/route.test.ts index 4e0025f90d..adb050825e 100644 --- a/apps/web/src/app/api/auth/native/otp/route.test.ts +++ b/apps/web/src/app/api/auth/native/otp/route.test.ts @@ -14,6 +14,8 @@ const mockDeleteSignInCode = jest.mocked(deleteSignInCode); const mockSendSignInCodeEmail = jest.mocked(sendSignInCodeEmail); const mockCheckEmailSignInEligibility = jest.mocked(checkEmailSignInEligibility); +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + describe('POST /api/auth/native/otp', () => { const createRequest = (body: unknown) => new NextRequest('http://localhost:3000/api/auth/native/otp', { @@ -33,16 +35,20 @@ describe('POST /api/auth/native/otp', () => { jest.clearAllMocks(); mockCheckEmailSignInEligibility.mockResolvedValue({ ok: true }); - mockCreateSignInCode.mockResolvedValue('123456'); + mockCreateSignInCode.mockResolvedValue({ + code: '123456', + challengeId: 'c0000000-0000-0000-0000-000000000001', + }); mockSendSignInCodeEmail.mockResolvedValue({ sent: true }); }); - it('returns 200 { success: true } and sends the code by email', async () => { + it('returns 200 { success: true, challengeId } and sends the code by email', async () => { const response = await POST(createRequest({ email: 'user@example.com' })); const data = await response.json(); expect(response.status).toBe(200); - expect(data).toEqual({ success: true }); + expect(data.success).toBe(true); + expect(data.challengeId).toBe('c0000000-0000-0000-0000-000000000001'); expect(mockCreateSignInCode).toHaveBeenCalledWith('user@example.com'); expect(mockSendSignInCodeEmail).toHaveBeenCalledWith('user@example.com', '123456'); }); @@ -84,8 +90,127 @@ describe('POST /api/auth/native/otp', () => { const response = await POST(createRequest({ email: 'new+alias@example.com' })); expect(response.status).toBe(200); - expect(await response.json()).toEqual({ success: true }); + const data = await response.json(); + expect(data.success).toBe(true); + expect(data.challengeId).toMatch(UUID_REGEX); + expect(mockCreateSignInCode).not.toHaveBeenCalled(); + }); + + it('returns opaque 200 with a challengeId for a blocked domain', async () => { + mockCheckEmailSignInEligibility.mockResolvedValue({ + ok: false, + status: 403, + errorCode: 'BLOCKED', + body: { success: false, error: 'BLOCKED' }, + }); + + const response = await POST(createRequest({ email: 'user@blocked.example.com' })); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data.success).toBe(true); + expect(data.challengeId).toMatch(UUID_REGEX); + expect(mockCreateSignInCode).not.toHaveBeenCalled(); + expect(mockSendSignInCodeEmail).not.toHaveBeenCalled(); + }); + + it('returns identical body shape for blocked domain and eligible email', async () => { + // Eligible response + const eligibleResponse = await POST(createRequest({ email: 'eligible@example.com' })); + const eligibleData = await eligibleResponse.json(); + + // BLOCKED response + mockCheckEmailSignInEligibility.mockResolvedValue({ + ok: false, + status: 403, + errorCode: 'BLOCKED', + body: { success: false, error: 'BLOCKED' }, + }); + const blockedResponse = await POST(createRequest({ email: 'blocked@example.com' })); + const blockedData = await blockedResponse.json(); + + expect(eligibleResponse.status).toBe(200); + expect(blockedResponse.status).toBe(200); + expect(eligibleData).toHaveProperty('success', true); + expect(eligibleData).toHaveProperty('challengeId'); + expect(blockedData).toHaveProperty('success', true); + expect(blockedData).toHaveProperty('challengeId'); + expect(eligibleData.challengeId).toMatch(UUID_REGEX); + expect(blockedData.challengeId).toMatch(UUID_REGEX); + // Different challenge IDs (eligible real, blocked fake) + expect(eligibleData.challengeId).not.toBe(blockedData.challengeId); + }); + + it('allows a grandfathered user on a blocked TLD to sign in', async () => { + // Grandfathered users have existingUser: true in domain eligibility, + // which means checkEmailSignInEligibility returns { ok: true } even on a blocked TLD. + mockCheckEmailSignInEligibility.mockResolvedValue({ ok: true }); + + const response = await POST(createRequest({ email: 'existing@example.com' })); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + expect(data.challengeId).toBe('c0000000-0000-0000-0000-000000000001'); + expect(mockCreateSignInCode).toHaveBeenCalledWith('existing@example.com'); + expect(mockSendSignInCodeEmail).toHaveBeenCalled(); + }); + + it('returns indistinguishable status and body shape for blocked and grandfathered .zzz addresses', async () => { + // Blocked .zzz address — no code issued, no email sent + mockCheckEmailSignInEligibility.mockResolvedValue({ + ok: false, + status: 403, + errorCode: 'BLOCKED', + body: { success: false, error: 'BLOCKED' }, + }); + const blockedResponse = await POST(createRequest({ email: 'user@blocked.zzz' })); + const blockedData = await blockedResponse.json(); + + expect(blockedResponse.status).toBe(200); + expect(blockedData.success).toBe(true); + expect(blockedData.challengeId).toMatch(UUID_REGEX); expect(mockCreateSignInCode).not.toHaveBeenCalled(); + expect(mockSendSignInCodeEmail).not.toHaveBeenCalled(); + + // Grandfathered .zzz address (existingUser: true) — real code issued, real email sent + mockCheckEmailSignInEligibility.mockResolvedValue({ ok: true }); + const grandfatheredResponse = await POST(createRequest({ email: 'grandfathered@example.zzz' })); + const grandfatheredData = await grandfatheredResponse.json(); + + expect(grandfatheredResponse.status).toBe(200); + expect(grandfatheredData.success).toBe(true); + expect(grandfatheredData.challengeId).toBe('c0000000-0000-0000-0000-000000000001'); + expect(mockCreateSignInCode).toHaveBeenCalledWith('grandfathered@example.zzz'); + expect(mockSendSignInCodeEmail).toHaveBeenCalled(); + + // Status and body shape equality — an observer cannot distinguish the two cases + expect(blockedResponse.status).toBe(grandfatheredResponse.status); + expect(typeof blockedData.success).toBe(typeof grandfatheredData.success); + expect(typeof blockedData.challengeId).toBe(typeof grandfatheredData.challengeId); + }); + + it('preserves SSO_ERROR with ssoOrganizationId', async () => { + mockCheckEmailSignInEligibility.mockResolvedValue({ + ok: false, + status: 403, + errorCode: 'SSO_ERROR', + body: { + success: false, + error: 'Sign in with your organization SSO provider.', + ssoOrganizationId: 'org_abc123', + }, + }); + + const response = await POST(createRequest({ email: 'user@sso.example.com' })); + const data = await response.json(); + + expect(response.status).toBe(403); + expect(data).toEqual({ + success: false, + error: 'SSO_ERROR', + ssoOrganizationId: 'org_abc123', + }); }); it.each([ @@ -135,4 +260,138 @@ describe('POST /api/auth/native/otp', () => { expect(response.status).toBe(400); expect(await response.json()).toEqual({ success: false, error: 'INVALID_REQUEST' }); }); + + describe('timing floor', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('enforces the 250 ms floor on the eligible branch', async () => { + let resolved = false; + const promise = POST(createRequest({ email: 'user@example.com' })); + void promise.then(() => { + resolved = true; + }); + + // Drain non-timer async work — the promise must still be pending + // because setTimeout has not fired. + await Promise.resolve(); + expect(resolved).toBe(false); + + // At 249 ms the floor has not expired. + await jest.advanceTimersByTimeAsync(249); + expect(resolved).toBe(false); + + // At 250 ms the floor expires and the response resolves. + await jest.advanceTimersByTimeAsync(1); + const response = await promise; + + expect(resolved).toBe(true); + expect(response.status).toBe(200); + }); + + it('enforces the 250 ms floor on the blocked branch', async () => { + mockCheckEmailSignInEligibility.mockResolvedValue({ + ok: false, + status: 403, + errorCode: 'BLOCKED', + body: { success: false, error: 'BLOCKED' }, + }); + + let resolved = false; + const promise = POST(createRequest({ email: 'blocked@example.com' })); + void promise.then(() => { + resolved = true; + }); + + await Promise.resolve(); + expect(resolved).toBe(false); + + await jest.advanceTimersByTimeAsync(249); + expect(resolved).toBe(false); + + await jest.advanceTimersByTimeAsync(1); + const response = await promise; + + expect(resolved).toBe(true); + expect(response.status).toBe(200); + }); + + it('enforces the 250 ms floor on the 400 invalid-body branch', async () => { + let resolved = false; + const promise = POST(createRequest({ email: 'not-an-email' })); + void promise.then(() => { + resolved = true; + }); + + await Promise.resolve(); + expect(resolved).toBe(false); + + await jest.advanceTimersByTimeAsync(249); + expect(resolved).toBe(false); + + await jest.advanceTimersByTimeAsync(1); + const response = await promise; + + expect(resolved).toBe(true); + expect(response.status).toBe(400); + }); + + it('enforces the floor on the SSO_ERROR branch', async () => { + mockCheckEmailSignInEligibility.mockResolvedValue({ + ok: false, + status: 403, + errorCode: 'SSO_ERROR', + body: { + success: false, + error: 'Sign in with your organization SSO provider.', + ssoOrganizationId: 'org_abc123', + }, + }); + + let resolved = false; + const promise = POST(createRequest({ email: 'user@sso.example.com' })); + void promise.then(() => { + resolved = true; + }); + + await Promise.resolve(); + expect(resolved).toBe(false); + + await jest.advanceTimersByTimeAsync(249); + expect(resolved).toBe(false); + + await jest.advanceTimersByTimeAsync(1); + const response = await promise; + + expect(resolved).toBe(true); + expect(response.status).toBe(403); + }); + + it('enforces the floor on the email delivery failure branch', async () => { + mockSendSignInCodeEmail.mockResolvedValue({ sent: false, reason: 'provider_not_configured' }); + + let resolved = false; + const promise = POST(createRequest({ email: 'user@example.com' })); + void promise.then(() => { + resolved = true; + }); + + await Promise.resolve(); + expect(resolved).toBe(false); + + await jest.advanceTimersByTimeAsync(249); + expect(resolved).toBe(false); + + await jest.advanceTimersByTimeAsync(1); + const response = await promise; + + expect(resolved).toBe(true); + expect(response.status).toBe(500); + }); + }); }); diff --git a/apps/web/src/app/api/auth/native/otp/route.ts b/apps/web/src/app/api/auth/native/otp/route.ts index 0e3a07e2cd..6eaa21a56b 100644 --- a/apps/web/src/app/api/auth/native/otp/route.ts +++ b/apps/web/src/app/api/auth/native/otp/route.ts @@ -1,5 +1,6 @@ import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; +import { randomUUID } from 'node:crypto'; import { createSignInCode, deleteSignInCode } from '@/lib/auth/magic-link-tokens'; import { sendSignInCodeEmail } from '@/lib/email'; import * as z from 'zod'; @@ -9,18 +10,38 @@ const requestSchema = z.object({ email: z.string().email(), }); +/** + * Minimum response time for enumeration resistance. + * Measured eligible median 21.4 ms, maximum 53.0 ms; 250 ms leaves room for + * slower production databases while keeping latency invisible to a user typing. + */ +const RESPONSE_FLOOR_MS = 250; + +async function enforceResponseFloor(startTime: number): Promise { + const elapsed = Date.now() - startTime; + if (elapsed < RESPONSE_FLOOR_MS) { + await new Promise(resolve => setTimeout(resolve, RESPONSE_FLOOR_MS - elapsed)); + } +} + /** * API route to request an email sign-in code for native mobile sign-in. * Validates eligibility, issues a 6-digit code, and emails it. * - * The response is identical (200 { success: true }) whether or not a user - * exists for the email, to avoid leaking account existence. + * The response is identical (200 { success: true, challengeId }) for every + * syntactically valid email that is not SSO-governed, to avoid leaking + * account existence. A challengeId returned for a blocked address maps to no + * row and verifies as INVALID_CODE, identical to an eligible-but-wrong-code + * attempt. */ export async function POST(request: NextRequest) { + const startTime = Date.now(); + const body = await request.json().catch(() => undefined); const validation = requestSchema.safeParse(body); if (!validation.success) { + await enforceResponseFloor(startTime); return NextResponse.json({ success: false, error: 'INVALID_REQUEST' }, { status: 400 }); } @@ -29,8 +50,14 @@ export async function POST(request: NextRequest) { const eligibility = await checkEmailSignInEligibility(email, request); if (!eligibility.ok) { if (eligibility.errorCode === 'INVALID_EMAIL') { - return NextResponse.json({ success: true }); + await enforceResponseFloor(startTime); + return NextResponse.json({ success: true, challengeId: randomUUID() }); + } + if (eligibility.errorCode === 'BLOCKED') { + await enforceResponseFloor(startTime); + return NextResponse.json({ success: true, challengeId: randomUUID() }); } + await enforceResponseFloor(startTime); return NextResponse.json( { success: false, @@ -43,11 +70,12 @@ export async function POST(request: NextRequest) { ); } - const code = await createSignInCode(email); + const { code, challengeId } = await createSignInCode(email); const result = await sendSignInCodeEmail(email, code); if (!result.sent) { await deleteSignInCode(email, code); const neverbounceRejected = result.reason === 'neverbounce_rejected'; + await enforceResponseFloor(startTime); return NextResponse.json( { success: false, @@ -57,5 +85,6 @@ export async function POST(request: NextRequest) { ); } - return NextResponse.json({ success: true }); + await enforceResponseFloor(startTime); + return NextResponse.json({ success: true, challengeId }); } diff --git a/apps/web/src/app/api/auth/native/refresh/route.test.ts b/apps/web/src/app/api/auth/native/refresh/route.test.ts new file mode 100644 index 0000000000..9e79091270 --- /dev/null +++ b/apps/web/src/app/api/auth/native/refresh/route.test.ts @@ -0,0 +1,98 @@ +import { NextRequest } from 'next/server'; + +jest.mock('@/lib/auth/device-sessions'); + +import { POST } from './route'; +import { rotateRefreshToken } from '@/lib/auth/device-sessions'; + +const mockRotateRefreshToken = jest.mocked(rotateRefreshToken); + +describe('POST /api/auth/native/refresh', () => { + const createRequest = (body: unknown) => + new NextRequest('http://localhost:3000/api/auth/native/refresh', { + method: 'POST', + body: JSON.stringify(body), + headers: { 'Content-Type': 'application/json' }, + }); + + const createMalformedRequest = () => + new NextRequest('http://localhost:3000/api/auth/native/refresh', { + method: 'POST', + body: '{', + headers: { 'Content-Type': 'application/json' }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns 200 with new pair on successful rotation', async () => { + mockRotateRefreshToken.mockResolvedValue({ + ok: true, + token: 'new-access-token', + refreshToken: 'new-refresh-token', + expiresIn: 3600, + }); + + const response = await POST(createRequest({ refreshToken: 'valid-refresh' })); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + token: 'new-access-token', + refreshToken: 'new-refresh-token', + expiresIn: 3600, + }); + }); + + it('returns 401 for unknown refresh token', async () => { + mockRotateRefreshToken.mockResolvedValue({ + ok: false, + error: 'INVALID_REFRESH_TOKEN', + }); + + const response = await POST(createRequest({ refreshToken: 'bogus' })); + expect(response.status).toBe(401); + expect(await response.json()).toEqual({ error: 'INVALID_REFRESH_TOKEN' }); + }); + + it('returns 401 for reused refresh token', async () => { + mockRotateRefreshToken.mockResolvedValue({ + ok: false, + error: 'INVALID_REFRESH_TOKEN', + }); + + const response = await POST(createRequest({ refreshToken: 'reused-refresh' })); + expect(response.status).toBe(401); + }); + + it('returns 401 for revoked session', async () => { + mockRotateRefreshToken.mockResolvedValue({ + ok: false, + error: 'SESSION_REVOKED', + }); + + const response = await POST(createRequest({ refreshToken: 'revoked-session-refresh' })); + expect(response.status).toBe(401); + expect(await response.json()).toEqual({ error: 'SESSION_REVOKED' }); + }); + + it('returns 401 for blocked user', async () => { + mockRotateRefreshToken.mockResolvedValue({ + ok: false, + error: 'USER_BLOCKED', + }); + + const response = await POST(createRequest({ refreshToken: 'blocked-user-refresh' })); + expect(response.status).toBe(401); + expect(await response.json()).toEqual({ error: 'USER_BLOCKED' }); + }); + + it('returns 400 for missing refreshToken', async () => { + const response = await POST(createRequest({})); + expect(response.status).toBe(400); + }); + + it('returns 400 for malformed JSON', async () => { + const response = await POST(createMalformedRequest()); + expect(response.status).toBe(400); + }); +}); diff --git a/apps/web/src/app/api/auth/native/refresh/route.ts b/apps/web/src/app/api/auth/native/refresh/route.ts new file mode 100644 index 0000000000..0b13872afd --- /dev/null +++ b/apps/web/src/app/api/auth/native/refresh/route.ts @@ -0,0 +1,45 @@ +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; +import * as z from 'zod'; +import { rotateRefreshToken } from '@/lib/auth/device-sessions'; + +const requestSchema = z.object({ + refreshToken: z.string().min(1), +}); + +/** + * Native refresh endpoint. Accepts a refresh token and returns a new + * access/refresh pair. + * + * Response contract (frozen — mobile client is built against it): + * 200 { token, refreshToken, expiresIn } + * 401 { error: 'INVALID_REFRESH_TOKEN' } — unknown, expired, or reused refresh token + * 401 { error: 'SESSION_REVOKED' } — the parent device session was revoked + * 401 { error: 'USER_BLOCKED' } — the user's account is blocked + * 400 — invalid request body + */ +export async function POST(request: NextRequest) { + const body = await request.json().catch(() => undefined); + const validation = requestSchema.safeParse(body); + + if (!validation.success) { + return NextResponse.json({ error: 'INVALID_REQUEST' }, { status: 400 }); + } + + const { refreshToken } = validation.data; + + const result = await rotateRefreshToken(refreshToken); + + if (!result.ok) { + return NextResponse.json({ error: result.error }, { status: 401 }); + } + + return NextResponse.json( + { + token: result.token, + refreshToken: result.refreshToken, + expiresIn: result.expiresIn, + }, + { status: 200 } + ); +} diff --git a/apps/web/src/app/api/auth/native/token/route.test.ts b/apps/web/src/app/api/auth/native/token/route.test.ts index fff765076d..577920c450 100644 --- a/apps/web/src/app/api/auth/native/token/route.test.ts +++ b/apps/web/src/app/api/auth/native/token/route.test.ts @@ -2,9 +2,15 @@ import { NextRequest } from 'next/server'; import { verifyNativeAppleIdToken, verifyNativeGoogleIdToken, + exchangeNativeGoogleAuthCode, NativeIdTokenError, } from '@/lib/auth/native-id-tokens'; -import { verifyAndConsumeSignInCode } from '@/lib/auth/magic-link-tokens'; +import { + reserveSignInCode, + commitSignInCode, + releaseSignInCode, + consumeSignInCode, +} from '@/lib/auth/magic-link-tokens'; import { createOrUpdateUser, findUserById, @@ -21,23 +27,83 @@ jest.mock('@/lib/auth/native-id-tokens', () => ({ ...jest.requireActual('@/lib/auth/native-id-tokens'), verifyNativeAppleIdToken: jest.fn(), verifyNativeGoogleIdToken: jest.fn(), + exchangeNativeGoogleAuthCode: jest.fn(), })); jest.mock('@/lib/auth/magic-link-tokens'); jest.mock('@/lib/user'); jest.mock('@/lib/tokens'); jest.mock('@/lib/auth/email-signin-eligibility'); +jest.mock('@/lib/auth/native-admission', () => ({ + ...jest.requireActual('@/lib/auth/native-admission'), + checkNativeAdmission: jest.fn(), + validateAdmissionPayload: jest.fn(), + verifyAdmissionAsync: jest.fn(), + persistAttestedKey: jest.fn(), + shouldRefuseAsyncFailure: jest.fn(), +})); +jest.mock('@/lib/auth/device-sessions'); +jest.mock('@/lib/config.server', () => ({ + GOOGLE_CLIENT_ID: 'web-client-id', +})); +jest.mock('@sentry/nextjs', () => ({ + captureMessage: jest.fn(), +})); + +// eslint-disable-next-line no-var +var mockPosthogCapture: jest.Mock; +jest.mock('@/lib/posthog', () => { + const capture = jest.fn(); + mockPosthogCapture = capture; + return { + __esModule: true, + default: jest.fn(() => ({ + capture, + isFeatureEnabled: jest.fn(), + getFeatureFlag: jest.fn(), + debug: jest.fn(), + alias: jest.fn(), + })), + }; +}); import { POST } from './route'; +import { + checkNativeAdmission, + validateAdmissionPayload, + verifyAdmissionAsync, + persistAttestedKey, + shouldRefuseAsyncFailure, + KeyCollisionError, +} from '@/lib/auth/native-admission'; +import { + createDeviceSession, + issueSessionCredentials, + createDeviceSessionWithAttestedKey, +} from '@/lib/auth/device-sessions'; +import { captureMessage } from '@sentry/nextjs'; const mockVerifyNativeAppleIdToken = jest.mocked(verifyNativeAppleIdToken); const mockVerifyNativeGoogleIdToken = jest.mocked(verifyNativeGoogleIdToken); -const mockVerifyAndConsumeSignInCode = jest.mocked(verifyAndConsumeSignInCode); +const mockExchangeNativeGoogleAuthCode = jest.mocked(exchangeNativeGoogleAuthCode); +const mockReserveSignInCode = jest.mocked(reserveSignInCode); +const mockCommitSignInCode = jest.mocked(commitSignInCode); +const mockReleaseSignInCode = jest.mocked(releaseSignInCode); +const mockConsumeSignInCode = jest.mocked(consumeSignInCode); const mockCreateOrUpdateUser = jest.mocked(createOrUpdateUser); const mockFindUserById = jest.mocked(findUserById); const mockFindUserByNormalizedEmail = jest.mocked(findUserByNormalizedEmail); const mockFindUserIdByAuthProvider = jest.mocked(findUserIdByAuthProvider); const mockGenerateApiToken = jest.mocked(generateApiToken); const mockCheckDomainSignInEligibility = jest.mocked(checkDomainSignInEligibility); +const mockCheckNativeAdmission = jest.mocked(checkNativeAdmission); +const mockValidateAdmissionPayload = jest.mocked(validateAdmissionPayload); +const mockVerifyAdmissionAsync = jest.mocked(verifyAdmissionAsync); +const mockPersistAttestedKey = jest.mocked(persistAttestedKey); +const mockShouldRefuseAsyncFailure = jest.mocked(shouldRefuseAsyncFailure); +const mockCreateDeviceSession = jest.mocked(createDeviceSession); +const mockIssueSessionCredentials = jest.mocked(issueSessionCredentials); +const mockCreateDeviceSessionWithAttestedKey = jest.mocked(createDeviceSessionWithAttestedKey); +const mockCaptureMessage = jest.mocked(captureMessage); const fakeUser = { id: 'user-1', api_token_pepper: 'pepper' } as User; @@ -68,6 +134,21 @@ describe('POST /api/auth/native/token', () => { mockFindUserById.mockResolvedValue(undefined); mockFindUserByNormalizedEmail.mockResolvedValue(undefined); mockFindUserIdByAuthProvider.mockResolvedValue(null); + mockCheckNativeAdmission.mockReturnValue({ admission: { ok: true }, verifyAsync: true }); + mockValidateAdmissionPayload.mockReturnValue(undefined); + mockVerifyAdmissionAsync.mockResolvedValue({ ok: true, platform: 'ios', keyId: 'key1' }); + mockPersistAttestedKey.mockResolvedValue(undefined); + mockShouldRefuseAsyncFailure.mockReturnValue(false); + mockCreateDeviceSessionWithAttestedKey.mockResolvedValue({ + token: 'short-jwt', + refreshToken: 'refresh-xyz', + expiresIn: 3600, + sessionId: 'session-1', + }); + mockReserveSignInCode.mockResolvedValue('ok'); + mockCommitSignInCode.mockResolvedValue(true); + mockReleaseSignInCode.mockResolvedValue(undefined); + mockConsumeSignInCode.mockResolvedValue(true); }); describe('apple', () => { @@ -84,7 +165,7 @@ describe('POST /api/auth/native/token', () => { expect(response.status).toBe(200); expect(data).toEqual({ token: 'minted-jwt' }); - expect(mockVerifyNativeAppleIdToken).toHaveBeenCalledWith('apple-id-token'); + expect(mockVerifyNativeAppleIdToken).toHaveBeenCalledWith('apple-id-token', undefined); expect(mockCreateOrUpdateUser).toHaveBeenCalledWith( expect.objectContaining({ google_user_email: 'appleuser@example.com', @@ -95,7 +176,10 @@ describe('POST /api/auth/native/token', () => { }), undefined, false, - expect.any(Headers) + expect.any(Headers), + undefined, + undefined, + true ); expect(mockGenerateApiToken).toHaveBeenCalledWith(fakeUser); }); @@ -112,7 +196,10 @@ describe('POST /api/auth/native/token', () => { expect.objectContaining({ google_user_name: 'appleuser' }), undefined, false, - expect.any(Headers) + expect.any(Headers), + undefined, + undefined, + true ); }); @@ -175,6 +262,40 @@ describe('POST /api/auth/native/token', () => { expect(data).toEqual({ error: 'BLOCKED' }); expect(mockCreateOrUpdateUser).not.toHaveBeenCalled(); }); + + // C12: nonce forwarding + it('passes the raw nonce to verifyNativeAppleIdToken', async () => { + mockVerifyNativeAppleIdToken.mockResolvedValue({ + sub: 'apple-sub-1', + email: 'appleuser@example.com', + }); + + const response = await POST( + createRequest({ + provider: 'apple', + idToken: 'apple-id-token', + nonce: 'raw-nonce-from-client', + }) + ); + + expect(response.status).toBe(200); + expect(mockVerifyNativeAppleIdToken).toHaveBeenCalledWith( + 'apple-id-token', + 'raw-nonce-from-client' + ); + }); + + it('provides no nonce when the client sends none (legacy)', async () => { + mockVerifyNativeAppleIdToken.mockResolvedValue({ + sub: 'apple-sub-1', + email: 'appleuser@example.com', + }); + + const response = await POST(createRequest({ provider: 'apple', idToken: 'apple-id-token' })); + + expect(response.status).toBe(200); + expect(mockVerifyNativeAppleIdToken).toHaveBeenCalledWith('apple-id-token', undefined); + }); }); describe('google', () => { @@ -205,7 +326,10 @@ describe('POST /api/auth/native/token', () => { }), undefined, false, - expect.any(Headers) + expect.any(Headers), + undefined, + undefined, + true ); }); @@ -221,7 +345,10 @@ describe('POST /api/auth/native/token', () => { expect.objectContaining({ hosted_domain: '@@personal@@' }), undefined, false, - expect.any(Headers) + expect.any(Headers), + undefined, + undefined, + true ); }); @@ -304,12 +431,105 @@ describe('POST /api/auth/native/token', () => { expect(data).toEqual({ token: 'minted-jwt' }); expect(mockCreateOrUpdateUser).toHaveBeenCalled(); }); + + // C12: Google serverAuthCode flow + it('uses exchangeNativeGoogleAuthCode when serverAuthCode is present', async () => { + mockExchangeNativeGoogleAuthCode.mockResolvedValue({ + sub: 'google-sub-1', + email: 'googleuser@example.com', + name: 'Google User', + picture: 'https://example.com/pic.png', + hd: 'example.com', + }); + + const response = await POST( + createRequest({ + provider: 'google', + serverAuthCode: 'auth-code-123', + googleClientId: 'web-client-id', + }) + ); + + expect(response.status).toBe(200); + expect(mockExchangeNativeGoogleAuthCode).toHaveBeenCalledWith('auth-code-123'); + expect(mockVerifyNativeGoogleIdToken).not.toHaveBeenCalled(); + expect(mockCaptureMessage).not.toHaveBeenCalledWith('native_google_idtoken_legacy_count: 1'); + }); + + it('falls back to verifyNativeGoogleIdToken when only idToken is present and counts legacy', async () => { + mockVerifyNativeGoogleIdToken.mockResolvedValue({ + sub: 'google-sub-1', + email: 'googleuser@example.com', + }); + + const response = await POST( + createRequest({ provider: 'google', idToken: 'google-id-token' }) + ); + + expect(response.status).toBe(200); + expect(mockVerifyNativeGoogleIdToken).toHaveBeenCalledWith('google-id-token'); + expect(mockExchangeNativeGoogleAuthCode).not.toHaveBeenCalled(); + expect(mockCaptureMessage).toHaveBeenCalledWith('native_google_idtoken_legacy_count: 1'); + }); + + it('returns 400 INVALID_REQUEST when neither serverAuthCode nor idToken is provided', async () => { + const response = await POST(createRequest({ provider: 'google' })); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data).toEqual({ error: 'INVALID_REQUEST' }); + expect(mockCreateOrUpdateUser).not.toHaveBeenCalled(); + }); + + it('returns 401 INVALID_TOKEN when serverAuthCode exchange fails with NativeIdTokenError', async () => { + mockExchangeNativeGoogleAuthCode.mockRejectedValue(new NativeIdTokenError('exchange failed')); + + const response = await POST( + createRequest({ + provider: 'google', + serverAuthCode: 'replayed-code', + googleClientId: 'web-client-id', + }) + ); + const data = await response.json(); + + expect(response.status).toBe(401); + expect(data).toEqual({ error: 'INVALID_TOKEN' }); + }); + + it('rethrows (500) when serverAuthCode exchange fails with a non-token error (network failure)', async () => { + const networkError = new Error('connect ETIMEDOUT'); + mockExchangeNativeGoogleAuthCode.mockRejectedValue(networkError); + + await expect( + POST( + createRequest({ + provider: 'google', + serverAuthCode: 'auth-code', + googleClientId: 'web-client-id', + }) + ) + ).rejects.toBe(networkError); + }); + + it('rejects a serverAuthCode from a different mobile web client', async () => { + const response = await POST( + createRequest({ + provider: 'google', + serverAuthCode: 'auth-code', + googleClientId: 'wrong-client-id', + }) + ); + + expect(response.status).toBe(400); + expect(await response.json()).toEqual({ error: 'INVALID_REQUEST' }); + expect(mockExchangeNativeGoogleAuthCode).not.toHaveBeenCalled(); + expect(mockCreateOrUpdateUser).not.toHaveBeenCalled(); + }); }); describe('email', () => { - it('verifies the code before createOrUpdateUser, builds args mirroring createEmailAccountInfo, autoLink=true', async () => { - mockVerifyAndConsumeSignInCode.mockResolvedValue('ok'); - + it('reserves, settles, commits, and mints a token', async () => { const response = await POST( createRequest({ provider: 'email', email: 'emailuser@example.com', code: '123456' }) ); @@ -317,9 +537,15 @@ describe('POST /api/auth/native/token', () => { expect(response.status).toBe(200); expect(data).toEqual({ token: 'minted-jwt' }); - expect(mockVerifyAndConsumeSignInCode).toHaveBeenCalledWith( + expect(mockReserveSignInCode).toHaveBeenCalledWith( + 'emailuser@example.com', + '123456', + undefined + ); + expect(mockCommitSignInCode).toHaveBeenCalledWith( 'emailuser@example.com', - '123456' + '123456', + undefined ); expect(mockCreateOrUpdateUser).toHaveBeenCalledWith( expect.objectContaining({ @@ -331,13 +557,43 @@ describe('POST /api/auth/native/token', () => { }), undefined, true, - expect.any(Headers) + expect.any(Headers), + undefined, + undefined, + true ); expect(mockCheckDomainSignInEligibility).toHaveBeenCalledWith('emailuser@example.com'); + expect(mockReleaseSignInCode).not.toHaveBeenCalled(); + expect(mockConsumeSignInCode).not.toHaveBeenCalled(); + }); + + it('passes challengeId through to reserve/commit when the client sends it', async () => { + const challengeId = 'c0000000-0000-4000-8000-000000000001'; + const response = await POST( + createRequest({ + provider: 'email', + email: 'emailuser@example.com', + code: '123456', + challengeId, + }) + ); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data).toEqual({ token: 'minted-jwt' }); + expect(mockReserveSignInCode).toHaveBeenCalledWith( + 'emailuser@example.com', + '123456', + challengeId + ); + expect(mockCommitSignInCode).toHaveBeenCalledWith( + 'emailuser@example.com', + '123456', + challengeId + ); }); it('rechecks domain eligibility when redeeming an issued code', async () => { - mockVerifyAndConsumeSignInCode.mockResolvedValue('ok'); mockCheckDomainSignInEligibility.mockResolvedValue({ ok: false, status: 403, @@ -354,12 +610,11 @@ describe('POST /api/auth/native/token', () => { error: 'SSO_ERROR', ssoOrganizationId: 'workos-organization-id', }); + expect(mockReleaseSignInCode).toHaveBeenCalled(); expect(mockCreateOrUpdateUser).not.toHaveBeenCalled(); }); it('lowercases the client-supplied email before building args (does not trust client casing)', async () => { - mockVerifyAndConsumeSignInCode.mockResolvedValue('ok'); - await POST( createRequest({ provider: 'email', email: 'EmailUser@Example.com', code: '123456' }) ); @@ -373,12 +628,15 @@ describe('POST /api/auth/native/token', () => { }), undefined, true, - expect.any(Headers) + expect.any(Headers), + undefined, + undefined, + true ); }); it('returns 401 INVALID_CODE when the code is invalid, without calling createOrUpdateUser', async () => { - mockVerifyAndConsumeSignInCode.mockResolvedValue('invalid'); + mockReserveSignInCode.mockResolvedValue('invalid'); const response = await POST( createRequest({ provider: 'email', email: 'emailuser@example.com', code: '000000' }) @@ -390,8 +648,27 @@ describe('POST /api/auth/native/token', () => { expect(mockCreateOrUpdateUser).not.toHaveBeenCalled(); }); + it('returns 401 INVALID_CODE for a challengeId from an ineligible OTP response', async () => { + // The OTP route returns a fake challengeId for blocked or invalid + // addresses. Token verification must treat it as INVALID_CODE. + mockReserveSignInCode.mockResolvedValue('invalid'); + + const response = await POST( + createRequest({ + provider: 'email', + email: 'blocked@example.com', + code: '123456', + challengeId: 'a0000000-0000-4000-8000-000000000999', + }) + ); + + expect(response.status).toBe(401); + expect(await response.json()).toEqual({ error: 'INVALID_CODE' }); + expect(mockCreateOrUpdateUser).not.toHaveBeenCalled(); + }); + it('returns 429 TOO_MANY_ATTEMPTS when the attempt budget is exhausted', async () => { - mockVerifyAndConsumeSignInCode.mockResolvedValue('too_many_attempts'); + mockReserveSignInCode.mockResolvedValue('too_many_attempts'); const response = await POST( createRequest({ provider: 'email', email: 'emailuser@example.com', code: '000000' }) @@ -402,6 +679,191 @@ describe('POST /api/auth/native/token', () => { expect(data).toEqual({ error: 'TOO_MANY_ATTEMPTS' }); expect(mockCreateOrUpdateUser).not.toHaveBeenCalled(); }); + + it('returns 425 CODE_IN_PROGRESS when another request holds the reservation', async () => { + mockReserveSignInCode.mockResolvedValue('in_progress'); + + const response = await POST( + createRequest({ provider: 'email', email: 'emailuser@example.com', code: '123456' }) + ); + const data = await response.json(); + + expect(response.status).toBe(425); + expect(data).toEqual({ error: 'CODE_IN_PROGRESS' }); + expect(mockCreateOrUpdateUser).not.toHaveBeenCalled(); + }); + + it('releases the reservation when createOrUpdateUser fails', async () => { + mockCreateOrUpdateUser.mockResolvedValue({ success: false, error: 'BLOCKED' } as never); + + const response = await POST( + createRequest({ provider: 'email', email: 'emailuser@example.com', code: '123456' }) + ); + + expect(response.status).toBe(403); + expect(mockReleaseSignInCode).toHaveBeenCalled(); + }); + + it('releases the code on DIFFERENT-OAUTH settlement failure so retry succeeds', async () => { + mockCreateOrUpdateUser.mockResolvedValue({ + success: false, + error: 'DIFFERENT-OAUTH', + } as never); + + const response = await POST( + createRequest({ + provider: 'email', + email: 'different-oauth@example.com', + code: '123456', + }) + ); + + expect(response.status).toBe(403); + expect(await response.json()).toEqual({ error: 'DIFFERENT-OAUTH' }); + expect(mockReleaseSignInCode).toHaveBeenCalled(); + // The code was released, not consumed — no commit or consume occurred. + expect(mockCommitSignInCode).not.toHaveBeenCalled(); + expect(mockConsumeSignInCode).not.toHaveBeenCalled(); + }); + + it('retry after DIFFERENT-OAUTH release reserves and settles the same code', async () => { + // First call: DIFFERENT-OAUTH triggers release. The second call must + // re-reserve the same code and settle successfully, proving the release. + mockCreateOrUpdateUser + .mockResolvedValueOnce({ + success: false, + error: 'DIFFERENT-OAUTH', + } as never) + // Second call falls back to the beforeEach success default. + .mockResolvedValueOnce({ + success: true, + user: fakeUser, + isNew: false, + } as never); + + const email = 'retry-oauth@example.com'; + const code = '654321'; + + // First attempt: DIFFERENT-OAUTH → release. + const first = await POST(createRequest({ provider: 'email', email, code })); + expect(first.status).toBe(403); + expect(await first.json()).toEqual({ error: 'DIFFERENT-OAUTH' }); + expect(mockReleaseSignInCode).toHaveBeenCalledWith(email, code, undefined); + expect(mockCommitSignInCode).not.toHaveBeenCalled(); + expect(mockConsumeSignInCode).not.toHaveBeenCalled(); + + // Second attempt: code was released, re-reserve succeeds, settle works. + const second = await POST(createRequest({ provider: 'email', email, code })); + expect(second.status).toBe(200); + expect(await second.json()).toEqual({ token: 'minted-jwt' }); + + // One account minted across both attempts. + expect(mockCreateOrUpdateUser).toHaveBeenCalledTimes(2); + expect(mockGenerateApiToken).toHaveBeenCalledTimes(1); + expect(mockCommitSignInCode).toHaveBeenCalledTimes(1); + expect(mockCommitSignInCode).toHaveBeenCalledWith(email, code, undefined); + // releaseSignInCode was only called once (on the first failure). + expect(mockReleaseSignInCode).toHaveBeenCalledTimes(1); + }); + + it('releases the reservation on an unexpected error', async () => { + mockCreateOrUpdateUser.mockRejectedValue(new Error('DB crash')); + + await expect( + POST(createRequest({ provider: 'email', email: 'emailuser@example.com', code: '123456' })) + ).rejects.toThrow('DB crash'); + expect(mockReleaseSignInCode).toHaveBeenCalled(); + }); + + it('unconditionally consumes the code when the reservation lapsed mid-settlement', async () => { + mockCommitSignInCode.mockResolvedValue(false); + + const response = await POST( + createRequest({ provider: 'email', email: 'emailuser@example.com', code: '123456' }) + ); + + expect(response.status).toBe(200); + expect(mockConsumeSignInCode).toHaveBeenCalledWith( + 'emailuser@example.com', + '123456', + undefined + ); + expect(mockCaptureMessage).toHaveBeenCalledWith('native_token_code_reservation_lapsed'); + }); + + it('returns 401 INVALID_CODE when lapse-consume fails (another request already consumed the code)', async () => { + mockCommitSignInCode.mockResolvedValue(false); + mockConsumeSignInCode.mockResolvedValue(false); + + const response = await POST( + createRequest({ provider: 'email', email: 'emailuser@example.com', code: '123456' }) + ); + + expect(response.status).toBe(401); + expect(await response.json()).toEqual({ error: 'INVALID_CODE' }); + // No credentials must be issued when consumeSignInCode fails. + expect(mockGenerateApiToken).not.toHaveBeenCalled(); + expect(mockCreateDeviceSession).not.toHaveBeenCalled(); + expect(mockReleaseSignInCode).not.toHaveBeenCalled(); + }); + + it('reservation-lapse full-flow produces one account and one device session', async () => { + mockCommitSignInCode.mockResolvedValue(false); + mockConsumeSignInCode.mockResolvedValue(true); + mockCreateDeviceSession.mockResolvedValue('session-lapse-1'); + mockIssueSessionCredentials.mockResolvedValue({ + token: 'short-jwt', + refreshToken: 'refresh-xyz', + expiresIn: 3600, + }); + + const response = await POST( + createRequest({ + provider: 'email', + email: 'lapse@example.com', + code: '123456', + supportsRefresh: true, + }) + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + token: 'short-jwt', + refreshToken: 'refresh-xyz', + expiresIn: 3600, + }); + + // One account, one session, no legacy token. + expect(mockCreateOrUpdateUser).toHaveBeenCalledTimes(1); + expect(mockCreateDeviceSession).toHaveBeenCalledTimes(1); + expect(mockIssueSessionCredentials).toHaveBeenCalledTimes(1); + expect(mockConsumeSignInCode).toHaveBeenCalledWith('lapse@example.com', '123456', undefined); + expect(mockCaptureMessage).toHaveBeenCalledWith('native_token_code_reservation_lapsed'); + expect(mockGenerateApiToken).not.toHaveBeenCalled(); + expect(mockReleaseSignInCode).not.toHaveBeenCalled(); + }); + + it('concurrent submissions produce exactly one settlement and one credential', async () => { + // First call gets the reservation; second gets IN_PROGRESS. + mockReserveSignInCode.mockResolvedValueOnce('ok').mockResolvedValueOnce('in_progress'); + mockCommitSignInCode.mockResolvedValue(true); + + const req = (code: string) => + createRequest({ + provider: 'email', + email: 'concurrent@example.com', + code, + }); + + const [resA, resB] = await Promise.all([POST(req('654321')), POST(req('654321'))]); + + const [statusA, statusB] = [resA.status, resB.status].toSorted(); + expect([statusA, statusB]).toEqual([200, 425]); + + // Only one createOrUpdateUser and one credential. + expect(mockCreateOrUpdateUser).toHaveBeenCalledTimes(1); + expect(mockGenerateApiToken).toHaveBeenCalledTimes(1); + }); }); it('returns 403 with the AuthErrorType when createOrUpdateUser fails', async () => { @@ -467,6 +929,53 @@ describe('POST /api/auth/native/token', () => { expect(mockGenerateApiToken).not.toHaveBeenCalled(); }); + it('refuses eligibility before persisting a device session, refresh token, or attested key (apple/google)', async () => { + mockVerifyNativeGoogleIdToken.mockResolvedValue({ + sub: 'google-sub-1', + email: 'googleuser@example.com', + }); + mockCreateOrUpdateUser.mockResolvedValue({ + success: true, + user: { ...fakeUser, google_user_email: 'user@sso-required.com' }, + isNew: false, + } as never); + mockCheckDomainSignInEligibility + .mockResolvedValueOnce({ ok: true, existingUser: false }) + .mockResolvedValueOnce({ + ok: false, + status: 403, + errorCode: 'SSO_ERROR', + ssoOrganizationId: 'workos-organization-id', + }); + mockVerifyAdmissionAsync.mockResolvedValue({ + ok: true, + platform: 'ios', + keyId: 'key1', + publicKey: 'base64pubkey', + }); + + const response = await POST( + createRequest({ + provider: 'google', + idToken: 'google-id-token', + supportsRefresh: true, + admission: {}, + }) + ); + + expect(response.status).toBe(403); + expect(await response.json()).toEqual({ + error: 'SSO_ERROR', + ssoOrganizationId: 'workos-organization-id', + }); + // No credential-bearing side effects may run before the refusal. + expect(mockCreateDeviceSessionWithAttestedKey).not.toHaveBeenCalled(); + expect(mockPersistAttestedKey).not.toHaveBeenCalled(); + expect(mockCreateDeviceSession).not.toHaveBeenCalled(); + expect(mockIssueSessionCredentials).not.toHaveBeenCalled(); + expect(mockGenerateApiToken).not.toHaveBeenCalled(); + }); + it('checks a linked provider account primary email before user sync', async () => { mockVerifyNativeGoogleIdToken.mockResolvedValue({ sub: 'google-sub-1', @@ -512,4 +1021,605 @@ describe('POST /api/auth/native/token', () => { expect(response.status).toBe(400); expect(await response.json()).toEqual({ error: 'INVALID_REQUEST' }); }); + + describe('admission', () => { + it('returns 403 ADMISSION_REQUIRED when admission check fails', async () => { + mockCheckNativeAdmission.mockReturnValue({ + admission: { + ok: false, + errorCode: 'ADMISSION_REQUIRED', + }, + verifyAsync: false, + }); + + const response = await POST( + createRequest({ provider: 'google', idToken: 'google-id-token' }) + ); + expect(response.status).toBe(403); + expect(await response.json()).toEqual({ error: 'ADMISSION_REQUIRED' }); + // Must not proceed to provider verification. + expect(mockVerifyNativeGoogleIdToken).not.toHaveBeenCalled(); + expect(mockCreateOrUpdateUser).not.toHaveBeenCalled(); + }); + + it('runs admission check before provider verification', async () => { + mockVerifyNativeGoogleIdToken.mockResolvedValue({ + sub: 'google-sub-1', + email: 'googleuser@example.com', + }); + + const response = await POST( + createRequest({ provider: 'google', idToken: 'google-id-token' }) + ); + expect(response.status).toBe(200); + expect(mockCheckNativeAdmission).toHaveBeenCalled(); + expect(mockVerifyNativeGoogleIdToken).toHaveBeenCalled(); + }); + + // ── Fix 3: ownership check before code commit (email path) ───────── + + it('refuses ownership mismatch WITHOUT consuming the sign-in code (enforce, email path)', async () => { + mockShouldRefuseAsyncFailure.mockReturnValue(true); + mockValidateAdmissionPayload.mockReturnValue({ + platform: 'ios', + kind: 'assertion', + challenge: 'ch123', + payload: 'data', + keyId: 'key1', + }); + mockVerifyAdmissionAsync.mockResolvedValue({ + ok: true, + platform: 'ios', + keyId: 'key1', + signCount: 11, + existingKeyUserId: 'other-user', + }); + + const response = await POST( + createRequest({ + provider: 'email', + email: 'emailuser@example.com', + code: '123456', + admission: {}, + }) + ); + + expect(response.status).toBe(403); + expect(await response.json()).toEqual({ error: 'ADMISSION_REQUIRED' }); + // Code must NOT be committed — ownership check happens before commit. + expect(mockCommitSignInCode).not.toHaveBeenCalled(); + expect(mockReleaseSignInCode).toHaveBeenCalled(); + }); + + // ── Fix 1: KeyCollisionError is not swallowed ───────────────────── + + // ── C14 repair: report mode logs mismatch and issues credentials ─ + + it('logs ownership mismatch and issues credentials in report mode (email path)', async () => { + // shouldRefuseAsyncFailure defaults to false (report mode) + mockValidateAdmissionPayload.mockReturnValue({ + platform: 'ios', + kind: 'assertion', + challenge: 'ch123', + payload: 'data', + keyId: 'key1', + }); + mockVerifyAdmissionAsync.mockResolvedValue({ + ok: true, + platform: 'ios', + keyId: 'key1', + signCount: 11, + existingKeyUserId: 'other-user', + }); + + const response = await POST( + createRequest({ + provider: 'email', + email: 'emailuser@example.com', + code: '123456', + admission: {}, + }) + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ token: 'minted-jwt' }); + // Ownership mismatch is logged before code commit. + expect(mockCaptureMessage).toHaveBeenCalledWith('native_attested_key_ownership_mismatch'); + // Code is committed and credentials are issued. + expect(mockCommitSignInCode).toHaveBeenCalled(); + expect(mockReleaseSignInCode).not.toHaveBeenCalled(); + // Key persistence is skipped — mismatch was detected pre-commit. + expect(mockPersistAttestedKey).not.toHaveBeenCalled(); + }); + + it('refuses attestation key collision before code commit (enforce, email path)', async () => { + mockShouldRefuseAsyncFailure.mockReturnValue(true); + mockValidateAdmissionPayload.mockReturnValue({ + platform: 'ios', + kind: 'attestation', + challenge: 'ch123', + payload: 'data', + keyId: 'key1', + }); + mockVerifyAdmissionAsync.mockResolvedValue({ + ok: true, + platform: 'ios', + keyId: 'key1', + publicKey: 'base64pubkey', + existingKeyUserId: 'other-user', + }); + + const response = await POST( + createRequest({ + provider: 'email', + email: 'emailuser@example.com', + code: '123456', + admission: {}, + }) + ); + + expect(response.status).toBe(403); + expect(await response.json()).toEqual({ error: 'ADMISSION_REQUIRED' }); + // Code must NOT be committed — attestation ownership is checked pre-commit. + expect(mockCommitSignInCode).not.toHaveBeenCalled(); + expect(mockReleaseSignInCode).toHaveBeenCalled(); + }); + + // ── Fix 3: KeyCollisionError during persistence does not burn the code ─ + + it('refuses key collision during persistence without burning the sign-in code (enforce, email, supportsRefresh)', async () => { + mockShouldRefuseAsyncFailure.mockReturnValue(true); + mockValidateAdmissionPayload.mockReturnValue({ + platform: 'ios', + kind: 'attestation', + challenge: 'ch123', + payload: 'data', + keyId: 'key1', + }); + // Verification succeeds with no prior owner — passes preflight ownership check. + mockVerifyAdmissionAsync.mockResolvedValue({ + ok: true, + platform: 'ios', + keyId: 'key1', + publicKey: 'base64pubkey', + // No existingKeyUserId — preflight check passes. + }); + // But the transactional persistence throws KeyCollisionError (concurrent insert). + mockCreateDeviceSessionWithAttestedKey.mockRejectedValue(new KeyCollisionError()); + + const response = await POST( + createRequest({ + provider: 'email', + email: 'emailuser@example.com', + code: '123456', + supportsRefresh: true, + admission: {}, + }) + ); + + expect(response.status).toBe(403); + expect(await response.json()).toEqual({ error: 'ADMISSION_REQUIRED' }); + // Code must NOT be committed — persistence failed BEFORE commit. + expect(mockCommitSignInCode).not.toHaveBeenCalled(); + // Code must be released so it remains usable. + expect(mockReleaseSignInCode).toHaveBeenCalled(); + expect(mockCaptureMessage).toHaveBeenCalledWith('native_attested_key_cross_user_collision'); + }); + + it('refuses key collision during persistence without burning the sign-in code (enforce, email, no supportsRefresh)', async () => { + mockShouldRefuseAsyncFailure.mockReturnValue(true); + mockValidateAdmissionPayload.mockReturnValue({ + platform: 'ios', + kind: 'attestation', + challenge: 'ch123', + payload: 'data', + keyId: 'key1', + }); + mockVerifyAdmissionAsync.mockResolvedValue({ + ok: true, + platform: 'ios', + keyId: 'key1', + publicKey: 'base64pubkey', + }); + mockPersistAttestedKey.mockRejectedValue(new KeyCollisionError()); + + const response = await POST( + createRequest({ + provider: 'email', + email: 'emailuser@example.com', + code: '123456', + supportsRefresh: false, + admission: {}, + }) + ); + + expect(response.status).toBe(403); + expect(await response.json()).toEqual({ error: 'ADMISSION_REQUIRED' }); + expect(mockCommitSignInCode).not.toHaveBeenCalled(); + expect(mockReleaseSignInCode).toHaveBeenCalled(); + expect(mockCaptureMessage).toHaveBeenCalledWith('native_attested_key_cross_user_collision'); + }); + + it('logs ownership mismatch and issues token in report mode (apple/google)', async () => { + // shouldRefuseAsyncFailure defaults to false (report mode) + mockVerifyNativeAppleIdToken.mockResolvedValue({ + sub: 'apple-sub-1', + email: 'appleuser@example.com', + }); + mockValidateAdmissionPayload.mockReturnValue({ + platform: 'ios', + kind: 'assertion', + challenge: 'ch123', + payload: 'data', + keyId: 'key1', + }); + mockVerifyAdmissionAsync.mockResolvedValue({ + ok: true, + platform: 'ios', + keyId: 'key1', + signCount: 11, + existingKeyUserId: 'other-user', + }); + + const response = await POST( + createRequest({ + provider: 'apple', + idToken: 'apple-id-token', + admission: {}, + }) + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ token: 'minted-jwt' }); + expect(mockCaptureMessage).toHaveBeenCalledWith('native_attested_key_ownership_mismatch'); + // Key persistence is skipped. + expect(mockPersistAttestedKey).not.toHaveBeenCalled(); + }); + + it('logs KeyCollisionError and issues token in report mode (apple/google, no supportsRefresh)', async () => { + // shouldRefuseAsyncFailure defaults to false (report mode) + mockVerifyNativeAppleIdToken.mockResolvedValue({ + sub: 'apple-sub-1', + email: 'appleuser@example.com', + }); + mockValidateAdmissionPayload.mockReturnValue({ + platform: 'ios', + kind: 'attestation', + challenge: 'ch123', + payload: 'data', + keyId: 'key1', + }); + mockVerifyAdmissionAsync.mockResolvedValue({ + ok: true, + platform: 'ios', + keyId: 'key1', + publicKey: 'base64pubkey', + }); + mockPersistAttestedKey.mockRejectedValue(new KeyCollisionError()); + + const response = await POST( + createRequest({ + provider: 'apple', + idToken: 'apple-id-token', + admission: {}, + }) + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ token: 'minted-jwt' }); + expect(mockCaptureMessage).toHaveBeenCalledWith('native_attested_key_cross_user_collision'); + }); + + // ── Fix 1: KeyCollisionError is not swallowed ───────────────────── + + it('returns 403 ADMISSION_REQUIRED when persistAttestedKey throws KeyCollisionError (enforce, apple/google, no supportsRefresh)', async () => { + mockShouldRefuseAsyncFailure.mockReturnValue(true); + mockVerifyNativeAppleIdToken.mockResolvedValue({ + sub: 'apple-sub-1', + email: 'appleuser@example.com', + }); + mockValidateAdmissionPayload.mockReturnValue({ + platform: 'ios', + kind: 'attestation', + challenge: 'ch123', + payload: 'data', + keyId: 'key1', + }); + mockVerifyAdmissionAsync.mockResolvedValue({ + ok: true, + platform: 'ios', + keyId: 'key1', + publicKey: 'base64pubkey', + }); + mockPersistAttestedKey.mockRejectedValue(new KeyCollisionError()); + + const response = await POST( + createRequest({ + provider: 'apple', + idToken: 'apple-id-token', + admission: {}, + }) + ); + + expect(response.status).toBe(403); + expect(await response.json()).toEqual({ error: 'ADMISSION_REQUIRED' }); + // Must not issue credentials after collision. + expect(mockGenerateApiToken).not.toHaveBeenCalled(); + }); + + it('returns 403 ADMISSION_REQUIRED when createDeviceSessionWithAttestedKey throws KeyCollisionError (enforce, apple/google, supportsRefresh)', async () => { + mockShouldRefuseAsyncFailure.mockReturnValue(true); + mockVerifyNativeAppleIdToken.mockResolvedValue({ + sub: 'apple-sub-1', + email: 'appleuser@example.com', + }); + mockValidateAdmissionPayload.mockReturnValue({ + platform: 'ios', + kind: 'attestation', + challenge: 'ch123', + payload: 'data', + keyId: 'key1', + }); + mockVerifyAdmissionAsync.mockResolvedValue({ + ok: true, + platform: 'ios', + keyId: 'key1', + publicKey: 'base64pubkey', + }); + mockCreateDeviceSessionWithAttestedKey.mockRejectedValue(new KeyCollisionError()); + + const response = await POST( + createRequest({ + provider: 'apple', + idToken: 'apple-id-token', + supportsRefresh: true, + admission: {}, + }) + ); + + expect(response.status).toBe(403); + expect(await response.json()).toEqual({ error: 'ADMISSION_REQUIRED' }); + expect(mockGenerateApiToken).not.toHaveBeenCalled(); + }); + + // ── Fix 6: attestation key + session in one transaction ──────────── + + it('uses createDeviceSessionWithAttestedKey when attestation and supportsRefresh', async () => { + mockVerifyNativeAppleIdToken.mockResolvedValue({ + sub: 'apple-sub-1', + email: 'appleuser@example.com', + }); + mockValidateAdmissionPayload.mockReturnValue({ + platform: 'ios', + kind: 'attestation', + challenge: 'ch123', + payload: 'data', + keyId: 'key1', + }); + mockVerifyAdmissionAsync.mockResolvedValue({ + ok: true, + platform: 'ios', + keyId: 'key1', + publicKey: 'base64pubkey', + }); + mockCreateDeviceSessionWithAttestedKey.mockResolvedValue({ + token: 'short-jwt', + refreshToken: 'refresh-abc', + expiresIn: 3600, + sessionId: 'session-1', + }); + + const response = await POST( + createRequest({ + provider: 'apple', + idToken: 'apple-id-token', + supportsRefresh: true, + admission: {}, + }) + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + token: 'short-jwt', + refreshToken: 'refresh-abc', + expiresIn: 3600, + }); + // Must use the transactional combined function, not separate calls. + expect(mockCreateDeviceSessionWithAttestedKey).toHaveBeenCalledWith({ + userId: fakeUser.id, + userAgent: undefined, + user: fakeUser, + verification: expect.objectContaining({ + platform: 'ios', + keyId: 'key1', + publicKey: 'base64pubkey', + }), + }); + expect(mockCreateDeviceSession).not.toHaveBeenCalled(); + expect(mockIssueSessionCredentials).not.toHaveBeenCalled(); + expect(mockPersistAttestedKey).not.toHaveBeenCalled(); + }); + }); + + describe('supportsRefresh', () => { + it('returns short-lived pair when supportsRefresh is true', async () => { + mockVerifyNativeGoogleIdToken.mockResolvedValue({ + sub: 'google-sub-1', + email: 'googleuser@example.com', + }); + mockCreateDeviceSession.mockResolvedValue('session-1'); + mockIssueSessionCredentials.mockResolvedValue({ + token: 'short-jwt', + refreshToken: 'refresh-abc', + expiresIn: 3600, + }); + + const response = await POST( + createRequest({ + provider: 'google', + idToken: 'google-id-token', + supportsRefresh: true, + }) + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + token: 'short-jwt', + refreshToken: 'refresh-abc', + expiresIn: 3600, + }); + expect(mockCreateDeviceSession).toHaveBeenCalledWith({ + userId: fakeUser.id, + userAgent: undefined, // NextRequest has no user-agent header by default + }); + expect(mockIssueSessionCredentials).toHaveBeenCalledWith(fakeUser, 'session-1'); + expect(mockGenerateApiToken).not.toHaveBeenCalled(); + // idToken path records legacy use even with supportsRefresh. + expect(mockCaptureMessage).toHaveBeenCalledWith('native_google_idtoken_legacy_count: 1'); + }); + + it('returns long-lived token when supportsRefresh is absent', async () => { + mockVerifyNativeGoogleIdToken.mockResolvedValue({ + sub: 'google-sub-1', + email: 'googleuser@example.com', + }); + + const response = await POST( + createRequest({ provider: 'google', idToken: 'google-id-token' }) + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ token: 'minted-jwt' }); + expect(mockGenerateApiToken).toHaveBeenCalledWith(fakeUser); + expect(mockCreateDeviceSession).not.toHaveBeenCalled(); + expect(mockIssueSessionCredentials).not.toHaveBeenCalled(); + expect(mockCaptureMessage).toHaveBeenCalledWith('native_token_legacy_long_lived_count: 1'); + }); + + it('returns long-lived token when supportsRefresh is false', async () => { + mockVerifyNativeGoogleIdToken.mockResolvedValue({ + sub: 'google-sub-1', + email: 'googleuser@example.com', + }); + + const response = await POST( + createRequest({ + provider: 'google', + idToken: 'google-id-token', + supportsRefresh: false, + }) + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ token: 'minted-jwt' }); + expect(mockCreateDeviceSession).not.toHaveBeenCalled(); + expect(mockCaptureMessage).toHaveBeenCalledWith('native_token_legacy_long_lived_count: 1'); + }); + }); + + describe('deferred sign-in analytics', () => { + const deferredEvent = { + distinctId: 'googleuser@example.com', + event: 'user_signed_in', + properties: { name: 'Google User', id: 'user-1' }, + }; + + beforeEach(() => { + mockPosthogCapture.mockClear(); + }); + + it('does not emit deferred sign-in event when user is blocked at post-settlement gate', async () => { + mockVerifyNativeGoogleIdToken.mockResolvedValue({ + sub: 'google-sub-1', + email: 'googleuser@example.com', + }); + mockCreateOrUpdateUser.mockResolvedValue({ + success: true, + user: { ...fakeUser, blocked_reason: 'manual block' }, + isNew: false, + deferredSignInEvent: deferredEvent, + } as never); + + const response = await POST( + createRequest({ provider: 'google', idToken: 'google-id-token' }) + ); + + expect(response.status).toBe(403); + expect(await response.json()).toEqual({ error: 'BLOCKED' }); + expect(mockPosthogCapture).not.toHaveBeenCalled(); + }); + + it('does not emit deferred sign-in event when SSO is required', async () => { + mockVerifyNativeGoogleIdToken.mockResolvedValue({ + sub: 'google-sub-1', + email: 'googleuser@example.com', + }); + mockCreateOrUpdateUser.mockResolvedValue({ + success: true, + user: { ...fakeUser, google_user_email: 'user@sso-required.com' }, + isNew: false, + deferredSignInEvent: deferredEvent, + } as never); + mockCheckDomainSignInEligibility + .mockResolvedValueOnce({ ok: true, existingUser: false }) + .mockResolvedValueOnce({ + ok: false, + status: 403, + errorCode: 'SSO_ERROR', + ssoOrganizationId: 'workos-organization-id', + }); + + const response = await POST( + createRequest({ provider: 'google', idToken: 'google-id-token' }) + ); + + expect(response.status).toBe(403); + expect(await response.json()).toEqual({ + error: 'SSO_ERROR', + ssoOrganizationId: 'workos-organization-id', + }); + expect(mockPosthogCapture).not.toHaveBeenCalled(); + }); + + it('emits deferred sign-in event after all gates pass for successful native sign-in', async () => { + mockVerifyNativeGoogleIdToken.mockResolvedValue({ + sub: 'google-sub-1', + email: 'googleuser@example.com', + }); + mockCreateOrUpdateUser.mockResolvedValue({ + success: true, + user: fakeUser, + isNew: false, + deferredSignInEvent: deferredEvent, + } as never); + + const response = await POST( + createRequest({ provider: 'google', idToken: 'google-id-token' }) + ); + + expect(response.status).toBe(200); + expect(mockPosthogCapture).toHaveBeenCalledWith(deferredEvent); + expect(mockPosthogCapture).toHaveBeenCalledTimes(1); + }); + + it('does not emit when createOrUpdateUser returns success without deferred event (new user sign-up)', async () => { + mockVerifyNativeGoogleIdToken.mockResolvedValue({ + sub: 'google-sub-1', + email: 'googleuser@example.com', + }); + mockCreateOrUpdateUser.mockResolvedValue({ + success: true, + user: fakeUser, + isNew: true, + // no deferredSignInEvent — new user sign-up, not a sign-in + } as never); + + const response = await POST( + createRequest({ provider: 'google', idToken: 'google-id-token' }) + ); + + expect(response.status).toBe(200); + expect(mockPosthogCapture).not.toHaveBeenCalled(); + }); + }); }); diff --git a/apps/web/src/app/api/auth/native/token/route.ts b/apps/web/src/app/api/auth/native/token/route.ts index 8280b17634..b1a6d671ac 100644 --- a/apps/web/src/app/api/auth/native/token/route.ts +++ b/apps/web/src/app/api/auth/native/token/route.ts @@ -4,10 +4,16 @@ import * as z from 'zod'; import { verifyNativeAppleIdToken, verifyNativeGoogleIdToken, + exchangeNativeGoogleAuthCode, NativeIdTokenError, } from '@/lib/auth/native-id-tokens'; import { AppleJwtClientError } from '@/lib/auth/apple-jwks'; -import { verifyAndConsumeSignInCode } from '@/lib/auth/magic-link-tokens'; +import { + reserveSignInCode, + commitSignInCode, + releaseSignInCode, + consumeSignInCode, +} from '@/lib/auth/magic-link-tokens'; import { hosted_domain_specials } from '@/lib/auth/constants'; import { createOrUpdateUser, @@ -18,6 +24,25 @@ import { } from '@/lib/user'; import { generateApiToken } from '@/lib/tokens'; import { checkDomainSignInEligibility } from '@/lib/auth/email-signin-eligibility'; +import { + checkNativeAdmission, + validateAdmissionPayload, + verifyAdmissionAsync, + persistAttestedKey, + shouldRefuseAsyncFailure, + KeyCollisionError, + type AdmissionPayload, + type VerifyAdmissionOk, +} from '@/lib/auth/native-admission'; +import { + createDeviceSession, + issueSessionCredentials, + createDeviceSessionWithAttestedKey, +} from '@/lib/auth/device-sessions'; +import { captureMessage } from '@sentry/nextjs'; +import PostHogClient from '@/lib/posthog'; + +const posthogClient = PostHogClient(); // Bad/expired ID tokens are a 401; JWKS-fetch or network failures during verification are // server faults and must surface as 500, not be misreported as an invalid token. @@ -63,32 +88,50 @@ const requestSchema = z.discriminatedUnion('provider', [ provider: z.literal('apple'), idToken: z.string(), fullName: z.string().optional(), + nonce: z.string().optional(), + supportsRefresh: z.boolean().optional(), + admission: z.unknown().optional(), }), z.object({ provider: z.literal('google'), - idToken: z.string(), + idToken: z.string().optional(), + serverAuthCode: z.string().optional(), + googleClientId: z.string().optional(), + supportsRefresh: z.boolean().optional(), + admission: z.unknown().optional(), }), z.object({ provider: z.literal('email'), email: z.string().email(), code: z.string(), + challengeId: z.string().uuid().optional(), + supportsRefresh: z.boolean().optional(), + admission: z.unknown().optional(), }), ]); /** * Native (mobile) sign-in token exchange. Verifies an Apple/Google ID token or an - * email sign-in code, creates or updates the user, and mints the same API token - * shape as the device-auth poll endpoint. + * email sign-in code, creates or updates the user, and mints an API token. + * + * Verification order per plan: + * 1. Sync admission gate (checkNativeAdmission). + * 2. Provider identity verification. + * 3. Async admission verification (BEFORE user settlement). + * 4. User settlement (createOrUpdateUser). + * 5. Key persistence (after settlement, binds key to user id). * * Response contract (frozen — mobile client is built against it): - * 200 { token } - * 401 { error: 'INVALID_TOKEN' } — bad apple/google ID token - * 401 { error: 'INVALID_CODE' } — bad email sign-in code - * 429 { error: 'TOO_MANY_ATTEMPTS' } — email code attempt budget exhausted - * 403/503 { error: 'BLOCKED' | 'SSO_ERROR', ssoOrganizationId? } — apple/google domain - * blacklisted or SSO-enforced (checkDomainSignInEligibility) - * 403 { error: AuthErrorType } — createOrUpdateUser rejected the sign-in - * 400 — invalid request body + * 200 { token, refreshToken?, expiresIn? } + * 401 { error: 'INVALID_TOKEN' } + * 401 { error: 'INVALID_CODE' } + * 425 { error: 'CODE_IN_PROGRESS' } + * 429 { error: 'TOO_MANY_ATTEMPTS' } + * 403/503 { error: 'BLOCKED' | 'SSO_ERROR', ssoOrganizationId? } + * 403 { error: AuthErrorType } + * 403 { error: 'ADMISSION_REQUIRED' } + * 400 invalid request body + * 500 provider infrastructure error */ export async function POST(request: NextRequest) { const body = await request.json().catch(() => undefined); @@ -99,13 +142,28 @@ export async function POST(request: NextRequest) { } const data = validation.data; + + // ── Step 1: Sync admission gate ────────────────────────────────────────── + const admissionGate = checkNativeAdmission(body); + if (!admissionGate.admission.ok) { + return NextResponse.json({ error: admissionGate.admission.errorCode }, { status: 403 }); + } + + // ── Step 2: Extract and validate admission payload ─────────────────────── + // Only extract when async verification is needed (enforce or report mode). + let admissionPayload: AdmissionPayload | undefined; + if (admissionGate.verifyAsync && body['admission'] && typeof body['admission'] === 'object') { + admissionPayload = validateAdmissionPayload(body['admission']); + } + + // ── Step 3: Provider identity verification ─────────────────────────────── let args: CreateOrUpdateUserArgs; let autoLinkToExistingUser: boolean; if (data.provider === 'apple') { let verified; try { - verified = await verifyNativeAppleIdToken(data.idToken); + verified = await verifyNativeAppleIdToken(data.idToken, data.nonce); } catch (error) { if (!isInvalidNativeTokenError(error)) { throw error; @@ -135,7 +193,20 @@ export async function POST(request: NextRequest) { } else if (data.provider === 'google') { let verified; try { - verified = await verifyNativeGoogleIdToken(data.idToken); + if (data.serverAuthCode) { + const { GOOGLE_CLIENT_ID } = await import('@/lib/config.server'); + if (!data.googleClientId || data.googleClientId !== GOOGLE_CLIENT_ID) { + return NextResponse.json({ error: 'INVALID_REQUEST' }, { status: 400 }); + } + verified = await exchangeNativeGoogleAuthCode(data.serverAuthCode); + } else if (data.idToken) { + // ponytail: remove legacy idToken-only path after all shipped clients send + // serverAuthCode and the legacy counter has drained. + captureMessage('native_google_idtoken_legacy_count: 1'); + verified = await verifyNativeGoogleIdToken(data.idToken); + } else { + return NextResponse.json({ error: 'INVALID_REQUEST' }, { status: 400 }); + } } catch (error) { if (!isInvalidNativeTokenError(error)) { throw error; @@ -163,35 +234,248 @@ export async function POST(request: NextRequest) { }; autoLinkToExistingUser = false; } else { + // Email sign-in code path: reserve → settle → commit. const existingUser = await findUserByNormalizedEmail(data.email); const email = existingUser?.google_user_email ?? data.email.toLowerCase(); - const codeResult = await verifyAndConsumeSignInCode(data.email, data.code); - if (codeResult === 'invalid') { + + const reserveResult = await reserveSignInCode(data.email, data.code, data.challengeId); + if (reserveResult === 'invalid') { return NextResponse.json({ error: 'INVALID_CODE' }, { status: 401 }); } - if (codeResult === 'too_many_attempts') { + if (reserveResult === 'too_many_attempts') { return NextResponse.json({ error: 'TOO_MANY_ATTEMPTS' }, { status: 429 }); } + if (reserveResult === 'in_progress') { + return NextResponse.json({ error: 'CODE_IN_PROGRESS' }, { status: 425 }); + } - const eligibility = await checkDomainSignInEligibility(email); - if (!eligibility.ok) { - return eligibilityResponse(eligibility); + let phase: 'reserved' | 'release' | 'committed' = 'reserved'; + + try { + const eligibility = await checkDomainSignInEligibility(email); + if (!eligibility.ok) { + phase = 'release'; + return eligibilityResponse(eligibility); + } + + const emailDomain = email.split('@')[1]; + args = { + google_user_email: email, + google_user_name: email.split('@')[0], + google_user_image_url: '', + hosted_domain: emailDomain || hosted_domain_specials.email, + provider: 'email', + provider_account_id: email, + display_name: null, + }; + autoLinkToExistingUser = true; + + // ── Step 3b: Async admission verification BEFORE settlement ──────── + let admissionVerification: VerifyAdmissionOk | undefined; + if (admissionPayload) { + try { + const verified = await verifyAdmissionAsync(admissionPayload); + if (!verified.ok) { + // Under report mode, evaluate but still admit. + if (shouldRefuseAsyncFailure()) { + phase = 'release'; + return NextResponse.json({ error: verified.errorCode }, { status: 403 }); + } + } else { + admissionVerification = verified; + } + } catch { + // Provider infrastructure failure — surface as 5xx. + phase = 'release'; + return NextResponse.json({ error: 'INTERNAL_ERROR' }, { status: 500 }); + } + } + + // ── Step 4: User settlement ────────────────────────────────────── + const result = await createOrUpdateUser( + args, + undefined, + autoLinkToExistingUser, + request.headers, + undefined, + undefined, + true + ); + if (!result.success) { + phase = 'release'; + return NextResponse.json({ error: result.error }, { status: 403 }); + } + + if (result.user.blocked_reason) { + phase = 'release'; + return NextResponse.json({ error: 'BLOCKED' }, { status: 403 }); + } + + const resolvedEligibility = await checkDomainSignInEligibility(result.user.google_user_email); + if (!resolvedEligibility.ok) { + phase = 'release'; + return eligibilityResponse(resolvedEligibility); + } + + // ── Step 4.5: Key ownership check BEFORE code commit ───────────── + // For assertion (existing key) and attestation (keyId already bound to + // another user): enforce → refuse without consuming the code so a + // legitimate retry remains possible. Report → log, skip persistence, + // and issue credentials. + if (admissionVerification) { + const hasOwnershipMismatch = + admissionVerification.existingKeyUserId && + admissionVerification.existingKeyUserId !== result.user.id; + if (hasOwnershipMismatch) { + captureMessage('native_attested_key_ownership_mismatch'); + if (shouldRefuseAsyncFailure()) { + phase = 'release'; + return NextResponse.json({ error: 'ADMISSION_REQUIRED' }, { status: 403 }); + } + // Report mode: skip key persistence, admit, and issue credentials. + admissionVerification = undefined; + } + } + + // ── Step 5: Persist attested key after settlement ───────────────── + // Must run BEFORE code commit so a key collision under enforce does + // not burn the sign-in code without issuing a credential. + let sessionId: string | undefined; + let refreshCredentials: + | { token: string; refreshToken: string; expiresIn: number } + | undefined; + + if (admissionVerification && data.supportsRefresh) { + // Bind key persistence and session creation in one transaction. + try { + const combined = await createDeviceSessionWithAttestedKey({ + userId: result.user.id, + userAgent: request.headers.get('user-agent') ?? undefined, + user: result.user, + verification: admissionVerification, + }); + sessionId = combined.sessionId; + refreshCredentials = { + token: combined.token, + refreshToken: combined.refreshToken, + expiresIn: combined.expiresIn, + }; + } catch (err) { + if (err instanceof KeyCollisionError) { + captureMessage('native_attested_key_cross_user_collision'); + if (shouldRefuseAsyncFailure()) { + phase = 'release'; + return NextResponse.json({ error: 'ADMISSION_REQUIRED' }, { status: 403 }); + } + // Report mode: log, admit, and issue credentials without binding the key. + } else { + // Bookkeeping failure — log and fall through to legacy token. + captureMessage('native_attested_key_persist_failed_after_settlement'); + } + } + } else if (admissionVerification) { + try { + await persistAttestedKey(result.user.id, admissionVerification); + } catch (err) { + if (err instanceof KeyCollisionError) { + captureMessage('native_attested_key_cross_user_collision'); + if (shouldRefuseAsyncFailure()) { + phase = 'release'; + return NextResponse.json({ error: 'ADMISSION_REQUIRED' }, { status: 403 }); + } + // Report mode: log, admit, and issue credentials without binding the key. + } else { + captureMessage('native_attested_key_persist_failed_after_settlement'); + } + } + } + + // ── Step 6: Consume the sign-in code AFTER key persistence ───────── + // The code is only committed once all pre-credential gates pass, so + // a refusal never burns a code without issuing a credential. + const committed = await commitSignInCode(data.email, data.code, data.challengeId); + if (!committed) { + const consumed = await consumeSignInCode(data.email, data.code, data.challengeId); + if (!consumed) { + return NextResponse.json({ error: 'INVALID_CODE' }, { status: 401 }); + } + captureMessage('native_token_code_reservation_lapsed'); + } + phase = 'committed'; + + // Emit deferred sign-in analytics after all gates pass. + if (result.deferredSignInEvent) { + posthogClient.capture(result.deferredSignInEvent); + } + + if (refreshCredentials) { + return NextResponse.json( + { + token: refreshCredentials.token, + refreshToken: refreshCredentials.refreshToken, + expiresIn: refreshCredentials.expiresIn, + }, + { status: 200 } + ); + } + + if (data.supportsRefresh) { + const sid = + sessionId ?? + (await createDeviceSession({ + userId: result.user.id, + userAgent: request.headers.get('user-agent') ?? undefined, + })); + const pair = await issueSessionCredentials(result.user, sid); + return NextResponse.json( + { token: pair.token, refreshToken: pair.refreshToken, expiresIn: pair.expiresIn }, + { status: 200 } + ); + } + + captureMessage('native_token_legacy_long_lived_count: 1'); + const token = generateApiToken(result.user); + return NextResponse.json({ token }, { status: 200 }); + } catch (error) { + phase = 'release'; + throw error; + } finally { + if (phase === 'release') { + await releaseSignInCode(data.email, data.code, data.challengeId); + } } + } - const emailDomain = email.split('@')[1]; - args = { - google_user_email: email, - google_user_name: email.split('@')[0], - google_user_image_url: '', - hosted_domain: emailDomain || hosted_domain_specials.email, - provider: 'email', - provider_account_id: email, - display_name: null, - }; - autoLinkToExistingUser = true; + // Apple/Google path. + + // ── Step 3c: Async admission verification BEFORE settlement ────────────── + let admissionVerification: VerifyAdmissionOk | undefined; + if (admissionPayload) { + try { + const verified = await verifyAdmissionAsync(admissionPayload); + if (!verified.ok) { + if (shouldRefuseAsyncFailure()) { + return NextResponse.json({ error: verified.errorCode }, { status: 403 }); + } + } else { + admissionVerification = verified; + } + } catch { + // Provider infrastructure failure — surface as 5xx. + return NextResponse.json({ error: 'INTERNAL_ERROR' }, { status: 500 }); + } } - const result = await createOrUpdateUser(args, undefined, autoLinkToExistingUser, request.headers); + // ── Step 4: User settlement ────────────────────────────────────────────── + const result = await createOrUpdateUser( + args, + undefined, + autoLinkToExistingUser, + request.headers, + undefined, + undefined, + true + ); if (!result.success) { return NextResponse.json({ error: result.error }, { status: 403 }); } @@ -200,11 +484,109 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'BLOCKED' }, { status: 403 }); } + // ── Step 5: Final eligibility check BEFORE persisting credentials ──────── + // The resolved account email can differ from the ID-token email (provider + // account linking). Refuse before a device session, refresh token, or + // attested key is persisted so an ineligible account leaves no credentials. const resolvedEligibility = await checkDomainSignInEligibility(result.user.google_user_email); if (!resolvedEligibility.ok) { return eligibilityResponse(resolvedEligibility); } + // ── Step 6: Persist attested key after settlement ──────────────────────── + let sessionId: string | undefined; + let refreshCredentials: { token: string; refreshToken: string; expiresIn: number } | undefined; + + if (admissionVerification) { + // Cross-user ownership: enforce → refuse, report → log and skip persistence. + if ( + admissionVerification.existingKeyUserId && + admissionVerification.existingKeyUserId !== result.user.id + ) { + captureMessage('native_attested_key_ownership_mismatch'); + if (shouldRefuseAsyncFailure()) { + return NextResponse.json({ error: 'ADMISSION_REQUIRED' }, { status: 403 }); + } + // Report mode: skip key persistence, admit, and issue credentials. + admissionVerification = undefined; + } + + if (admissionVerification) { + if (data.supportsRefresh) { + // Bind key persistence and session creation in one transaction. + try { + const combined = await createDeviceSessionWithAttestedKey({ + userId: result.user.id, + userAgent: request.headers.get('user-agent') ?? undefined, + user: result.user, + verification: admissionVerification, + }); + sessionId = combined.sessionId; + refreshCredentials = { + token: combined.token, + refreshToken: combined.refreshToken, + expiresIn: combined.expiresIn, + }; + } catch (err) { + if (err instanceof KeyCollisionError) { + captureMessage('native_attested_key_cross_user_collision'); + if (shouldRefuseAsyncFailure()) { + return NextResponse.json({ error: 'ADMISSION_REQUIRED' }, { status: 403 }); + } + // Report mode: log, admit, and issue credentials without binding the key. + } else { + captureMessage('native_attested_key_persist_failed_after_settlement'); + } + } + } else { + try { + await persistAttestedKey(result.user.id, admissionVerification); + } catch (err) { + if (err instanceof KeyCollisionError) { + captureMessage('native_attested_key_cross_user_collision'); + if (shouldRefuseAsyncFailure()) { + return NextResponse.json({ error: 'ADMISSION_REQUIRED' }, { status: 403 }); + } + // Report mode: log, admit, and issue credentials without binding the key. + } else { + captureMessage('native_attested_key_persist_failed_after_settlement'); + } + } + } + } + } + + // Emit deferred sign-in analytics after all gates pass. + if (result.deferredSignInEvent) { + posthogClient.capture(result.deferredSignInEvent); + } + + if (refreshCredentials) { + return NextResponse.json( + { + token: refreshCredentials.token, + refreshToken: refreshCredentials.refreshToken, + expiresIn: refreshCredentials.expiresIn, + }, + { status: 200 } + ); + } + + if (data.supportsRefresh) { + const sid = + sessionId ?? + (await createDeviceSession({ + userId: result.user.id, + userAgent: request.headers.get('user-agent') ?? undefined, + })); + const pair = await issueSessionCredentials(result.user, sid); + return NextResponse.json( + { token: pair.token, refreshToken: pair.refreshToken, expiresIn: pair.expiresIn }, + { status: 200 } + ); + } + + captureMessage('native_token_legacy_long_lived_count: 1'); const token = generateApiToken(result.user); return NextResponse.json({ token }, { status: 200 }); } diff --git a/apps/web/src/app/api/cron/cleanup-device-auth/route.test.ts b/apps/web/src/app/api/cron/cleanup-device-auth/route.test.ts index 30949ae62c..36b6c0ecd1 100644 --- a/apps/web/src/app/api/cron/cleanup-device-auth/route.test.ts +++ b/apps/web/src/app/api/cron/cleanup-device-auth/route.test.ts @@ -8,11 +8,17 @@ jest.mock('@kilocode/worker-utils/scheduled-job-observability', () => ({ })); jest.mock('@/lib/device-auth/device-auth', () => ({ cleanupExpiredDeviceAuthRequests: jest.fn() })); +jest.mock('@/lib/auth/native-admission', () => ({ cleanupExpiredAdmissionChallenges: jest.fn() })); jest.mock('@/lib/kiloclaw/access-codes', () => ({ cleanupExpiredAccessCodes: jest.fn() })); +jest.mock('@/lib/integrations/github/install-state', () => ({ + cleanupExpiredInstallStates: jest.fn(), +})); jest.mock('@/lib/utils.server', () => ({ sentryLogger: jest.fn(() => jest.fn()) })); import { cleanupExpiredDeviceAuthRequests } from '@/lib/device-auth/device-auth'; +import { cleanupExpiredAdmissionChallenges } from '@/lib/auth/native-admission'; import { cleanupExpiredAccessCodes } from '@/lib/kiloclaw/access-codes'; +import { cleanupExpiredInstallStates } from '@/lib/integrations/github/install-state'; import { emitScheduledJobEvent } from '@kilocode/worker-utils/scheduled-job-observability'; import { GET } from './route'; @@ -21,9 +27,11 @@ const mockEmitScheduledJobEvent = jest.mocked(emitScheduledJobEvent); describe('GET /api/cron/cleanup-device-auth', () => { beforeEach(() => jest.clearAllMocks()); - it('emits one success event with both cleanup counts', async () => { + it('emits one success event with all cleanup counts', async () => { jest.mocked(cleanupExpiredDeviceAuthRequests).mockResolvedValue(2); + jest.mocked(cleanupExpiredAdmissionChallenges).mockResolvedValue(1); jest.mocked(cleanupExpiredAccessCodes).mockResolvedValue(3); + jest.mocked(cleanupExpiredInstallStates).mockResolvedValue(4); const response = await GET( new Request('http://localhost/api/cron/cleanup-device-auth', { @@ -35,7 +43,9 @@ describe('GET /api/cron/cleanup-device-auth', () => { expect(mockEmitScheduledJobEvent).toHaveBeenCalledWith({ outcome: 'succeeded', deleted_device_auth_request_count: 2, + deleted_admission_challenge_count: 1, deleted_access_code_count: 3, + deleted_install_state_count: 4, }); }); diff --git a/apps/web/src/app/api/cron/cleanup-device-auth/route.ts b/apps/web/src/app/api/cron/cleanup-device-auth/route.ts index 4a407f8b84..db024f5c62 100644 --- a/apps/web/src/app/api/cron/cleanup-device-auth/route.ts +++ b/apps/web/src/app/api/cron/cleanup-device-auth/route.ts @@ -6,7 +6,9 @@ import { emitScheduledJobEvent, } from '@kilocode/worker-utils/scheduled-job-observability'; import { cleanupExpiredDeviceAuthRequests } from '@/lib/device-auth/device-auth'; +import { cleanupExpiredAdmissionChallenges } from '@/lib/auth/native-admission'; import { cleanupExpiredAccessCodes } from '@/lib/kiloclaw/access-codes'; +import { cleanupExpiredInstallStates } from '@/lib/integrations/github/install-state'; import { sentryLogger } from '@/lib/utils.server'; const CRON_SECRET = process.env['CRON_SECRET']; @@ -41,19 +43,30 @@ export async function GET(request: Request) { const deletedCount = await cleanupExpiredDeviceAuthRequests(); sentryLogger('cron', 'info')(`Cleaned up ${deletedCount} expired device auth requests`); + const challengesDeleted = await cleanupExpiredAdmissionChallenges(); + sentryLogger('cron', 'info')(`Cleaned up ${challengesDeleted} expired admission challenges`); + const accessCodesDeleted = await cleanupExpiredAccessCodes(); sentryLogger('cron', 'info')(`Cleaned up ${accessCodesDeleted} expired access codes`); + + const installStatesDeleted = await cleanupExpiredInstallStates(); + sentryLogger('cron', 'info')(`Cleaned up ${installStatesDeleted} expired install states`); + emitScheduledJobEvent( buildScheduledJobSuccessEvent(run, { deleted_device_auth_request_count: deletedCount, + deleted_admission_challenge_count: challengesDeleted, deleted_access_code_count: accessCodesDeleted, + deleted_install_state_count: installStatesDeleted, }) ); return NextResponse.json({ success: true, deletedCount, + challengesDeleted, accessCodesDeleted, + installStatesDeleted, timestamp: new Date().toISOString(), }); } catch (error) { diff --git a/apps/web/src/app/api/device-auth/codes/[code]/route.test.ts b/apps/web/src/app/api/device-auth/codes/[code]/route.test.ts new file mode 100644 index 0000000000..1b372b29e7 --- /dev/null +++ b/apps/web/src/app/api/device-auth/codes/[code]/route.test.ts @@ -0,0 +1,219 @@ +process.env.NEXTAUTH_SECRET ||= 'test-nextauth-secret'; + +import { NextRequest } from 'next/server'; + +jest.mock('@/lib/device-auth/device-auth'); +jest.mock('@/lib/user/server'); +jest.mock('@/lib/device-auth/device-auth-viewer-token'); +jest.mock('@vercel/firewall'); +jest.mock('@sentry/nextjs'); + +import { pollDeviceAuthRequest, denyDeviceAuthRequest } from '@/lib/device-auth/device-auth'; +import { getUserFromAuth } from '@/lib/user/server'; +import { verifyDeviceAuthViewerToken } from '@/lib/device-auth/device-auth-viewer-token'; +import { checkRateLimit } from '@vercel/firewall'; +import * as Sentry from '@sentry/nextjs'; +import { GET, DELETE } from './route'; + +const mockPoll = jest.mocked(pollDeviceAuthRequest); +const mockDeny = jest.mocked(denyDeviceAuthRequest); +const mockGetUserFromAuth = jest.mocked(getUserFromAuth); +const mockVerifyToken = jest.mocked(verifyDeviceAuthViewerToken); +const mockCheckRateLimit = jest.mocked(checkRateLimit); + +const fakeUser = { id: 'user-1' } as never; + +describe('GET /api/device-auth/codes/[code] (legacy poll)', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('returns 202 for pending', async () => { + mockPoll.mockResolvedValue({ status: 'pending' }); + + const response = await GET(new NextRequest('http://localhost:3000'), { + params: Promise.resolve({ code: 'ABCD-EFGH' }), + }); + expect(response.status).toBe(202); + expect(await response.json()).toEqual({ status: 'pending' }); + }); + + test('returns 200 with token for approved', async () => { + mockPoll.mockResolvedValue({ + status: 'approved', + token: 'jwt', + userId: 'user-1', + userEmail: 'user@example.com', + }); + + const response = await GET(new NextRequest('http://localhost:3000'), { + params: Promise.resolve({ code: 'ABCD-EFGH' }), + }); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + status: 'approved', + token: 'jwt', + userId: 'user-1', + userEmail: 'user@example.com', + }); + }); + + test('returns 403 for denied', async () => { + mockPoll.mockResolvedValue({ status: 'denied' }); + + const response = await GET(new NextRequest('http://localhost:3000'), { + params: Promise.resolve({ code: 'ABCD-EFGH' }), + }); + expect(response.status).toBe(403); + }); + + test('returns 410 for expired', async () => { + mockPoll.mockResolvedValue({ status: 'expired' }); + + const response = await GET(new NextRequest('http://localhost:3000'), { + params: Promise.resolve({ code: 'ABCD-EFGH' }), + }); + expect(response.status).toBe(410); + }); + + test('returns 400 for missing code', async () => { + const response = await GET(new NextRequest('http://localhost:3000'), { + params: Promise.resolve({ code: '' }), + }); + expect(response.status).toBe(400); + }); + + test('legacy poll counts without user code in Sentry extras', async () => { + const sentrySpy = jest.spyOn(Sentry, 'captureMessage'); + mockPoll.mockResolvedValue({ status: 'pending' }); + + await GET(new NextRequest('http://localhost:3000'), { + params: Promise.resolve({ code: 'ABCD-EFGH' }), + }); + + expect(sentrySpy).toHaveBeenCalledWith('legacy-poll-device-auth-count: 1', { + level: 'info', + }); + // The user code must never appear in Sentry extras. + const call = sentrySpy.mock.calls[0]!; + expect(call[1] as Record).not.toHaveProperty('extra'); + sentrySpy.mockRestore(); + }); +}); + +describe('DELETE /api/device-auth/codes/[code] (deny with viewer token)', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetUserFromAuth.mockResolvedValue({ user: fakeUser, authFailedResponse: null }); + mockCheckRateLimit.mockResolvedValue({ rateLimited: false } as never); + }); + + function createRequest(code: string, headers?: Record) { + return new NextRequest(`http://localhost:3000/api/device-auth/codes/${code}`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json', ...headers }, + }); + } + + test('returns 200 on successful deny', async () => { + mockVerifyToken.mockReturnValue({ code: 'ABCD-EFGH', userId: 'user-1' }); + + const response = await DELETE( + createRequest('ABCD-EFGH', { 'x-device-auth-viewer-token': 'valid-token' }), + { params: Promise.resolve({ code: 'ABCD-EFGH' }) } + ); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ success: true }); + expect(mockDeny).toHaveBeenCalledWith('ABCD-EFGH'); + }); + + test('returns 403 without viewer token', async () => { + mockVerifyToken.mockReturnValue(null); + + const response = await DELETE(createRequest('ABCD-EFGH'), { + params: Promise.resolve({ code: 'ABCD-EFGH' }), + }); + expect(response.status).toBe(403); + expect(response.statusText).toBeDefined(); // invalid or expired + expect(mockDeny).not.toHaveBeenCalled(); + }); + + test('returns 403 when viewer token code does not match route code', async () => { + mockVerifyToken.mockReturnValue({ code: 'DIFFERENT', userId: 'user-1' }); + + const response = await DELETE( + createRequest('ABCD-EFGH', { 'x-device-auth-viewer-token': 'valid-token' }), + { params: Promise.resolve({ code: 'ABCD-EFGH' }) } + ); + expect(response.status).toBe(403); + expect(mockDeny).not.toHaveBeenCalled(); + }); + + test('returns 403 when viewer token userId does not match authenticated user', async () => { + mockVerifyToken.mockReturnValue({ code: 'ABCD-EFGH', userId: 'different-user' }); + + const response = await DELETE( + createRequest('ABCD-EFGH', { 'x-device-auth-viewer-token': 'valid-token' }), + { params: Promise.resolve({ code: 'ABCD-EFGH' }) } + ); + expect(response.status).toBe(403); + expect(mockDeny).not.toHaveBeenCalled(); + }); + + test('returns 401 when user is not authenticated', async () => { + mockGetUserFromAuth.mockResolvedValue({ + user: null, + authFailedResponse: new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 }), + } as never); + + const response = await DELETE( + createRequest('ABCD-EFGH', { 'x-device-auth-viewer-token': 'valid-token' }), + { params: Promise.resolve({ code: 'ABCD-EFGH' }) } + ); + expect(response.status).toBe(401); + }); + + test('returns 429 when rate limited', async () => { + mockVerifyToken.mockReturnValue({ code: 'ABCD-EFGH', userId: 'user-1' }); + mockCheckRateLimit.mockResolvedValue({ rateLimited: true } as never); + + const response = await DELETE( + createRequest('ABCD-EFGH', { 'x-device-auth-viewer-token': 'valid-token' }), + { params: Promise.resolve({ code: 'ABCD-EFGH' }) } + ); + expect(response.status).toBe(429); + expect(mockDeny).not.toHaveBeenCalled(); + }); + + test('returns 400 for missing code', async () => { + const response = await DELETE(createRequest(''), { params: Promise.resolve({ code: '' }) }); + expect(response.status).toBe(400); + }); + + test('returns 409 on second deny (controlled refusal, not 500)', async () => { + mockVerifyToken.mockReturnValue({ code: 'ABCD-EFGH', userId: 'user-1' }); + mockDeny.mockRejectedValue(new Error('Device authorization request is not pending')); + + const response = await DELETE( + createRequest('ABCD-EFGH', { 'x-device-auth-viewer-token': 'valid-token' }), + { params: Promise.resolve({ code: 'ABCD-EFGH' }) } + ); + expect(response.status).toBe(409); + expect(await response.json()).toEqual({ + error: 'Device authorization request can no longer be denied', + }); + expect(mockDeny).toHaveBeenCalledWith('ABCD-EFGH'); + }); + + test('returns 404 when deny targets a non-existent request', async () => { + mockVerifyToken.mockReturnValue({ code: 'ABCD-EFGH', userId: 'user-1' }); + mockDeny.mockRejectedValue(new Error('Device authorization request not found')); + + const response = await DELETE( + createRequest('ABCD-EFGH', { 'x-device-auth-viewer-token': 'valid-token' }), + { params: Promise.resolve({ code: 'ABCD-EFGH' }) } + ); + expect(response.status).toBe(404); + expect(await response.json()).toEqual({ error: 'Not found' }); + }); +}); diff --git a/apps/web/src/app/api/device-auth/codes/[code]/route.ts b/apps/web/src/app/api/device-auth/codes/[code]/route.ts index f10981e7be..adb8cb5003 100644 --- a/apps/web/src/app/api/device-auth/codes/[code]/route.ts +++ b/apps/web/src/app/api/device-auth/codes/[code]/route.ts @@ -1,11 +1,19 @@ import { NextResponse } from 'next/server'; import { pollDeviceAuthRequest, denyDeviceAuthRequest } from '@/lib/device-auth/device-auth'; import { getUserFromAuth } from '@/lib/user/server'; +import { verifyDeviceAuthViewerToken } from '@/lib/device-auth/device-auth-viewer-token'; +import { checkRateLimit } from '@vercel/firewall'; +import crypto from 'node:crypto'; +import * as Sentry from '@sentry/nextjs'; +import { NEXTAUTH_SECRET } from '@/lib/config.server'; type RouteContext = { params: Promise<{ code: string }>; }; +// ──────────────── legacy poll ──────────────── +// @ponytail: remove this GET after all shipped clients migrate to POST /api/device-auth/token + export async function GET(_request: Request, context: RouteContext) { const { code } = await context.params; @@ -13,6 +21,8 @@ export async function GET(_request: Request, context: RouteContext) { return NextResponse.json({ error: 'Code parameter is required' }, { status: 400 }); } + Sentry.captureMessage('legacy-poll-device-auth-count: 1', { level: 'info' }); + const result = await pollDeviceAuthRequest(code); // Return appropriate status codes based on the result @@ -42,21 +52,75 @@ export async function GET(_request: Request, context: RouteContext) { } } -export async function DELETE(_request: Request, context: RouteContext) { +// ──────────────── deny with viewer token ──────────────── + +const DEVICE_AUTH_DENY_RATE_LIMIT_ID = 'device-auth-deny'; + +function getDenyRateLimitKey(userId: string): string { + return crypto.createHmac('sha256', NEXTAUTH_SECRET).update(userId).digest('base64url'); +} + +export async function DELETE(request: Request, context: RouteContext) { + const { code } = await context.params; + + if (!code) { + return NextResponse.json({ error: 'Code parameter is required' }, { status: 400 }); + } + // Authenticate the user - const { authFailedResponse } = await getUserFromAuth({ adminOnly: false }); + const { user, authFailedResponse } = await getUserFromAuth({ adminOnly: false }); if (authFailedResponse) { return authFailedResponse; } - const { code } = await context.params; + // Rate limit deny attempts + const { rateLimited } = await checkRateLimit(DEVICE_AUTH_DENY_RATE_LIMIT_ID, { + request, + rateLimitKey: getDenyRateLimitKey(user.id), + }); - if (!code) { - return NextResponse.json({ error: 'Code parameter is required' }, { status: 400 }); + if (rateLimited) { + return NextResponse.json( + { error: 'Rate limit exceeded. Please try again later.' }, + { status: 429 } + ); + } + + // Verify viewer token + const viewerToken = request.headers.get('x-device-auth-viewer-token'); + const verified = verifyDeviceAuthViewerToken(viewerToken); + + if (!verified) { + return NextResponse.json({ error: 'Invalid or expired viewer token' }, { status: 403 }); } - await denyDeviceAuthRequest(code); + if (verified.code !== code) { + return NextResponse.json({ error: 'Viewer token code mismatch' }, { status: 403 }); + } + + if (verified.userId !== user.id) { + return NextResponse.json({ error: 'Viewer token user mismatch' }, { status: 403 }); + } + + try { + await denyDeviceAuthRequest(code); + } catch (error) { + if (error instanceof Error) { + if (error.message === 'Device authorization request not found') { + return NextResponse.json({ error: 'Not found' }, { status: 404 }); + } + if (error.message === 'Device authorization request is not pending') { + return NextResponse.json( + { error: 'Device authorization request can no longer be denied' }, + { status: 409 } + ); + } + } + const message = error instanceof Error ? error.message : String(error); + Sentry.captureException(error instanceof Error ? error : new Error(message)); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } return NextResponse.json({ success: true }); } diff --git a/apps/web/src/app/api/device-auth/codes/route.test.ts b/apps/web/src/app/api/device-auth/codes/route.test.ts new file mode 100644 index 0000000000..031ec6d1a9 --- /dev/null +++ b/apps/web/src/app/api/device-auth/codes/route.test.ts @@ -0,0 +1,106 @@ +import { NextRequest } from 'next/server'; +import { APP_URL } from '@/lib/constants'; + +jest.mock('next/headers', () => ({ + headers: jest.fn().mockResolvedValue(new Headers()), +})); + +jest.mock('@/lib/device-auth/device-auth', () => { + const actual = jest.requireActual('@/lib/device-auth/device-auth'); + return { + ...actual, + createDeviceAuthRequest: jest.fn(), + }; +}); + +import { + createDeviceAuthRequest, + DeviceAuthPendingLimitError, + DEVICE_AUTH_PENDING_LIMIT_MESSAGE, +} from '@/lib/device-auth/device-auth'; +import { POST } from './route'; + +const mockCreate = jest.mocked(createDeviceAuthRequest); + +describe('POST /api/device-auth/codes', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('returns code, user_code, device_code, verificationUrl, and expiresIn', async () => { + const expiresAt = new Date(Date.now() + 600_000); + mockCreate.mockResolvedValue({ + code: 'ABCD-EFGH', + userCode: 'ABCD-EFGH', + deviceCode: 'base64url-device-secret', + expiresAt, + }); + + const req = new NextRequest('http://localhost:3000/api/device-auth/codes', { + method: 'POST', + }); + + const response = await POST(req); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.code).toBe('ABCD-EFGH'); + expect(data.user_code).toBe('ABCD-EFGH'); + expect(data.device_code).toBe('base64url-device-secret'); + expect(data.verificationUrl).toContain('/device-auth'); + expect(data.verificationUrl).toContain('ABCD-EFGH'); + // The device secret must never appear in a URL. + expect(data.verificationUrl).not.toContain('base64url-device-secret'); + expect(data.expiresIn).toBeGreaterThan(0); + expect(data.expiresIn).toBeLessThanOrEqual(600); + }); + + test('verificationUrl uses the user code, never the device secret', async () => { + const expiresAt = new Date(Date.now() + 600_000); + mockCreate.mockResolvedValue({ + code: 'WXYZ-1234', + userCode: 'WXYZ-1234', + deviceCode: 'super-long-secret-that-should-not-leak', + expiresAt, + }); + + const req = new NextRequest('http://localhost:3000/api/device-auth/codes', { + method: 'POST', + }); + + const response = await POST(req); + const data = await response.json(); + + // The URL contains only the 9-char user code, never the 43+ char device secret. + expect(data.verificationUrl).toMatch(/code=WXYZ-1234/); + expect(data.verificationUrl).not.toContain('super-long-secret-that-should-not-leak'); + expect(data.verificationUrl).toEqual( + expect.stringContaining(`${APP_URL}/device-auth?code=WXYZ-1234`) + ); + }); + + test('returns 429 with non-empty message when pending limit is hit', async () => { + mockCreate.mockRejectedValue(new DeviceAuthPendingLimitError()); + + const req = new NextRequest('http://localhost:3000/api/device-auth/codes', { + method: 'POST', + }); + + const response = await POST(req); + const data = await response.json(); + + expect(response.status).toBe(429); + expect(data.error).toBe(DEVICE_AUTH_PENDING_LIMIT_MESSAGE); + expect(data.error.length).toBeGreaterThan(0); + }); + + test('re-throws unexpected errors to the global 500 handler', async () => { + mockCreate.mockRejectedValue(new Error('unexpected')); + + const req = new NextRequest('http://localhost:3000/api/device-auth/codes', { + method: 'POST', + }); + + await expect(POST(req)).rejects.toThrow('unexpected'); + }); +}); diff --git a/apps/web/src/app/api/device-auth/codes/route.ts b/apps/web/src/app/api/device-auth/codes/route.ts index da708e9780..cd8dde4bdf 100644 --- a/apps/web/src/app/api/device-auth/codes/route.ts +++ b/apps/web/src/app/api/device-auth/codes/route.ts @@ -1,5 +1,8 @@ import { NextResponse } from 'next/server'; -import { createDeviceAuthRequest } from '@/lib/device-auth/device-auth'; +import { + createDeviceAuthRequest, + DeviceAuthPendingLimitError, +} from '@/lib/device-auth/device-auth'; import { headers } from 'next/headers'; import { APP_URL } from '@/lib/constants'; import { @@ -12,18 +15,27 @@ export async function POST(request: Request) { const userAgent = headersList.get('user-agent') || undefined; const ipAddress = headersList.get('x-forwarded-for') || undefined; - const { code, expiresAt } = await createDeviceAuthRequest({ - userAgent, - ipAddress, - }); + try { + const { code, userCode, deviceCode, expiresAt } = await createDeviceAuthRequest({ + userAgent, + ipAddress, + }); - const verificationUrl = buildDeviceAuthVerificationUrl(APP_URL, code, { - app: getDeviceAuthAppModeFromRequestUrl(request.url), - }); + const verificationUrl = buildDeviceAuthVerificationUrl(APP_URL, userCode, { + app: getDeviceAuthAppModeFromRequestUrl(request.url), + }); - return NextResponse.json({ - code, - verificationUrl, - expiresIn: Math.floor((expiresAt.getTime() - Date.now()) / 1000), - }); + return NextResponse.json({ + code, + user_code: userCode, + device_code: deviceCode, + verificationUrl, + expiresIn: Math.floor((expiresAt.getTime() - Date.now()) / 1000), + }); + } catch (error) { + if (error instanceof DeviceAuthPendingLimitError) { + return NextResponse.json({ error: error.message }, { status: 429 }); + } + throw error; + } } diff --git a/apps/web/src/app/api/device-auth/token/route.test.ts b/apps/web/src/app/api/device-auth/token/route.test.ts new file mode 100644 index 0000000000..dd58f1681e --- /dev/null +++ b/apps/web/src/app/api/device-auth/token/route.test.ts @@ -0,0 +1,144 @@ +import { NextRequest } from 'next/server'; + +jest.mock('@/lib/device-auth/device-auth'); + +import { consumeDeviceAuthByDeviceCode } from '@/lib/device-auth/device-auth'; +import { POST } from './route'; + +const mockConsume = jest.mocked(consumeDeviceAuthByDeviceCode); + +describe('POST /api/device-auth/token', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const createRequest = (body: unknown) => + new NextRequest('http://localhost:3000/api/device-auth/token', { + method: 'POST', + body: JSON.stringify(body), + headers: { 'Content-Type': 'application/json' }, + }); + + test('returns 200 with token on approved', async () => { + mockConsume.mockResolvedValue({ + status: 'approved', + token: 'jwt-token', + userId: 'user-1', + userEmail: 'user@example.com', + }); + + const response = await POST(createRequest({ deviceCode: 'secret123' })); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data).toEqual({ + status: 'approved', + token: 'jwt-token', + userId: 'user-1', + userEmail: 'user@example.com', + }); + expect(mockConsume).toHaveBeenCalledWith('secret123', { supportsRefresh: undefined }); + }); + + test('passes supportsRefresh through to consume', async () => { + mockConsume.mockResolvedValue({ + status: 'approved', + token: 'jwt-token', + userId: 'user-1', + userEmail: 'user@example.com', + }); + + await POST(createRequest({ deviceCode: 'secret123', supportsRefresh: true })); + expect(mockConsume).toHaveBeenCalledWith('secret123', { supportsRefresh: true }); + }); + + test('returns refreshToken and expiresIn when consumer returns short pair', async () => { + mockConsume.mockResolvedValue({ + status: 'approved', + token: 'short-jwt', + refreshToken: 'refresh-abc', + expiresIn: 3600, + userId: 'user-1', + userEmail: 'user@example.com', + }); + + const response = await POST(createRequest({ deviceCode: 'secret123', supportsRefresh: true })); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data).toEqual({ + status: 'approved', + token: 'short-jwt', + refreshToken: 'refresh-abc', + expiresIn: 3600, + userId: 'user-1', + userEmail: 'user@example.com', + }); + }); + + test('returns 202 on pending', async () => { + mockConsume.mockResolvedValue({ status: 'pending' }); + + const response = await POST(createRequest({ deviceCode: 'secret123' })); + const data = await response.json(); + + expect(response.status).toBe(202); + expect(data).toEqual({ status: 'pending' }); + }); + + test('returns 403 on denied', async () => { + mockConsume.mockResolvedValue({ status: 'denied' }); + + const response = await POST(createRequest({ deviceCode: 'secret123' })); + const data = await response.json(); + + expect(response.status).toBe(403); + expect(data).toEqual({ status: 'denied' }); + }); + + test('returns 410 on expired', async () => { + mockConsume.mockResolvedValue({ status: 'expired' }); + + const response = await POST(createRequest({ deviceCode: 'secret123' })); + const data = await response.json(); + + expect(response.status).toBe(410); + expect(data).toEqual({ status: 'expired' }); + }); + + test('returns 410 on consumed', async () => { + mockConsume.mockResolvedValue({ status: 'consumed' }); + + const response = await POST(createRequest({ deviceCode: 'secret123' })); + const data = await response.json(); + + expect(response.status).toBe(410); + expect(data).toEqual({ status: 'expired' }); + }); + + test('returns 410 when user_code (display code) is used instead of device secret', async () => { + mockConsume.mockResolvedValue({ status: 'expired' }); + + const response = await POST(createRequest({ deviceCode: 'ABCD-EFGH' })); + const data = await response.json(); + + expect(response.status).toBe(410); + expect(data).toEqual({ status: 'expired' }); + expect(mockConsume).toHaveBeenCalledWith('ABCD-EFGH', { supportsRefresh: undefined }); + }); + + test('returns 400 for missing deviceCode', async () => { + const response = await POST(createRequest({})); + expect(response.status).toBe(400); + }); + + test('returns 400 for invalid JSON', async () => { + const req = new NextRequest('http://localhost:3000/api/device-auth/token', { + method: 'POST', + body: '{', + headers: { 'Content-Type': 'application/json' }, + }); + const response = await POST(req); + expect(response.status).toBe(400); + }); +}); diff --git a/apps/web/src/app/api/device-auth/token/route.ts b/apps/web/src/app/api/device-auth/token/route.ts new file mode 100644 index 0000000000..6d42f28c39 --- /dev/null +++ b/apps/web/src/app/api/device-auth/token/route.ts @@ -0,0 +1,57 @@ +import { NextResponse } from 'next/server'; +import { consumeDeviceAuthByDeviceCode } from '@/lib/device-auth/device-auth'; +import * as z from 'zod'; + +const TokenBodySchema = z.object({ + deviceCode: z.string().min(1), + supportsRefresh: z.boolean().optional(), +}); + +export async function POST(request: Request) { + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + const validation = TokenBodySchema.safeParse(body); + if (!validation.success) { + return NextResponse.json( + { error: 'Invalid request body', details: validation.error.issues }, + { status: 400 } + ); + } + + const { deviceCode, supportsRefresh } = validation.data; + + const result = await consumeDeviceAuthByDeviceCode(deviceCode, { supportsRefresh }); + + switch (result.status) { + case 'pending': + return NextResponse.json({ status: 'pending' }, { status: 202 }); + + case 'approved': + return NextResponse.json( + { + status: 'approved', + token: result.token, + ...(result.refreshToken ? { refreshToken: result.refreshToken } : {}), + ...(result.expiresIn ? { expiresIn: result.expiresIn } : {}), + userId: result.userId, + userEmail: result.userEmail, + }, + { status: 200 } + ); + + case 'denied': + return NextResponse.json({ status: 'denied' }, { status: 403 }); + + case 'expired': + case 'consumed': + return NextResponse.json({ status: 'expired' }, { status: 410 }); + + default: + return NextResponse.json({ error: 'Unknown status' }, { status: 500 }); + } +} diff --git a/apps/web/src/app/api/integrations/github/callback/route.test.ts b/apps/web/src/app/api/integrations/github/callback/route.test.ts index 40814658ca..5a5caaa3db 100644 --- a/apps/web/src/app/api/integrations/github/callback/route.test.ts +++ b/apps/web/src/app/api/integrations/github/callback/route.test.ts @@ -8,12 +8,17 @@ import { exchangeGitHubOAuthCode } from '@/lib/integrations/platforms/github/ada import { linkKiloUser } from '@/lib/bot-identity'; import { bot } from '@/lib/bot'; import { failureResult } from '@/lib/maybe-result'; +import { consumeInstallState } from '@/lib/integrations/github/install-state'; +import { getEnvVariable } from '@/lib/dotenvx'; import { findIntegrationByInstallationId, upsertPlatformIntegrationForOwner, } from '@/lib/integrations/db/platform-integrations'; import { isOrganizationMember } from '@/lib/organizations/organizations'; +import { assertUserAdministersInstallation } from '@/lib/integrations/platforms/github/app-selector'; +import { captureException, captureMessage } from '@sentry/nextjs'; import type { StateAdapter } from 'chat'; +import { parseStateReturn } from '@/lib/integrations/validate-return-path'; const mockState = { kind: 'state' } as unknown as StateAdapter; @@ -48,6 +53,7 @@ jest.mock('@/lib/integrations/platforms/github/app-selector', () => ({ appName: 'KiloConnect', webhookSecret: 'webhook-secret', })), + assertUserAdministersInstallation: jest.fn(async () => true), })); jest.mock('@/routers/organizations/utils', () => ({ ensureOrganizationAccess: jest.fn(), @@ -56,7 +62,7 @@ jest.mock('@/lib/integrations/db/platform-integrations', () => ({ createPendingIntegration: jest.fn(), findIntegrationByInstallationId: jest.fn(), findPendingInstallationByRequesterId: jest.fn(), - upsertPlatformIntegrationForOwner: jest.fn(), + upsertPlatformIntegrationForOwner: jest.fn(async () => ({ ok: true })), })); jest.mock('@/lib/organizations/organizations', () => ({ isOrganizationMember: jest.fn(), @@ -65,6 +71,25 @@ jest.mock('@sentry/nextjs', () => ({ captureException: jest.fn(), captureMessage: jest.fn(), })); +jest.mock('@/lib/integrations/github/install-state', () => ({ + consumeInstallState: jest.fn(), +})); +jest.mock('@/lib/dotenvx', () => ({ + getEnvVariable: jest.fn((key: string) => process.env[key] ?? ''), + requireEnv: (name: string, value: string | undefined) => { + if (!value) throw new Error(`Missing required environment variable ${name}`); + return value; + }, +})); +jest.mock('@/lib/integrations/validate-return-path', () => { + const actual = jest.requireActual('@/lib/integrations/validate-return-path'); + return { + ...actual, + // Wrapped so tests can force the invalid-owner branch while every other + // path keeps the real return-path parsing. + parseStateReturn: jest.fn(actual.parseStateReturn), + }; +}); const mockedGetUserFromAuth = jest.mocked(getUserFromAuth); const mockedVerifyGitHubBotLinkState = jest.mocked(verifyGitHubBotLinkState); @@ -76,6 +101,12 @@ const mockedCreateAppAuth = jest.mocked(createAppAuth); const mockedOctokit = jest.mocked(Octokit); const mockedUpsertPlatformIntegrationForOwner = jest.mocked(upsertPlatformIntegrationForOwner); const mockedIsOrganizationMember = jest.mocked(isOrganizationMember); +const mockedConsumeInstallState = jest.mocked(consumeInstallState); +const mockedGetEnvVariable = jest.mocked(getEnvVariable); +const mockedAssertUserAdministersInstallation = jest.mocked(assertUserAdministersInstallation); +const mockedCaptureException = jest.mocked(captureException); +const mockedCaptureMessage = jest.mocked(captureMessage); +const mockedParseStateReturn = jest.mocked(parseStateReturn); const USER_ID = '034489e8-19e0-4479-9d69-2edad719e847'; const OTHER_USER_ID = 'c00b91a1-6959-4b04-9ef8-e8d37b340f4a'; @@ -106,7 +137,11 @@ describe('GET /api/integrations/github/callback bot link flow', () => { installationId: INSTALLATION_ID, callbackPath: '/github/link', }); - mockedExchangeGitHubOAuthCode.mockResolvedValue({ id: GITHUB_USER_ID, login: 'octocat' }); + mockedExchangeGitHubOAuthCode.mockResolvedValue({ + id: GITHUB_USER_ID, + login: 'octocat', + accessToken: 'test-token', + }); mockedFindIntegrationByInstallationId.mockResolvedValue({ owned_by_organization_id: 'org_1', owned_by_user_id: null, @@ -270,3 +305,1051 @@ describe('GET /api/integrations/github/callback installation flow', () => { ); }); }); + +describe('GET /api/integrations/github/callback database-backed install flow', () => { + const DB_TOKEN = 'valid-database-token-' + Date.now(); + + beforeEach(() => { + jest.clearAllMocks(); + + mockedGetUserFromAuth.mockResolvedValue({ + user: { + id: USER_ID, + google_user_email: 'mobile-e2e@example.com', + google_user_name: 'Mobile E2E', + }, + authFailedResponse: null, + } as never); + mockedVerifyGitHubBotLinkState.mockReturnValue(null); + mockedGetEnvVariable.mockReturnValue('true'); + mockedCreateAppAuth.mockReturnValue( + jest.fn(async () => ({ token: 'github-app-token' })) as never + ); + mockedOctokit.mockImplementation( + () => + ({ + apps: { + getInstallation: jest.fn(async () => ({ + data: { + account: { id: 12_345, login: 'securexg' }, + created_at: '2026-07-09T19:00:00.000Z', + events: ['issues'], + permissions: { contents: 'write' }, + repository_selection: 'all', + }, + })), + listReposAccessibleToInstallation: jest.fn(), + }, + }) as never + ); + }); + + test('completes a callback using a valid database-minted token', async () => { + mockedConsumeInstallState.mockResolvedValue({ + token: DB_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'user', + owner_id: USER_ID, + github_app_type: 'standard', + return_to: '/github-app', + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${DB_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/github-app?github_install=success'); + // The token consumed by the callback must be bare — no return suffix. + expect(DB_TOKEN).not.toContain('|'); + expect(DB_TOKEN).not.toContain('return'); + expect(mockedConsumeInstallState).toHaveBeenCalledWith(DB_TOKEN); + expect(mockedUpsertPlatformIntegrationForOwner).toHaveBeenCalledWith( + { type: 'user', id: USER_ID }, + expect.objectContaining({ + platform: 'github', + integrationType: 'app', + platformInstallationId: INSTALLATION_ID, + }) + ); + }); + + test('consumes a bare token and rejects suffixed tokens (producer-shaped state)', async () => { + // Simulate the real DB: only the bare token returns a row. + // A suffixed token never matches the stored bare token. + const mockRow: Awaited> = { + token: DB_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'user', + owner_id: USER_ID, + github_app_type: 'standard', + return_to: '/github-app', + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }; + mockedConsumeInstallState.mockImplementation(async (token: string) => { + return token === DB_TOKEN ? mockRow : null; + }); + + const { GET } = await import('./route'); + + // If a producer erroneously sends a suffixed state, consumeInstallState + // receives the full suffix. The real DB returns null because it stores + // only the bare token. The callback must redirect to the home page + // instead of proceeding with the install. + const suffixed = `${DB_TOKEN}|return=${encodeURIComponent('/github-app')}`; + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${encodeURIComponent(suffixed)}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/'); + expect(mockedConsumeInstallState).toHaveBeenCalledWith(suffixed); + expect(mockedUpsertPlatformIntegrationForOwner).not.toHaveBeenCalled(); + }); + + test('rejects a token consumed by a different user (foreign state)', async () => { + mockedConsumeInstallState.mockResolvedValue({ + token: DB_TOKEN, + kilo_user_id: OTHER_USER_ID, + owner_type: 'org', + owner_id: 'org-123', + github_app_type: 'standard', + return_to: null, + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${DB_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/github-app?error=install_state_user_mismatch'); + expect(mockedConsumeInstallState).toHaveBeenCalledWith(DB_TOKEN); + expect(mockedUpsertPlatformIntegrationForOwner).not.toHaveBeenCalled(); + }); + + test('redirects with fromApp=1 when user mismatch on app-initiated state', async () => { + mockedConsumeInstallState.mockResolvedValue({ + token: DB_TOKEN, + kilo_user_id: OTHER_USER_ID, + owner_type: 'user', + owner_id: OTHER_USER_ID, + github_app_type: 'standard', + // App-initiated: return_to is /cloud/sessions (starts with /cloud/) + return_to: '/cloud/sessions', + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${DB_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/github-app?error=install_state_user_mismatch&fromApp=1'); + expect(mockedConsumeInstallState).toHaveBeenCalledWith(DB_TOKEN); + expect(mockedUpsertPlatformIntegrationForOwner).not.toHaveBeenCalled(); + }); + + test('rejects a replayed (already consumed) token', async () => { + mockedConsumeInstallState.mockResolvedValue(null); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${DB_TOKEN}` + ) as never + ); + + // Replayed tokens fall through to unrecognized state + expect(response.status).toBe(307); + expectRedirectLocation(response, '/'); + expect(mockedConsumeInstallState).toHaveBeenCalledWith(DB_TOKEN); + expect(mockedUpsertPlatformIntegrationForOwner).not.toHaveBeenCalled(); + }); + + test('consumes a DB-minted token before legacy prefix dispatch (unambiguous routing)', async () => { + // A token whose base64url shape begins with the legacy user_ prefix must + // still route through the database flow, never the legacy plaintext flow. + const PREFIXED_DB_TOKEN = `user_${DB_TOKEN}`; + mockedConsumeInstallState.mockResolvedValue({ + token: PREFIXED_DB_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'org', + owner_id: 'org-db-owner', + github_app_type: 'standard', + return_to: '/github-app', + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${PREFIXED_DB_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/github-app?github_install=success'); + expect(mockedConsumeInstallState).toHaveBeenCalledWith(PREFIXED_DB_TOKEN); + // The owner comes from the DB row (org), not from a legacy user_ parse. + expect(mockedUpsertPlatformIntegrationForOwner).toHaveBeenCalledWith( + { type: 'org', id: 'org-db-owner' }, + expect.objectContaining({ platform: 'github' }) + ); + }); + + test('handles a database-minted token with returnTo', async () => { + mockedConsumeInstallState.mockResolvedValue({ + token: DB_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'org', + owner_id: 'org-456', + github_app_type: 'lite', + return_to: '/organizations/org-456/integrations/github', + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + + // Override to return the matching org owner + jest.doMock('@/routers/organizations/utils', () => ({ + ensureOrganizationAccess: jest.fn(), + })); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${DB_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation( + response, + '/organizations/org-456/integrations/github?github_install=success' + ); + }); + + test('redirects to /github-app fallback (not /cloud/sessions) for app-initiated success', async () => { + mockedConsumeInstallState.mockResolvedValue({ + token: DB_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'user', + owner_id: USER_ID, + github_app_type: 'standard', + // App-initiated: return_to is /cloud/sessions — claimed UL route. + return_to: '/cloud/sessions', + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${DB_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + // Must redirect to /github-app fallback page, not /cloud/sessions. + expectRedirectLocation(response, '/github-app?fromApp=1&github_install=success'); + expect(mockedUpsertPlatformIntegrationForOwner).toHaveBeenCalled(); + }); + + test('redirects to /github-app fallback for app-initiated pending approval', async () => { + mockedConsumeInstallState.mockResolvedValue({ + token: DB_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'user', + owner_id: USER_ID, + github_app_type: 'standard', + return_to: '/cloud/sessions', + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + + const { createPendingIntegration } = + await import('@/lib/integrations/db/platform-integrations'); + const mockedCreatePending = jest.mocked(createPendingIntegration); + mockedCreatePending.mockResolvedValue(undefined as never); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=request&state=${DB_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/github-app?fromApp=1&github_pending_approval=true'); + expect(mockedUpsertPlatformIntegrationForOwner).not.toHaveBeenCalled(); + }); + + test('rejects non-/cloud/ returnTo as non-app (no fallback redirect)', async () => { + // A non-app returnTo must not trigger the /github-app fallback. + mockedConsumeInstallState.mockResolvedValue({ + token: DB_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'org', + owner_id: 'org-attacker', + github_app_type: 'standard', + return_to: '/organizations/org-attacker/integrations/github', + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + + jest.doMock('@/routers/organizations/utils', () => ({ + ensureOrganizationAccess: jest.fn(), + })); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${DB_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + // Non-app returnTo: redirect to integration path, not /github-app. + expectRedirectLocation( + response, + '/organizations/org-attacker/integrations/github?github_install=success' + ); + }); + + test('app-initiated installation_already_claimed redirects to /github-app fallback', async () => { + mockedConsumeInstallState.mockResolvedValue({ + token: DB_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'user', + owner_id: USER_ID, + github_app_type: 'standard', + return_to: '/cloud/sessions', + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + mockedUpsertPlatformIntegrationForOwner.mockResolvedValue({ + ok: false, + reason: 'claimed_by_other_owner', + }); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${DB_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/github-app?fromApp=1&error=installation_already_claimed'); + }); + + test('app-initiated missing_installation_id redirects to /github-app fallback', async () => { + mockedConsumeInstallState.mockResolvedValue({ + token: DB_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'user', + owner_id: USER_ID, + github_app_type: 'standard', + return_to: '/cloud/sessions', + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?setup_action=install&state=${DB_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/github-app?fromApp=1&error=missing_installation_id'); + expect(mockedUpsertPlatformIntegrationForOwner).not.toHaveBeenCalled(); + }); + + test('app-initiated installation_not_found redirects to /github-app fallback', async () => { + mockedConsumeInstallState.mockResolvedValue({ + token: DB_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'user', + owner_id: USER_ID, + github_app_type: 'standard', + return_to: '/cloud/sessions', + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + mockedOctokit.mockImplementation( + () => + ({ + apps: { + getInstallation: jest.fn(async () => { + const err = Object.assign(new Error('Not Found'), { status: 404 }); + throw err; + }), + listReposAccessibleToInstallation: jest.fn(), + }, + }) as never + ); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${DB_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/github-app?fromApp=1&error=installation_not_found'); + expect(mockedUpsertPlatformIntegrationForOwner).not.toHaveBeenCalled(); + }); + + test('app-initiated pending_setup_failed redirects to /github-app fallback', async () => { + mockedConsumeInstallState.mockResolvedValue({ + token: DB_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'user', + owner_id: USER_ID, + github_app_type: 'standard', + return_to: '/cloud/sessions', + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + + const { createPendingIntegration } = + await import('@/lib/integrations/db/platform-integrations'); + const mockedCreatePending = jest.mocked(createPendingIntegration); + mockedCreatePending.mockRejectedValue(new Error('DB error') as never); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=request&state=${DB_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/github-app?fromApp=1&error=pending_setup_failed'); + }); + + test('app-initiated org success preserves organizationId on redirect', async () => { + const ORG_ID = 'org-789'; + mockedUpsertPlatformIntegrationForOwner.mockResolvedValue({ ok: true }); + mockedConsumeInstallState.mockResolvedValue({ + token: DB_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'org', + owner_id: ORG_ID, + github_app_type: 'standard', + return_to: '/cloud/sessions', + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + + jest.doMock('@/routers/organizations/utils', () => ({ + ensureOrganizationAccess: jest.fn(), + })); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${DB_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation( + response, + `/github-app?fromApp=1&github_install=success&organizationId=${ORG_ID}` + ); + expect(mockedUpsertPlatformIntegrationForOwner).toHaveBeenCalled(); + }); + + test('app-initiated user success omits organizationId', async () => { + mockedUpsertPlatformIntegrationForOwner.mockResolvedValue({ ok: true }); + mockedConsumeInstallState.mockResolvedValue({ + token: DB_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'user', + owner_id: USER_ID, + github_app_type: 'standard', + return_to: '/cloud/sessions', + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${DB_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + // No organizationId for user-scoped install. + expectRedirectLocation(response, '/github-app?fromApp=1&github_install=success'); + }); + + test('app-initiated org pending approval preserves organizationId', async () => { + const ORG_ID = 'org-pending'; + mockedConsumeInstallState.mockResolvedValue({ + token: DB_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'org', + owner_id: ORG_ID, + github_app_type: 'standard', + return_to: '/cloud/sessions', + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + + const { createPendingIntegration } = + await import('@/lib/integrations/db/platform-integrations'); + const mockedCreatePending = jest.mocked(createPendingIntegration); + mockedCreatePending.mockResolvedValue(undefined as never); + + jest.doMock('@/routers/organizations/utils', () => ({ + ensureOrganizationAccess: jest.fn(), + })); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=request&state=${DB_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation( + response, + `/github-app?fromApp=1&github_pending_approval=true&organizationId=${ORG_ID}` + ); + }); +}); + +describe('GET /api/integrations/github/callback legacy flag gating', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockedUpsertPlatformIntegrationForOwner.mockResolvedValue({ ok: true }); + mockedGetUserFromAuth.mockResolvedValue({ + user: { + id: USER_ID, + google_user_email: 'mobile-e2e@example.com', + google_user_name: 'Mobile E2E', + }, + authFailedResponse: null, + } as never); + mockedVerifyGitHubBotLinkState.mockReturnValue(null); + mockedConsumeInstallState.mockResolvedValue(null); + mockedCreateAppAuth.mockReturnValue( + jest.fn(async () => ({ token: 'github-app-token' })) as never + ); + mockedOctokit.mockImplementation( + () => + ({ + apps: { + getInstallation: jest.fn(async () => ({ + data: { + account: { id: 12_345, login: 'securexg' }, + created_at: '2026-07-09T19:00:00.000Z', + events: ['issues'], + permissions: { contents: 'write' }, + repository_selection: 'all', + }, + })), + listReposAccessibleToInstallation: jest.fn(), + }, + }) as never + ); + }); + + test('accepts legacy org_ prefixed state when flag is enabled', async () => { + mockedGetEnvVariable.mockReturnValue('true'); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=org_${USER_ID}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation( + response, + `/organizations/${USER_ID}/integrations/github?success=installed` + ); + expect(mockedUpsertPlatformIntegrationForOwner).toHaveBeenCalled(); + }); + + test('refuses legacy org_ prefixed state when flag is disabled', async () => { + mockedGetEnvVariable.mockReturnValue('false'); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=org_${USER_ID}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/'); + expect(mockedUpsertPlatformIntegrationForOwner).not.toHaveBeenCalled(); + }); + + test('accepts legacy user_ prefixed state when flag is enabled', async () => { + mockedGetEnvVariable.mockReturnValue('true'); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=user_${USER_ID}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, `/integrations/github?success=installed`); + expect(mockedUpsertPlatformIntegrationForOwner).toHaveBeenCalled(); + }); + + test('refuses legacy user_ prefixed state when flag is disabled', async () => { + mockedGetEnvVariable.mockReturnValue('false'); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=user_${USER_ID}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/'); + expect(mockedUpsertPlatformIntegrationForOwner).not.toHaveBeenCalled(); + }); + + test('accepts legacy state when flag is unset (default enabled)', async () => { + mockedGetEnvVariable.mockReturnValue(undefined as unknown as string); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=user_${USER_ID}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, `/integrations/github?success=installed`); + expect(mockedUpsertPlatformIntegrationForOwner).toHaveBeenCalled(); + }); +}); + +describe('GET /api/integrations/github/callback admin proof (report mode)', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockedGetUserFromAuth.mockResolvedValue({ + user: { + id: USER_ID, + google_user_email: 'mobile-e2e@example.com', + google_user_name: 'Mobile E2E', + }, + authFailedResponse: null, + } as never); + mockedVerifyGitHubBotLinkState.mockReturnValue(null); + mockedConsumeInstallState.mockResolvedValue(null); + mockedGetEnvVariable.mockReturnValue('true'); + mockedCreateAppAuth.mockReturnValue( + jest.fn(async () => ({ token: 'github-app-token' })) as never + ); + mockedOctokit.mockImplementation( + () => + ({ + apps: { + getInstallation: jest.fn(async () => ({ + data: { + account: { id: 12_345, login: 'securexg' }, + created_at: '2026-07-09T19:00:00.000Z', + events: ['issues'], + permissions: { contents: 'write' }, + repository_selection: 'all', + }, + })), + listReposAccessibleToInstallation: jest.fn(), + }, + }) as never + ); + mockedUpsertPlatformIntegrationForOwner.mockResolvedValue({ ok: true }); + mockedAssertUserAdministersInstallation.mockResolvedValue(true); + mockedExchangeGitHubOAuthCode.mockResolvedValue({ + id: GITHUB_USER_ID, + login: 'octocat', + accessToken: 'ghu_test-token', + }); + }); + + test('completes an install when code is absent (report mode)', async () => { + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=user_${USER_ID}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, `/integrations/github?success=installed`); + expect(mockedUpsertPlatformIntegrationForOwner).toHaveBeenCalled(); + // Admin proof was not run. + expect(mockedExchangeGitHubOAuthCode).not.toHaveBeenCalled(); + expect(mockedAssertUserAdministersInstallation).not.toHaveBeenCalled(); + }); + + test('completes an install when code is present and user is admin (report mode)', async () => { + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=user_${USER_ID}&code=abc` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, `/integrations/github?success=installed`); + expect(mockedExchangeGitHubOAuthCode).toHaveBeenCalledWith('abc', 'standard'); + expect(mockedAssertUserAdministersInstallation).toHaveBeenCalledWith({ + accessToken: 'ghu_test-token', + installationId: INSTALLATION_ID, + }); + expect(mockedUpsertPlatformIntegrationForOwner).toHaveBeenCalled(); + }); + + test('still completes install when admin check returns false (report mode does not block non-admin)', async () => { + mockedAssertUserAdministersInstallation.mockResolvedValue(false); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=user_${USER_ID}&code=abc` + ) as never + ); + + // Report mode: non-admin is logged but the install proceeds. + expect(response.status).toBe(307); + expectRedirectLocation(response, `/integrations/github?success=installed`); + expect(mockedExchangeGitHubOAuthCode).toHaveBeenCalled(); + expect(mockedAssertUserAdministersInstallation).toHaveBeenCalled(); + expect(mockedUpsertPlatformIntegrationForOwner).toHaveBeenCalled(); + }); + + test('still completes install when code exchange fails (report mode)', async () => { + mockedExchangeGitHubOAuthCode.mockRejectedValue(new Error('Token exchange failed')); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=user_${USER_ID}&code=abc` + ) as never + ); + + // API failure in admin proof does not block the install. + expect(response.status).toBe(307); + expectRedirectLocation(response, `/integrations/github?success=installed`); + expect(mockedUpsertPlatformIntegrationForOwner).toHaveBeenCalled(); + }); + + test('redirects with installation_already_claimed when upsert detects cross-owner claim', async () => { + mockedUpsertPlatformIntegrationForOwner.mockResolvedValue({ + ok: false, + reason: 'claimed_by_other_owner', + }); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=user_${USER_ID}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, `/integrations/github?error=installation_already_claimed`); + }); + + test('logs distinct messages for code-absent vs non-admin (report mode distinguishes)', async () => { + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + + // Case 1: code absent — should log code_absent. + const { GET } = await import('./route'); + await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=user_${USER_ID}` + ) as never + ); + + const codeAbsentLogs = logSpy.mock.calls.filter( + (call: unknown[]) => + typeof call[0] === 'string' && (call[0] as string).includes('[github_admin_proof') + ); + expect(codeAbsentLogs.length).toBeGreaterThan(0); + expect(codeAbsentLogs[0][0]).toContain('code_absent'); + expect(codeAbsentLogs[0][0]).not.toContain('fail_non_admin'); + + logSpy.mockClear(); + + // Case 2: code present but non-admin — should log fail_non_admin. + mockedAssertUserAdministersInstallation.mockResolvedValue(false); + await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=user_${USER_ID}&code=abc` + ) as never + ); + + const nonAdminLogs = logSpy.mock.calls.filter( + (call: unknown[]) => + typeof call[0] === 'string' && (call[0] as string).includes('[github_admin_proof') + ); + expect(nonAdminLogs.length).toBeGreaterThan(0); + expect(nonAdminLogs[0][0]).toContain('fail_non_admin'); + expect(nonAdminLogs[0][0]).not.toContain('code_absent'); + + logSpy.mockRestore(); + }); +}); + +describe('GET /api/integrations/github/callback Sentry redaction', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockedGetUserFromAuth.mockResolvedValue({ + user: { + id: USER_ID, + google_user_email: 'mobile-e2e@example.com', + google_user_name: 'Mobile E2E', + }, + authFailedResponse: null, + } as never); + mockedVerifyGitHubBotLinkState.mockReturnValue(null); + mockedConsumeInstallState.mockResolvedValue(null); + mockedGetEnvVariable.mockReturnValue('true'); + mockedCreateAppAuth.mockReturnValue( + jest.fn(async () => ({ token: 'github-app-token' })) as never + ); + mockedOctokit.mockImplementation( + () => + ({ + apps: { + getInstallation: jest.fn(async () => ({ + data: { + account: { id: 12_345, login: 'securexg' }, + created_at: '2026-07-09T19:00:00.000Z', + events: ['issues'], + permissions: { contents: 'write' }, + repository_selection: 'all', + }, + })), + listReposAccessibleToInstallation: jest.fn(), + }, + }) as never + ); + mockedUpsertPlatformIntegrationForOwner.mockResolvedValue({ ok: true }); + }); + + test('unrecognized-state warning does not include the raw state token', async () => { + const RAW_TOKEN = `sentry-redaction-unknown-${Date.now()}`; + mockedConsumeInstallState.mockResolvedValue(null); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${RAW_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/'); + expect(mockedCaptureMessage).toHaveBeenCalled(); + const serializedMessage = JSON.stringify(mockedCaptureMessage.mock.calls); + // The raw state is a bearer token and must never reach Sentry. + expect(serializedMessage).not.toContain(RAW_TOKEN); + // Safe diagnostics survive: state class, reason, and the callback ids. + expect(serializedMessage).toContain('install_token'); + expect(serializedMessage).toContain('state_not_bot_link_or_install_token'); + expect(serializedMessage).toContain(INSTALLATION_ID); + }); + + test('catch-path exception does not include the raw state token', async () => { + const RAW_TOKEN = `sentry-redaction-catch-${Date.now()}`; + mockedConsumeInstallState.mockResolvedValue({ + token: RAW_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'user', + owner_id: USER_ID, + github_app_type: 'standard', + return_to: null, + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + mockedOctokit.mockImplementation( + () => + ({ + apps: { + getInstallation: jest.fn(async () => { + throw new Error('get installation failed'); + }), + listReposAccessibleToInstallation: jest.fn(), + }, + }) as never + ); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${RAW_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/?error=installation_failed'); + expect(mockedCaptureException).toHaveBeenCalled(); + const serializedException = JSON.stringify(mockedCaptureException.mock.calls); + // The raw state is a bearer token and must never reach Sentry. + expect(serializedException).not.toContain(RAW_TOKEN); + // Safe diagnostics survive: state class, reason, and the callback ids. + expect(serializedException).toContain('install_token'); + expect(serializedException).toContain('callback_flow_error'); + expect(serializedException).toContain(INSTALLATION_ID); + }); + + test('legacy-enabled warning does not include the raw state token', async () => { + const RAW_TOKEN = `org_sentry-redaction-legacy-${Date.now()}`; + mockedGetEnvVariable.mockReturnValue('true'); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${RAW_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expect(mockedCaptureMessage).toHaveBeenCalled(); + const serializedMessage = JSON.stringify(mockedCaptureMessage.mock.calls); + // The raw state is a bearer token and must never reach Sentry. + expect(serializedMessage).not.toContain(RAW_TOKEN); + // Safe diagnostics survive: state class and the callback id. + expect(serializedMessage).toContain('legacy_org'); + expect(serializedMessage).toContain('legacy_install_state'); + expect(serializedMessage).toContain(INSTALLATION_ID); + }); + + test('legacy-refused warning does not include the raw state token', async () => { + const RAW_TOKEN = `user_sentry-redaction-refused-${Date.now()}`; + mockedGetEnvVariable.mockReturnValue('false'); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${RAW_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/'); + expect(mockedCaptureMessage).toHaveBeenCalled(); + const serializedMessage = JSON.stringify(mockedCaptureMessage.mock.calls); + // The raw state is a bearer token and must never reach Sentry. + expect(serializedMessage).not.toContain(RAW_TOKEN); + // Safe diagnostics survive: state class and the callback id. + expect(serializedMessage).toContain('legacy_user'); + expect(serializedMessage).toContain('legacy_install_state_disabled'); + expect(serializedMessage).toContain(INSTALLATION_ID); + }); + + test('missing-installation diagnostic does not include the raw state token or params', async () => { + const RAW_TOKEN = `user_${USER_ID}`; + mockedGetEnvVariable.mockReturnValue('true'); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?setup_action=install&state=${RAW_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/integrations/github?error=missing_installation_id'); + expect(mockedCaptureMessage).toHaveBeenCalled(); + const serializedMessage = JSON.stringify(mockedCaptureMessage.mock.calls); + // The raw state is a bearer token and must never reach Sentry. + expect(serializedMessage).not.toContain(RAW_TOKEN); + // Safe diagnostics survive: state class, reason, and the setup action. + expect(serializedMessage).toContain('legacy_user'); + expect(serializedMessage).toContain('missing_installation_id'); + expect(serializedMessage).toContain('setupAction'); + }); + + test('invalid-owner diagnostic does not include the raw state token or params', async () => { + const RAW_TOKEN = `user_sentry-redaction-invalid-owner-${Date.now()}`; + mockedGetEnvVariable.mockReturnValue('true'); + // Force the defensive invalid-owner branch inside handleLegacyInstallFlow, + // which is unreachable through the legacy prefix gate alone. + mockedParseStateReturn.mockReturnValue({ ownerToken: 'unrecognized', returnTo: null }); + + try { + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${RAW_TOKEN}` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation(response, '/'); + expect(mockedCaptureMessage).toHaveBeenCalled(); + const serializedMessage = JSON.stringify(mockedCaptureMessage.mock.calls); + // The raw state is a bearer token and must never reach Sentry. + expect(serializedMessage).not.toContain(RAW_TOKEN); + // Safe diagnostics survive: state class, reason, and the callback id. + expect(serializedMessage).toContain('legacy_user'); + expect(serializedMessage).toContain('owner_not_org_or_user_prefix'); + expect(serializedMessage).toContain(INSTALLATION_ID); + } finally { + const realParseStateReturn = jest.requireActual('@/lib/integrations/validate-return-path') + .parseStateReturn as (rawState: string | null) => { + ownerToken: string; + returnTo: string | null; + }; + mockedParseStateReturn.mockImplementation(realParseStateReturn); + } + }); +}); diff --git a/apps/web/src/app/api/integrations/github/callback/route.ts b/apps/web/src/app/api/integrations/github/callback/route.ts index af0927d729..0f54b06ed8 100644 --- a/apps/web/src/app/api/integrations/github/callback/route.ts +++ b/apps/web/src/app/api/integrations/github/callback/route.ts @@ -1,12 +1,15 @@ import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { getUserFromAuth } from '@/lib/user/server'; +import type { User } from '@kilocode/db/schema'; import { Octokit } from '@octokit/rest'; import { createAppAuth } from '@octokit/auth-app'; import { exchangeGitHubOAuthCode } from '@/lib/integrations/platforms/github/adapter'; import { getGitHubAppTypeForOrganization, getGitHubAppCredentials, + assertUserAdministersInstallation, + type GitHubAppType, } from '@/lib/integrations/platforms/github/app-selector'; import { ensureOrganizationAccess } from '@/routers/organizations/utils'; import { @@ -28,10 +31,36 @@ import { bot } from '@/lib/bot'; import { isOrganizationMember } from '@/lib/organizations/organizations'; import { PLATFORM } from '@/lib/integrations/core/constants'; import { APP_URL } from '@/lib/constants'; +import { consumeInstallState } from '@/lib/integrations/github/install-state'; +import type { GitHubInstallState } from '@kilocode/db/schema'; +import { getEnvVariable } from '@/lib/dotenvx'; const appendQueryParam = (path: string, queryParam: string): string => `${path}${path.includes('?') ? '&' : '?'}${queryParam}`; +type InstallStateClass = + | 'none' + | 'legacy_org' + | 'legacy_user' + | 'bot_link' + | 'install_token' + | 'unknown'; + +/** + * Classifies a GitHub callback state value without revealing its contents. + * Database install states are bearer tokens, so diagnostics must never carry + * the raw value — only its shape class. + */ +function classifyInstallState(rawState: string | null): InstallStateClass { + if (!rawState) return 'none'; + if (rawState.startsWith('org_')) return 'legacy_org'; + if (rawState.startsWith('user_')) return 'legacy_user'; + if (rawState.includes('.')) return 'bot_link'; + // Database install tokens are 32 random bytes, base64url-encoded (no dots). + if (/^[A-Za-z0-9_-]{16,}$/.test(rawState)) return 'install_token'; + return 'unknown'; +} + function htmlPage(title: string, message: string, status = 200): Response { return new Response( ` @@ -67,6 +96,9 @@ async function handleGitHubBotLinkCallback(request: NextRequest, user: { id: str ); } + // Bot-link states carry no GitHub app type, so this lookup is intentionally + // unscoped; the integration row's own github_app_type drives the OAuth + // exchange below. Ownership checks then restrict the result to this user. const integration = await findIntegrationByInstallationId(PLATFORM.GITHUB, state.installationId); if (!integration) { @@ -86,7 +118,7 @@ async function handleGitHubBotLinkCallback(request: NextRequest, user: { id: str return htmlPage('Link Failed', 'You are not the owner of this GitHub integration.', 403); } - const appType = integration.github_app_type ?? 'standard'; + const appType = (integration.github_app_type ?? 'standard') as GitHubAppType; const githubUser = await exchangeGitHubOAuthCode(code, appType); await bot.initialize(); @@ -116,8 +148,6 @@ export async function GET(request: NextRequest) { // 1. Verify user authentication const { user, authFailedResponse } = await getUserFromAuth({ adminOnly: false }); if (authFailedResponse) { - // If user is not authenticated (e.g., GitHub admin approving installation), - // redirect to homepage instead of showing "Unauthorized" return NextResponse.redirect(new URL('/', APP_URL)); } @@ -127,304 +157,554 @@ export async function GET(request: NextRequest) { const setupAction = searchParams.get('setup_action'); const rawState = searchParams.get('state'); - // 3. Bot-link callback hand-off — runs BEFORE owner parsing because - // bot-link state values do not start with `org_`/`user_` and have a - // different signature (verifyGitHubBotLinkState). - if (rawState && !rawState.startsWith('org_') && !rawState.startsWith('user_')) { + // 3. New database-backed install state — atomically consume the token. + // Consumed before any other dispatch so a minted token is unambiguous: + // even if a random base64url token's shape began with a legacy org_/user_ + // prefix, the DB row wins and the state is never misrouted to legacy or + // bot-link parsing. + if (rawState) { + const installRow = await consumeInstallState(rawState); + if (installRow) { + return await handleNewInstallFlow(request, user, installRow, installationId, setupAction); + } + + // 4. Legacy plaintext branch — starts with org_ or user_ prefix. + // Legacy states are never bot-link or database-minted tokens. + // They are accepted only when the GITHUB_LEGACY_INSTALL_STATE flag is enabled. + if (rawState.startsWith('org_') || rawState.startsWith('user_')) { + const legacyEnabled = (getEnvVariable('GITHUB_LEGACY_INSTALL_STATE') ?? 'true') !== 'false'; + + if (legacyEnabled) { + // ponytail: the legacy branch is deleted once the legacy-state counter + // reports zero for 30 consecutive days. + captureMessage('GitHub callback using legacy plaintext install state', { + level: 'info', + tags: { endpoint: 'github/callback', source: 'legacy_install_state' }, + extra: { installationId, stateClass: classifyInstallState(rawState) }, + }); + return await handleLegacyInstallFlow( + request, + user, + rawState, + installationId, + setupAction + ); + } + + // Legacy flag disabled: refuse the legacy state. + captureMessage('GitHub callback with legacy state refused (flag disabled)', { + level: 'warning', + tags: { endpoint: 'github/callback', source: 'legacy_install_state_disabled' }, + extra: { installationId, stateClass: classifyInstallState(rawState) }, + }); + return NextResponse.redirect(new URL('/', APP_URL)); + } + + // 5. Bot-link callback hand-off. Bot-link state tokens use a signed + // dot-separated format and never collide with database tokens, which are + // base64url (no dots). Tried last, after DB and legacy dispatch. const botLinkState = verifyGitHubBotLinkState(rawState); if (botLinkState) { return await handleGitHubBotLinkCallback(request, user); } } - // 4. Parse owner from state (with optional |return= suffix) - const { ownerToken, returnTo } = parseStateReturn(rawState); - let owner: Owner; - let ownerId: string; - - if (ownerToken.startsWith('org_')) { - ownerId = ownerToken.slice(4); - owner = { type: 'org', id: ownerId }; - } else if (ownerToken.startsWith('user_')) { - ownerId = ownerToken.slice(5); - owner = { type: 'user', id: ownerId }; - } else { - captureMessage('GitHub callback missing or invalid owner in state', { - level: 'warning', - tags: { endpoint: 'github/callback', source: 'github_app_installation' }, - extra: { installationId, rawState, allParams: Object.fromEntries(searchParams.entries()) }, - }); - return NextResponse.redirect(new URL('/', APP_URL)); - } + // 6. Both bot-link and database consume returned null. + captureMessage('GitHub callback with unrecognized state', { + level: 'warning', + tags: { endpoint: 'github/callback', source: 'github_app_installation' }, + extra: { + installationId, + setupAction, + stateClass: classifyInstallState(rawState), + reason: 'state_not_bot_link_or_install_token', + }, + }); + return NextResponse.redirect(new URL('/', APP_URL)); + } catch (error) { + console.error('Error handling GitHub App callback:', error); - // 4. Verify user has access to the owner - if (owner.type === 'org') { - await ensureOrganizationAccess({ user }, owner.id); - } else { - // For user-owned integrations, verify it's the same user - if (user.id !== owner.id) { - return NextResponse.redirect(new URL('/', APP_URL)); - } - } + const searchParams = request.nextUrl.searchParams; + const rawState = searchParams.get('state'); - const integrationPath = - owner.type === 'org' - ? `/organizations/${owner.id}/integrations/github` - : `/integrations/github`; - const redirectPath = returnTo || integrationPath; + captureException(error, { + tags: { + endpoint: 'github/callback', + source: 'github_app_installation', + }, + extra: { + installationId: searchParams.get('installation_id'), + setupAction: searchParams.get('setup_action'), + stateClass: classifyInstallState(rawState), + reason: 'callback_flow_error', + }, + }); - // 5. Determine which GitHub App to use based on organization settings - const appType = await getGitHubAppTypeForOrganization(owner.type === 'org' ? owner.id : null); - const credentials = getGitHubAppCredentials(appType); + const { ownerToken: errorOwnerToken, returnTo } = parseStateReturn(rawState); - // Handle uninstall/suspend actions - if (setupAction === 'delete' || setupAction === 'suspend') { - console.log(`GitHub App ${setupAction} action detected, skipping installation fetch`); + let redirectPath = returnTo || '/'; - return NextResponse.redirect( - new URL(appendQueryParam(redirectPath, `github_action=${setupAction}`), APP_URL) - ); + if (!returnTo && errorOwnerToken.startsWith('org_')) { + const orgId = errorOwnerToken.slice(4); + redirectPath = `/organizations/${orgId}/integrations/github`; + } else if (!returnTo && errorOwnerToken.startsWith('user_')) { + redirectPath = `/integrations/github`; } - // Handle pending approval - store requester info for webhook matching - if (setupAction === 'request') { - const code = searchParams.get('code'); - - try { - let githubRequester: { id: string; login: string } | undefined; - - // Exchange OAuth code for GitHub user identity - if (code) { - try { - githubRequester = await exchangeGitHubOAuthCode(code, appType); - - console.log('GitHub user fetched', { - github_user_id: githubRequester.id, - github_user_login: githubRequester.login, - }); - } catch (error) { - console.error('Error fetching GitHub user:', error); - captureException(error); - // Continue without GitHub user info - } - } + return NextResponse.redirect( + new URL(appendQueryParam(redirectPath, 'error=installation_failed'), APP_URL) + ); + } +} - // Check for existing pending installation by this GitHub user - if (githubRequester) { - const existingPending = await findPendingInstallationByRequesterId(githubRequester.id); - - if (existingPending) { - const existingOwnerId = - existingPending.owned_by_organization_id || existingPending.owned_by_user_id; - - console.log('User already has a pending installation', { - existingPendingId: existingPending.id, - existingOwnerId, - githubRequesterId: githubRequester.id, - }); - - const queryParam = - owner.type === 'org' - ? `error=pending_installation_exists&org=${existingOwnerId}` - : 'error=pending_installation_exists'; - - return NextResponse.redirect( - new URL(appendQueryParam(redirectPath, queryParam), APP_URL) - ); - } - } +/** + * New database-backed install flow. The state was atomically consumed. + * Verify the user matches and run the install flow using row data. + */ +async function handleNewInstallFlow( + request: NextRequest, + user: { id: string; google_user_email: string; google_user_name: string }, + installRow: GitHubInstallState, + installationId: string, + setupAction: string | null +): Promise { + // Verify user identity — a state minted for another user must never be usable. + if (installRow.kilo_user_id !== user.id) { + captureMessage('GitHub install state consumed by different user', { + level: 'warning', + tags: { endpoint: 'github/callback', source: 'install_state_user_mismatch' }, + extra: { + tokenUserId: installRow.kilo_user_id, + callbackUserId: user.id, + installationId, + }, + }); + // Redirect to the integration page with a specific error so the UI can + // display the corrective message. If the flow was started from the app + // (return_to is set to an app route like /cloud/sessions) include fromApp=1 + // so the /github-app fallback triggers. + const returnTo = installRow.return_to; + const isAppInitiated = returnTo?.startsWith('/cloud/') === true; + const organizationId = + installRow.owner_type === 'org' ? `&organizationId=${installRow.owner_id}` : ''; + const mismatchQuery = isAppInitiated + ? `error=install_state_user_mismatch&fromApp=1${organizationId}` + : 'error=install_state_user_mismatch'; + return NextResponse.redirect(new URL(appendQueryParam('/github-app', mismatchQuery), APP_URL)); + } - // Create pending installation record with requester info - await createPendingIntegration({ - organizationId: owner.type === 'org' ? owner.id : undefined, - userId: owner.type === 'user' ? owner.id : undefined, - requester: { - kilo_user_id: user.id, - kilo_user_email: user.google_user_email, - kilo_user_name: user.google_user_name, - requested_at: new Date().toISOString(), - }, - githubRequester, - githubAppType: appType, - }); + const ownerType = installRow.owner_type as Owner['type']; + const ownerId = installRow.owner_id; + const owner: Owner = { type: ownerType, id: ownerId }; + const returnTo = installRow.return_to; + + return handleCoreInstallFlow({ + request, + user, + owner, + ownerId, + returnTo, + installationId, + setupAction, + githubAppType: installRow.github_app_type as GitHubAppType, + }); +} - // Redirect back to integrations page with pending approval status - const queryParam = returnTo ? 'github_pending_approval=true' : 'pending_approval=true'; +/** + * Legacy plaintext install flow. Parses owner from the org_/user_ prefix. + * Gated by GITHUB_LEGACY_INSTALL_STATE flag. + */ +async function handleLegacyInstallFlow( + request: NextRequest, + user: { id: string; google_user_email: string; google_user_name: string }, + rawState: string, + installationId: string, + setupAction: string | null +): Promise { + // Parse owner from state (with optional |return= suffix) + const { ownerToken, returnTo } = parseStateReturn(rawState); + let owner: Owner; + let ownerId: string; + + if (ownerToken.startsWith('org_')) { + ownerId = ownerToken.slice(4); + owner = { type: 'org', id: ownerId }; + } else if (ownerToken.startsWith('user_')) { + ownerId = ownerToken.slice(5); + owner = { type: 'user', id: ownerId }; + } else { + captureMessage('GitHub callback missing or invalid owner in state', { + level: 'warning', + tags: { endpoint: 'github/callback', source: 'github_app_installation' }, + extra: { + installationId, + stateClass: classifyInstallState(rawState), + reason: 'owner_not_org_or_user_prefix', + }, + }); + return NextResponse.redirect(new URL('/', APP_URL)); + } - return NextResponse.redirect(new URL(appendQueryParam(redirectPath, queryParam), APP_URL)); - } catch (error) { - console.error('Error creating pending installation:', error); - captureException(error); + const appType = await getGitHubAppTypeForOrganization(owner.type === 'org' ? owner.id : null); + + return handleCoreInstallFlow({ + request, + user, + owner, + ownerId, + returnTo, + installationId, + setupAction, + githubAppType: appType, + }); +} - return NextResponse.redirect( - new URL(appendQueryParam(redirectPath, 'error=pending_setup_failed'), APP_URL) - ); - } +/** + * Core install flow shared by both legacy and new branches. + * Handles access checks, pending approval, and installation storage. + */ +async function handleCoreInstallFlow(params: { + request: NextRequest; + user: { id: string; google_user_email: string; google_user_name: string }; + owner: Owner; + ownerId: string; + returnTo: string | null; + installationId: string; + setupAction: string | null; + githubAppType: GitHubAppType; +}): Promise { + const { user, owner, ownerId, returnTo, installationId, setupAction, githubAppType } = params; + const searchParams = params.request.nextUrl.searchParams; + + // Verify user has access to the owner + if (owner.type === 'org') { + await ensureOrganizationAccess({ user: user as unknown as User }, owner.id); + } else { + if (user.id !== owner.id) { + return NextResponse.redirect(new URL('/', APP_URL)); } + } - // Validate installation_id is present for normal install action - if (!installationId) { - captureMessage('GitHub callback missing installation_id', { - level: 'warning', - tags: { endpoint: 'github/callback', source: 'github_app_installation' }, - extra: { setupAction, rawState, allParams: Object.fromEntries(searchParams.entries()) }, - }); + const integrationPath = + owner.type === 'org' + ? `/organizations/${owner.id}/integrations/github` + : `/integrations/github`; + const redirectPath = returnTo || integrationPath; + + // App-initiated flows carry returnTo="/cloud/sessions". A server redirect + // does not always open the app, so redirect to /github-app where the + // fallback card renders the outcome and a user-initiated "Return to Kilo + // App" link that reliably triggers the universal link. + const isAppInitiated = returnTo?.startsWith('/cloud/') === true; + const appFallbackPath = (query: string) => { + const organizationParam = + owner.type === 'org' ? `&organizationId=${encodeURIComponent(ownerId)}` : ''; + return `/github-app?fromApp=1&${query}${organizationParam}`; + }; + + const credentials = getGitHubAppCredentials(githubAppType); + + // Handle uninstall/suspend actions + if (setupAction === 'delete' || setupAction === 'suspend') { + console.log(`GitHub App ${setupAction} action detected, skipping installation fetch`); - return NextResponse.redirect( - new URL(appendQueryParam(redirectPath, 'error=missing_installation_id'), APP_URL) - ); - } + return NextResponse.redirect( + new URL(appendQueryParam(redirectPath, `github_action=${setupAction}`), APP_URL) + ); + } - // 6. Fetch installation details from GitHub - // Create app authentication without installationId to get installation details - const auth = createAppAuth({ - appId: credentials.appId, - privateKey: credentials.privateKey, - }); + // Handle pending approval - store requester info for webhook matching + if (setupAction === 'request') { + const code = searchParams.get('code'); - // Get app-level JWT token to fetch installation details - const appAuth = await auth({ type: 'app' }); - const octokitApp = new Octokit({ - auth: appAuth.token, - }); - - // Fetch installation details using app-level token - let installation; try { - console.log('Fetching installation details for ID:', installationId); - const result = await octokitApp.apps.getInstallation({ - installation_id: parseInt(installationId), - }); - installation = result.data; - } catch (error) { - const err = error as { message?: string; status?: number }; - - // Capture to Sentry for monitoring - captureException(error, { - tags: { - endpoint: 'github/callback', - source: 'github_api_get_installation', - status: err.status?.toString() || 'unknown', - }, - extra: { - installationId, - ownerId, - ownerType: owner.type, - setupAction, - errorStatus: err.status, - errorMessage: err.message, + let githubRequester: { id: string; login: string } | undefined; + + if (code) { + try { + const githubUser = await exchangeGitHubOAuthCode(code, githubAppType); + githubRequester = { id: githubUser.id, login: githubUser.login }; + + console.log('GitHub user fetched', { + github_user_id: githubRequester.id, + github_user_login: githubRequester.login, + }); + } catch (error) { + console.error('Error fetching GitHub user:', error); + captureException(error); + } + } + + if (githubRequester) { + const existingPending = await findPendingInstallationByRequesterId(githubRequester.id); + + if (existingPending) { + const existingOwnerId = + existingPending.owned_by_organization_id || existingPending.owned_by_user_id; + + console.log('User already has a pending installation', { + existingPendingId: existingPending.id, + existingOwnerId, + githubRequesterId: githubRequester.id, + }); + + const queryParam = + owner.type === 'org' + ? `error=pending_installation_exists&org=${existingOwnerId}` + : 'error=pending_installation_exists'; + + return NextResponse.redirect( + new URL( + isAppInitiated + ? appFallbackPath(queryParam) + : appendQueryParam(redirectPath, queryParam), + APP_URL + ) + ); + } + } + + await createPendingIntegration({ + organizationId: owner.type === 'org' ? owner.id : undefined, + userId: owner.type === 'user' ? owner.id : undefined, + requester: { + kilo_user_id: user.id, + kilo_user_email: user.google_user_email, + kilo_user_name: user.google_user_name, + requested_at: new Date().toISOString(), }, + githubRequester, + githubAppType, }); - // If installation not found, it might have been deleted or belongs to a different app - if (err.status === 404) { - const encodedInstallationId = encodeURIComponent(installationId); + const orgParam = + isAppInitiated && owner.type === 'org' + ? `&organizationId=${encodeURIComponent(ownerId)}` + : ''; + const queryParam = isAppInitiated + ? `fromApp=1&github_pending_approval=true${orgParam}` + : returnTo + ? 'github_pending_approval=true' + : 'pending_approval=true'; + + const pendingRedirectPath = isAppInitiated ? '/github-app' : redirectPath; + return NextResponse.redirect( + new URL(appendQueryParam(pendingRedirectPath, queryParam), APP_URL) + ); + } catch (error) { + console.error('Error creating pending installation:', error); + captureException(error); + if (isAppInitiated) { return NextResponse.redirect( - new URL( - appendQueryParam( - redirectPath, - `error=installation_not_found&id=${encodedInstallationId}` - ), - APP_URL - ) + new URL(appFallbackPath('error=pending_setup_failed'), APP_URL) ); } - - throw error; + return NextResponse.redirect( + new URL(appendQueryParam(redirectPath, 'error=pending_setup_failed'), APP_URL) + ); } + } - // 7. Get selected repositories - // For 'selected' repositories, we fetch the list. For 'all', we set it to null - let repositories: PlatformRepository[] | null = null; - if (installation.repository_selection === 'selected') { - // Need to use installation token (not app token) to list repos - console.log('Fetching repositories for installation:', installationId); - const installationAuth = await auth({ - type: 'installation', - installationId: parseInt(installationId), - }); - const octokitInstallation = new Octokit({ - auth: installationAuth.token, - }); + // Validate installation_id is present for normal install action + if (!installationId) { + captureMessage('GitHub callback missing installation_id', { + level: 'warning', + tags: { endpoint: 'github/callback', source: 'github_app_installation' }, + extra: { + setupAction, + stateClass: classifyInstallState(searchParams.get('state')), + reason: 'missing_installation_id', + }, + }); - const { data: reposData } = - await octokitInstallation.apps.listReposAccessibleToInstallation(); - repositories = reposData.repositories.map(repo => ({ - id: repo.id, - name: repo.name, - full_name: repo.full_name, - private: repo.private, - })); + if (isAppInitiated) { + return NextResponse.redirect( + new URL(appFallbackPath('error=missing_installation_id'), APP_URL) + ); } + return NextResponse.redirect( + new URL(appendQueryParam(redirectPath, 'error=missing_installation_id'), APP_URL) + ); + } - // 8. Store installation in database using new platform_integrations table - if (setupAction === 'install' || setupAction === 'update') { - // Handle null account and union type (User | Organization) - if (!installation.account) { - throw new Error('Installation account is missing'); - } + // Admin proof — report mode. The GitHub App does not yet request OAuth + // authorization during installation. When `code` is present we verify + // administration and log the outcome; when absent we log and proceed. + // A follow-up commit will hard-require `code` after the App setting is + // enabled in the GitHub App dashboard. + if (setupAction === 'install' || setupAction === 'update') { + const code = searchParams.get('code'); + + if (code) { + try { + const exchangeResult = await exchangeGitHubOAuthCode(code, githubAppType); + const isAdmin = await assertUserAdministersInstallation({ + accessToken: exchangeResult.accessToken, + installationId, + }); - const account = installation.account; - const accountId = account.id.toString(); - const accountLogin = - 'login' in account ? account.login : 'slug' in account ? account.slug : accountId; - - await upsertPlatformIntegrationForOwner(owner, { - platform: 'github', - integrationType: 'app', - platformInstallationId: installationId, - platformAccountId: accountId, - platformAccountLogin: accountLogin, - permissions: installation.permissions as IntegrationPermissions, - scopes: installation.events || [], - repositoryAccess: installation.repository_selection, - repositories: repositories && repositories.length > 0 ? repositories : null, - installedAt: installation.created_at - ? new Date(installation.created_at).toISOString() - : new Date().toISOString(), - githubAppType: appType, + if (isAdmin) { + console.log('[github_admin_proof:pass]', { + github_user_id: exchangeResult.id, + github_user_login: exchangeResult.login, + installation_id: installationId, + }); + } else { + console.log('[github_admin_proof:fail_non_admin]', { + github_user_id: exchangeResult.id, + github_user_login: exchangeResult.login, + installation_id: installationId, + }); + } + } catch (error) { + console.error('[github_admin_proof:error]', { + installation_id: installationId, + error: (error as Error).message, + }); + captureException(error, { + tags: { + endpoint: 'github/callback', + source: 'github_admin_proof', + }, + extra: { installationId }, + }); + } + } else { + console.log('[github_admin_proof:code_absent]', { + installation_id: installationId, + setup_action: setupAction, }); } + } - // 9. Redirect to success page - const successQueryParam = returnTo ? 'github_install=success' : 'success=installed'; + // Fetch installation details from GitHub + const auth = createAppAuth({ + appId: credentials.appId, + privateKey: credentials.privateKey, + }); - return NextResponse.redirect( - new URL(appendQueryParam(redirectPath, successQueryParam), APP_URL) - ); - } catch (error) { - console.error('Error handling GitHub App callback:', error); + const appAuth = await auth({ type: 'app' }); + const octokitApp = new Octokit({ + auth: appAuth.token, + }); - // Capture error to Sentry with context for debugging - const searchParams = request.nextUrl.searchParams; - const rawState = searchParams.get('state'); + let installation; + try { + console.log('Fetching installation details for ID:', installationId); + const result = await octokitApp.apps.getInstallation({ + installation_id: parseInt(installationId), + }); + installation = result.data; + } catch (error) { + const err = error as { message?: string; status?: number }; captureException(error, { tags: { endpoint: 'github/callback', - source: 'github_app_installation', + source: 'github_api_get_installation', + status: err.status?.toString() || 'unknown', }, extra: { - installationId: searchParams.get('installation_id'), - setupAction: searchParams.get('setup_action'), - rawState, + installationId, + ownerId, + ownerType: owner.type, + setupAction, + errorStatus: err.status, + errorMessage: err.message, }, }); - const { ownerToken: errorOwnerToken, returnTo } = parseStateReturn(rawState); + if (err.status === 404) { + const encodedInstallationId = encodeURIComponent(installationId); - let redirectPath = returnTo || '/'; + if (isAppInitiated) { + return NextResponse.redirect( + new URL(appFallbackPath('error=installation_not_found'), APP_URL) + ); + } + return NextResponse.redirect( + new URL( + appendQueryParam( + redirectPath, + `error=installation_not_found&id=${encodedInstallationId}` + ), + APP_URL + ) + ); + } - if (!returnTo && errorOwnerToken.startsWith('org_')) { - const orgId = errorOwnerToken.slice(4); - redirectPath = `/organizations/${orgId}/integrations/github`; - } else if (!returnTo && errorOwnerToken.startsWith('user_')) { - redirectPath = `/integrations/github`; + if (isAppInitiated) { + return NextResponse.redirect(new URL(appFallbackPath('error=installation_failed'), APP_URL)); } + throw error; + } - return NextResponse.redirect( - new URL(appendQueryParam(redirectPath, 'error=installation_failed'), APP_URL) - ); + // Get selected repositories + let repositories: PlatformRepository[] | null = null; + if (installation.repository_selection === 'selected') { + console.log('Fetching repositories for installation:', installationId); + const installationAuth = await auth({ + type: 'installation', + installationId: parseInt(installationId), + }); + const octokitInstallation = new Octokit({ + auth: installationAuth.token, + }); + + const { data: reposData } = await octokitInstallation.apps.listReposAccessibleToInstallation(); + repositories = reposData.repositories.map(repo => ({ + id: repo.id, + name: repo.name, + full_name: repo.full_name, + private: repo.private, + })); + } + + // Store installation in database + if (setupAction === 'install' || setupAction === 'update') { + if (!installation.account) { + throw new Error('Installation account is missing'); + } + + const account = installation.account; + const accountId = account.id.toString(); + const accountLogin = + 'login' in account ? account.login : 'slug' in account ? account.slug : accountId; + + const upsertResult = await upsertPlatformIntegrationForOwner(owner, { + platform: 'github', + integrationType: 'app', + platformInstallationId: installationId, + platformAccountId: accountId, + platformAccountLogin: accountLogin, + permissions: installation.permissions as IntegrationPermissions, + scopes: installation.events || [], + repositoryAccess: installation.repository_selection, + repositories: repositories && repositories.length > 0 ? repositories : null, + installedAt: installation.created_at + ? new Date(installation.created_at).toISOString() + : new Date().toISOString(), + githubAppType, + }); + + if (!upsertResult.ok) { + if (isAppInitiated) { + return NextResponse.redirect( + new URL(appFallbackPath('error=installation_already_claimed'), APP_URL) + ); + } + return NextResponse.redirect( + new URL(appendQueryParam(redirectPath, 'error=installation_already_claimed'), APP_URL) + ); + } } + + // Redirect to success page + if (isAppInitiated) { + return NextResponse.redirect(new URL(appFallbackPath('github_install=success'), APP_URL)); + } + const successQueryParam = returnTo ? 'github_install=success' : 'success=installed'; + + return NextResponse.redirect(new URL(appendQueryParam(redirectPath, successQueryParam), APP_URL)); } diff --git a/apps/web/src/app/collab/authorize/_components/AuthorizeFlow.tsx b/apps/web/src/app/collab/authorize/_components/AuthorizeFlow.tsx index d11e7ffd9a..3065b409b8 100644 --- a/apps/web/src/app/collab/authorize/_components/AuthorizeFlow.tsx +++ b/apps/web/src/app/collab/authorize/_components/AuthorizeFlow.tsx @@ -8,12 +8,15 @@ import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import KiloLogo from '@/components/KiloLogo'; import { useUser } from '@/hooks/useUser'; +import { useTRPC } from '@/lib/trpc/utils'; +import { useMutation } from '@tanstack/react-query'; import { getPlatformOAuthConnectPath, type StandardOAuthPlatform, } from '@/lib/integrations/oauth/paths'; import { getPlatform, type PlatformId, type PlatformOption } from '../../_components/platforms'; import { buildReturnToPath } from './authorize-path'; +import { buildGitHubInstallState } from '@/components/integrations/github-install-state'; type ProgressListProps = { count: number; @@ -32,6 +35,8 @@ export function AuthorizeFlow(props: AuthorizeFlowProps) { const { serviceIds, connectedServiceIds, organizationId, initialIndex, initialError } = props; const router = useRouter(); const { data: user } = useUser(); + const trpc = useTRPC(); + const mintInstallState = useMutation(trpc.githubApps.mintInstallState.mutationOptions()); const [index, setIndex] = useState(initialIndex); const [done, setDone] = useState(initialIndex >= serviceIds.length); const [connectionError, setConnectionError] = useState(initialError ?? null); @@ -70,6 +75,18 @@ export function AuthorizeFlow(props: AuthorizeFlowProps) { try { setIsStartingOAuth(true); + + if (current.id === 'github') { + const result = await mintInstallState.mutateAsync({ + organizationId: organizationId ?? undefined, + returnTo, + }); + const githubAppName = process.env.NEXT_PUBLIC_GITHUB_APP_NAME || 'KiloConnect'; + const state = buildGitHubInstallState(result.token); + window.location.href = `https://github.com/apps/${githubAppName}/installations/new?state=${encodeURIComponent(state)}`; + return; + } + const oauthUrl = await getOAuthUrl(current.id, { organizationId, returnTo, @@ -177,17 +194,6 @@ async function getOAuthUrl( if (isCollabOAuthConnectPlatform(platformId)) { return getPlatformOAuthConnectPath(platformId, options.organizationId, options.returnTo); } - if (platformId === 'github') { - const ownerToken = options.organizationId - ? `org_${options.organizationId}` - : options.userId - ? `user_${options.userId}` - : null; - if (!ownerToken) return null; - const githubAppName = process.env.NEXT_PUBLIC_GITHUB_APP_NAME || 'KiloConnect'; - const state = `${ownerToken}|return=${encodeURIComponent(options.returnTo)}`; - return `https://github.com/apps/${githubAppName}/installations/new?state=${encodeURIComponent(state)}`; - } return null; } diff --git a/apps/web/src/app/device-auth/DeviceAuthClient.tsx b/apps/web/src/app/device-auth/DeviceAuthClient.tsx index e69ed4974c..86240dfcc9 100644 --- a/apps/web/src/app/device-auth/DeviceAuthClient.tsx +++ b/apps/web/src/app/device-auth/DeviceAuthClient.tsx @@ -17,6 +17,7 @@ import { type DeviceAuthClientProps = { code: string; + viewerToken: string; isAppMode: boolean; user: { name: string; @@ -45,7 +46,7 @@ function getUserInitials(name: string): string { return name.slice(0, 2).toUpperCase(); } -export function DeviceAuthClient({ code, isAppMode, user }: DeviceAuthClientProps) { +export function DeviceAuthClient({ code, viewerToken, isAppMode, user }: DeviceAuthClientProps) { const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'denied' | 'error'>('idle'); const [errorMessage, setErrorMessage] = useState(''); const [isSigningOut, setIsSigningOut] = useState(false); @@ -95,6 +96,9 @@ export function DeviceAuthClient({ code, isAppMode, user }: DeviceAuthClientProp // Deny: DELETE to /api/device-auth/codes/:code const response = await fetch(`/api/device-auth/codes/${code}`, { method: 'DELETE', + headers: { + 'x-device-auth-viewer-token': viewerToken, + }, }); if (!response.ok) { diff --git a/apps/web/src/app/device-auth/page.tsx b/apps/web/src/app/device-auth/page.tsx index 04f14f86ad..2d5dd45aa2 100644 --- a/apps/web/src/app/device-auth/page.tsx +++ b/apps/web/src/app/device-auth/page.tsx @@ -3,6 +3,7 @@ import { z } from 'zod'; import { getUserFromAuthOrRedirect } from '@/lib/user/server'; import { DeviceAuthClient } from './DeviceAuthClient'; import { buildDeviceAuthPath, isDeviceAuthAppMode } from './device-auth-url'; +import { createDeviceAuthViewerToken } from '@/lib/device-auth/device-auth-viewer-token'; type PageProps = { searchParams: Promise>; @@ -31,9 +32,12 @@ export default async function DeviceAuthPage({ searchParams }: PageProps) { redirect('/'); } + const viewerToken = createDeviceAuthViewerToken(code, user.id); + return ( ; -function getGitHubAppReturnPath(organizationId?: string): string { - if (!organizationId) { - return '/github-app'; - } - return `/github-app?organizationId=${encodeURIComponent(organizationId)}`; +function getGitHubAppReturnPath( + organizationId?: string, + installState?: string, + fromApp?: string +): string { + const params = new URLSearchParams(); + if (organizationId) params.set('organizationId', organizationId); + if (installState) params.set('installState', installState); + if (fromApp) params.set('fromApp', fromApp); + const query = params.toString(); + return query ? `/github-app?${query}` : '/github-app'; } export default async function GitHubAppPage({ @@ -35,7 +43,9 @@ export default async function GitHubAppPage({ searchParams: GitHubAppSearchParams; }) { const search = await searchParams; - const returnPath = getGitHubAppReturnPath(search.organizationId); + const isFromApp = search.fromApp === '1'; + const installState = search.installState; + const returnPath = getGitHubAppReturnPath(search.organizationId, installState, search.fromApp); await getUserFromAuthOrRedirect(`/users/sign_in?callbackPath=${encodeURIComponent(returnPath)}`); @@ -46,19 +56,23 @@ export default async function GitHubAppPage({

Kilo App

Connect GitHub

- Connect GitHub, then return to Kilo App to start a Cloud Agent session. + {isFromApp + ? 'Install the Kilo GitHub App, then return to the Kilo App.' + : 'Connect GitHub, then return to Kilo App to start a Cloud Agent session.'}

diff --git a/apps/web/src/components/integrations/GitHubIntegrationDetails.test.ts b/apps/web/src/components/integrations/GitHubIntegrationDetails.test.ts new file mode 100644 index 0000000000..05e67898ad --- /dev/null +++ b/apps/web/src/components/integrations/GitHubIntegrationDetails.test.ts @@ -0,0 +1,120 @@ +import { buildAppReturnOutcomeView } from './GitHubIntegrationDetails'; + +describe('GitHubIntegrationDetails fromApp outcome CTA behavior', () => { + it('success: Continue back to /cloud/sessions with github_install=success', () => { + expect(buildAppReturnOutcomeView({ success: true })).toEqual({ + kind: 'installed', + title: 'GitHub App installed', + description: 'Your repositories are now connected.', + cta: 'Continue', + href: '/cloud/sessions?github_install=success', + }); + }); + + it('pending: Done back to /cloud/sessions with github_pending_approval=true', () => { + expect(buildAppReturnOutcomeView({ pendingApproval: true })).toEqual({ + kind: 'pending', + title: 'Awaiting admin approval', + description: 'An organization admin must approve the installation request.', + cta: 'Done', + href: '/cloud/sessions?github_pending_approval=true', + }); + }); + + it('retryable failure: Try again with a return href that carries the error', () => { + expect(buildAppReturnOutcomeView({ error: 'installation_failed' })).toEqual({ + kind: 'retryable', + title: 'Installation failed', + description: 'The installation did not complete. Try again or return to the Kilo App.', + cta: 'Try again', + href: '/cloud/sessions?error=installation_failed', + }); + }); + + it('retryable unknown error: keeps the raw code in the return href', () => { + const view = buildAppReturnOutcomeView({ error: 'github_authorization_required' }); + expect(view.kind).toBe('retryable'); + expect(view.cta).toBe('Try again'); + expect(view.href).toBe('/cloud/sessions?error=github_authorization_required'); + }); + + it('non-retryable admin error: Back and no retry', () => { + expect(buildAppReturnOutcomeView({ error: 'not_installation_admin' })).toEqual({ + kind: 'blocked', + title: 'Cannot complete installation', + description: + 'Only a GitHub admin of that account can connect it. Ask an organization admin to install Kilo.', + cta: 'Back', + href: '/cloud/sessions?error=not_installation_admin', + }); + }); + + it('non-retryable claimed error: Back and no retry', () => { + const view = buildAppReturnOutcomeView({ error: 'installation_already_claimed' }); + expect(view.kind).toBe('blocked'); + expect(view.cta).toBe('Back'); + expect(view.href).toBe('/cloud/sessions?error=installation_already_claimed'); + }); + + it('non-retryable user mismatch: Back, no retry, mismatch copy preserved', () => { + const view = buildAppReturnOutcomeView({ error: 'install_state_user_mismatch' }); + expect(view.kind).toBe('blocked'); + expect(view.cta).toBe('Back'); + expect(view.description).toContain('different account'); + }); + + it('success takes precedence over error, and pending over error (callback ordering)', () => { + expect(buildAppReturnOutcomeView({ success: true, error: 'installation_failed' }).kind).toBe( + 'installed' + ); + expect( + buildAppReturnOutcomeView({ pendingApproval: true, error: 'installation_failed' }).kind + ).toBe('pending'); + }); + + it('org-scoped retryable failure: return href carries organizationId for app retry', () => { + expect( + buildAppReturnOutcomeView({ error: 'installation_failed', organizationId: 'org-123' }) + ).toEqual({ + kind: 'retryable', + title: 'Installation failed', + description: 'The installation did not complete. Try again or return to the Kilo App.', + cta: 'Try again', + href: '/cloud/sessions?error=installation_failed&organizationId=org-123', + }); + }); + + it('org-scoped success: return href carries organizationId', () => { + expect(buildAppReturnOutcomeView({ success: true, organizationId: 'org-123' }).href).toBe( + '/cloud/sessions?github_install=success&organizationId=org-123' + ); + }); + + it('org-scoped pending: return href carries organizationId', () => { + expect( + buildAppReturnOutcomeView({ pendingApproval: true, organizationId: 'org-123' }).href + ).toBe('/cloud/sessions?github_pending_approval=true&organizationId=org-123'); + }); + + it('org-scoped blocked: Back href carries organizationId, still no retry', () => { + expect( + buildAppReturnOutcomeView({ error: 'not_installation_admin', organizationId: 'org-123' }) + ).toEqual({ + kind: 'blocked', + title: 'Cannot complete installation', + description: + 'Only a GitHub admin of that account can connect it. Ask an organization admin to install Kilo.', + cta: 'Back', + href: '/cloud/sessions?error=not_installation_admin&organizationId=org-123', + }); + }); + + it('user-scoped outcomes omit organizationId from the return href', () => { + expect(buildAppReturnOutcomeView({ error: 'installation_failed' }).href).toBe( + '/cloud/sessions?error=installation_failed' + ); + expect(buildAppReturnOutcomeView({ success: true }).href).toBe( + '/cloud/sessions?github_install=success' + ); + }); +}); diff --git a/apps/web/src/components/integrations/GitHubIntegrationDetails.tsx b/apps/web/src/components/integrations/GitHubIntegrationDetails.tsx index ff2222fda0..5ebeec1573 100644 --- a/apps/web/src/components/integrations/GitHubIntegrationDetails.tsx +++ b/apps/web/src/components/integrations/GitHubIntegrationDetails.tsx @@ -18,7 +18,6 @@ import Link from 'next/link'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTRPC } from '@/lib/trpc/utils'; -import { useUser } from '@/hooks/useUser'; import { DevAddGitHubInstallationCard } from './DevAddGitHubInstallationCard'; import { useOrganizationWithMembers } from '@/app/api/organizations/hooks'; import { ModelCombobox, type ModelOption } from '@/components/shared/ModelCombobox'; @@ -28,6 +27,11 @@ import { useConfirm } from '@/components/ui/confirm'; type GitHubIntegrationDetailsProps = { organizationId?: string; + /** Pre-minted C1 install state token from the mobile app. When set, the + * component skips its own mint and passes the token directly to GitHub. */ + installState?: string; + /** True when the page was opened by the mobile app via /github-app?fromApp=1. */ + fromApp?: boolean; success?: boolean; userConnectionSuccess?: boolean; error?: string; @@ -37,8 +41,85 @@ type GitHubIntegrationDetailsProps = { onInstallationDetected?: () => void; }; +/** + * /cloud/sessions return href for an app-initiated install outcome. Carries + * the original organizationId so the mobile app can retry an org-scoped + * install on the same organization owner. + */ +function buildAppReturnHref(query: string, organizationId?: string): string { + const orgParam = organizationId ? `&organizationId=${encodeURIComponent(organizationId)}` : ''; + return `/cloud/sessions?${query}${orgParam}`; +} + +export type AppReturnOutcomeView = { + kind: 'installed' | 'pending' | 'blocked' | 'retryable'; + title: string; + description: string; + cta: string; + href: string; +}; + +/** + * View model for the fromApp outcome card. `blocked` (non-retryable) shows + * `Back` and never offers retry; `retryable` shows `Try again` (a real action + * wired by the component) plus `Return to Kilo App`, both via `href`. + */ +export function buildAppReturnOutcomeView(input: { + success?: boolean; + pendingApproval?: boolean; + error?: string; + organizationId?: string; +}): AppReturnOutcomeView { + const isSuccess = Boolean(input.success); + const isPending = !isSuccess && Boolean(input.pendingApproval); + const isNonRetryable = + input.error === 'install_state_user_mismatch' || + input.error === 'not_installation_admin' || + input.error === 'installation_already_claimed'; + const returnQuery = isSuccess + ? 'github_install=success' + : isPending + ? 'github_pending_approval=true' + : `error=${encodeURIComponent(input.error ?? 'installation_failed')}`; + const title = isSuccess + ? 'GitHub App installed' + : isPending + ? 'Awaiting admin approval' + : isNonRetryable + ? 'Cannot complete installation' + : 'Installation failed'; + const description = isSuccess + ? 'Your repositories are now connected.' + : isPending + ? 'An organization admin must approve the installation request.' + : input.error === 'not_installation_admin' + ? 'Only a GitHub admin of that account can connect it. Ask an organization admin to install Kilo.' + : input.error === 'installation_already_claimed' + ? 'That GitHub installation is already connected to another Kilo account. Disconnect it there first.' + : input.error === 'install_state_user_mismatch' + ? 'This connection was started from the Kilo App signed in as a different account. Sign in to the web with that account, or start again from the app.' + : 'The installation did not complete. Try again or return to the Kilo App.'; + const cta = isSuccess ? 'Continue' : isPending ? 'Done' : isNonRetryable ? 'Back' : 'Try again'; + + return { + kind: isSuccess + ? 'installed' + : isPending + ? 'pending' + : isNonRetryable + ? 'blocked' + : 'retryable', + title, + description, + cta, + href: buildAppReturnHref(returnQuery, input.organizationId), + }; +} + export function GitHubIntegrationDetails({ organizationId, + installState, + fromApp, success, userConnectionSuccess, error, @@ -51,6 +132,7 @@ export function GitHubIntegrationDetails({ const queryClient = useQueryClient(); const confirm = useConfirm(); const input = organizationId ? { organizationId } : undefined; + const hasAppOutcome = Boolean(success || pendingApproval || error); // Fetch organization data to check GitHub app type const { data: organizationData } = useOrganizationWithMembers(organizationId ?? '', { @@ -180,6 +262,8 @@ export function GitHubIntegrationDetails({ }) ); + const mintInstallState = useMutation(trpc.githubApps.mintInstallState.mutationOptions()); + // Initialize selected model from installation data useEffect(() => { if (installationData?.installation?.modelSlug) { @@ -227,17 +311,31 @@ export function GitHubIntegrationDetails({ description: `You already have a pending GitHub installation in another organization. Please complete or cancel that installation first.`, duration: 8000, }); + } else if (error === 'not_installation_admin') { + toast.error( + 'Only a GitHub admin of that account can connect it. Ask an organization admin to install Kilo.', + { duration: 8000 } + ); + } else if (error === 'installation_already_claimed') { + toast.error( + 'That GitHub installation is already connected to another Kilo account. Disconnect it there first.', + { duration: 8000 } + ); + } else if (error === 'github_authorization_required') { + toast.error('GitHub did not return an authorization. Start the connection again.', { + duration: 8000, + }); } else if (error === 'already_connected_to_another_account') { toast.error('This GitHub identity is already connected to another Kilo account.'); } else if (error === 'disconnect_existing_identity_first') { toast.error('Disconnect your current GitHub identity before connecting another account.'); + } else if (error === 'install_state_user_mismatch') { + // Handled by the fromApp fallback card — no toast needed. } else if (error) { toast.error(`GitHub connection failed: ${error}`); } }, [success, userConnectionSuccess, error, pendingApproval, existingPendingOrg]); - const { data: user } = useUser(); - const handleModelChange = (modelSlug: string) => { setSelectedModel(modelSlug); updateModel.mutate( @@ -261,20 +359,59 @@ export function GitHubIntegrationDetails({ ); }; - const handleInstall = () => { - const state = organizationId ? `org_${organizationId}` : `user_${user?.id}`; - const installUrl = `https://github.com/apps/${githubAppName}/installations/new?state=${encodeURIComponent(buildGitHubInstallState(state, appReturnPath))}`; - if (appReturnPath) { + const handleInstall = async () => { + try { + // Pre-minted state from the mobile app — use it directly without a + // second mint. The state token is the raw database token; it must be + // passed to GitHub exactly as received. + if (installState) { + const installUrl = `https://github.com/apps/${githubAppName}/installations/new?state=${encodeURIComponent(buildGitHubInstallState(installState))}`; + window.open(installUrl, '_blank', 'noopener,noreferrer'); + return; + } + + const result = await mintInstallState.mutateAsync({ + organizationId: organizationId ?? undefined, + returnTo: appReturnPath ?? undefined, + }); + const installUrl = `https://github.com/apps/${githubAppName}/installations/new?state=${encodeURIComponent(buildGitHubInstallState(result.token))}`; + if (appReturnPath) { + window.open(installUrl, '_blank', 'noopener,noreferrer'); + return; + } + window.location.href = installUrl; + } catch (err) { + toast.error('Failed to start GitHub installation', { + description: err instanceof Error ? err.message : 'Unknown error', + }); + } + }; + + /** + * App-initiated retry: mints a fresh C1 state. The original pre-minted + * token was consumed by the callback. A retry must never reuse it. + */ + const handleAppRetry = async () => { + try { + const result = await mintInstallState.mutateAsync({ + organizationId: organizationId ?? undefined, + returnTo: '/cloud/sessions', + }); + const installUrl = `https://github.com/apps/${githubAppName}/installations/new?state=${encodeURIComponent(buildGitHubInstallState(result.token))}`; window.open(installUrl, '_blank', 'noopener,noreferrer'); - return; + } catch (err) { + toast.error('Failed to start GitHub installation', { + description: err instanceof Error ? err.message : 'Unknown error', + }); } - window.location.href = installUrl; }; const handleConnectIdentity = () => { connectUserAuthorization.mutate(undefined, { onError: error => { - toast.error('Failed to start GitHub connection', { description: error.message }); + toast.error('Failed to start GitHub connection', { + description: error.message, + }); }, }); }; @@ -290,7 +427,9 @@ export function GitHubIntegrationDetails({ ) { disconnectUserAuthorization.mutate(undefined, { onError: error => { - toast.error('Failed to disconnect GitHub identity', { description: error.message }); + toast.error('Failed to disconnect GitHub identity', { + description: error.message, + }); }, }); } @@ -359,7 +498,7 @@ export function GitHubIntegrationDetails({ }); }; - if (isLoading) { + if (isLoading && !hasAppOutcome) { return appReturnPath ? (
@@ -382,7 +521,32 @@ export function GitHubIntegrationDetails({ const status = installation?.status; const isPendingApproval = status === 'awaiting_installation'; - if (appReturnPath && !isInstalled && !isPendingApproval) { + // Non-app mismatch landing: show corrective copy when the callback + // detected a user mismatch and redirects without fromApp=1. The + // fromApp fallback card below handles the app-initiated case. + if (error === 'install_state_user_mismatch' && !fromApp) { + return ( + + +
+ +
+

Account mismatch

+

+ This connection was started from the Kilo App signed in as a different account. Sign + in to the web with that account, or start again from the app. +

+
+ +
+
+
+ ); + } + + if (appReturnPath && !isInstalled && !isPendingApproval && !hasAppOutcome) { return (
- Open GitHub setup + {mintInstallState.isPending ? 'Starting installation...' : 'Open GitHub setup'}

@@ -459,6 +624,51 @@ export function GitHubIntegrationDetails({ ); } + if (fromApp && hasAppOutcome) { + const view = buildAppReturnOutcomeView({ success, pendingApproval, error, organizationId }); + const isRetryable = view.kind === 'retryable'; + + return ( + + +

+ {view.kind === 'installed' ? ( + + ) : view.kind === 'pending' ? ( + + ) : error === 'install_state_user_mismatch' ? ( + + ) : ( + + )} +
+

{view.title}

+

{view.description}

+
+ {isRetryable ? ( +
+ + +
+ ) : ( + + )} +
+ + + ); + } + return (
{/* Pending Approval Alert */} @@ -652,9 +862,16 @@ export function GitHubIntegrationDetails({ ) : ( - )} @@ -771,6 +988,117 @@ export function GitHubIntegrationDetails({ ) )} + {/* App-initiated flow: show outcome and "Return to Kilo App" after install + completes. The callback redirects to /cloud/sessions (claimed universal-link + route) but a server redirect does not always open the app. This fallback + stays visible so the user can tap the button — a user-initiated navigation + reliably triggers the universal link. */} + {fromApp && (isInstalled || isPendingApproval || hasAppOutcome) && ( + + +
+ {success || isInstalled ? ( + <> + +
+

GitHub App installed

+

+ Your repositories are now connected. Return to the Kilo App to continue. +

+
+ + + ) : pendingApproval || isPendingApproval ? ( + <> + +
+

Awaiting admin approval

+

+ An organization admin must approve the installation request. Return to the + Kilo App to check later. +

+
+ + + ) : error === 'install_state_user_mismatch' ? ( + <> + +
+

Account mismatch

+

+ This connection was started from the Kilo App signed in as a different + account. Sign in to the web with that account, or start again from the app. +

+
+ + + ) : error === 'not_installation_admin' || error === 'installation_already_claimed' ? ( + <> + +
+

Cannot complete installation

+

+ {error === 'not_installation_admin' + ? 'Only a GitHub admin of that account can connect it. Ask an organization admin to install Kilo.' + : 'That GitHub installation is already connected to another Kilo account. Disconnect it there first.'} +

+
+ + + ) : ( + <> + +
+

Installation failed

+

+ The installation did not complete. Try again or return to the Kilo App. +

+
+
+ + +
+ + )} +
+
+
+ )} + {/* Dev-only card for adding existing installations - only show when no app is installed */} {!isInstalled && !appReturnPath && ( diff --git a/apps/web/src/components/integrations/github-install-state.test.ts b/apps/web/src/components/integrations/github-install-state.test.ts index 3c9483f7cb..727323bfe7 100644 --- a/apps/web/src/components/integrations/github-install-state.test.ts +++ b/apps/web/src/components/integrations/github-install-state.test.ts @@ -3,13 +3,7 @@ import { describe, expect, test } from '@jest/globals'; import { buildGitHubInstallState } from './github-install-state'; describe('buildGitHubInstallState', () => { - test('keeps normal integration installs unchanged', () => { - expect(buildGitHubInstallState('user_123')).toBe('user_123'); - }); - - test('adds an encoded app-specific return path', () => { - expect(buildGitHubInstallState('org_123', '/github-app?organizationId=org_123')).toBe( - 'org_123|return=%2Fgithub-app%3ForganizationId%3Dorg_123' - ); + test('returns a bare database token', () => { + expect(buildGitHubInstallState('db-token-abc123')).toBe('db-token-abc123'); }); }); diff --git a/apps/web/src/components/integrations/github-install-state.ts b/apps/web/src/components/integrations/github-install-state.ts index 5ef11d0269..5e370d1ed9 100644 --- a/apps/web/src/components/integrations/github-install-state.ts +++ b/apps/web/src/components/integrations/github-install-state.ts @@ -1,3 +1,4 @@ -export function buildGitHubInstallState(ownerToken: string, returnTo?: string): string { - return returnTo ? `${ownerToken}|return=${encodeURIComponent(returnTo)}` : ownerToken; +/** Build the state parameter for a GitHub App installation URL. The return_to path is stored in the database row and the callback reads it from there. The state parameter carries only the bare database token. */ +export function buildGitHubInstallState(stateToken: string): string { + return stateToken; } diff --git a/apps/web/src/lib/auth/device-sessions.test.ts b/apps/web/src/lib/auth/device-sessions.test.ts new file mode 100644 index 0000000000..32bd4c2cf7 --- /dev/null +++ b/apps/web/src/lib/auth/device-sessions.test.ts @@ -0,0 +1,402 @@ +import { describe, test, expect, beforeEach, afterEach } from '@jest/globals'; +import { db } from '@/lib/drizzle'; +import { device_sessions, device_refresh_tokens, kilocode_users } from '@kilocode/db/schema'; +import { eq, getTableName, sql } from 'drizzle-orm'; +import { + createDeviceSession, + issueSessionCredentials, + rotateRefreshToken, + revokeDeviceSession, +} from './device-sessions'; +import type { User } from '@kilocode/db/schema'; + +describe('device-sessions', () => { + const testUserId = 'test-user-ds-' + Date.now(); + const testUserEmail = `test-ds-${Date.now()}@example.com`; + + let fakeUser: User; + + beforeEach(async () => { + await db.insert(kilocode_users).values({ + id: testUserId, + google_user_email: testUserEmail, + google_user_name: 'Test User', + google_user_image_url: 'https://example.com/avatar.jpg', + stripe_customer_id: 'cus_test', + }); + fakeUser = { + id: testUserId, + google_user_email: testUserEmail, + google_user_name: 'Test User', + google_user_image_url: 'https://example.com/avatar.jpg', + api_token_pepper: undefined, + } as unknown as User; + }); + + afterEach(async () => { + // Cascade from kilocode_users cleans device_sessions → device_refresh_tokens. + await db.delete(kilocode_users).where(eq(kilocode_users.id, testUserId)); + }); + + describe('createDeviceSession', () => { + test('creates a session and returns its ID', async () => { + const sessionId = await createDeviceSession({ + userId: testUserId, + userAgent: 'test-agent', + }); + + expect(typeof sessionId).toBe('string'); + expect(sessionId.length).toBeGreaterThan(0); + + const [row] = await db + .select() + .from(device_sessions) + .where(eq(device_sessions.id, sessionId)); + + expect(row).toBeDefined(); + expect(row!.kilo_user_id).toBe(testUserId); + expect(row!.user_agent).toBe('test-agent'); + expect(row!.revoked_at).toBeNull(); + }); + }); + + describe('issueSessionCredentials', () => { + test('returns a one-hour access token and a refresh token', async () => { + const sessionId = await createDeviceSession({ userId: testUserId }); + const result = await issueSessionCredentials(fakeUser, sessionId); + + expect(result.token).toBeDefined(); + expect(result.refreshToken).toBeDefined(); + expect(result.expiresIn).toBe(60 * 60); // one hour + + // Verify refresh token was stored + const tokens = await db + .select() + .from(device_refresh_tokens) + .where(eq(device_refresh_tokens.device_session_id, sessionId)); + + expect(tokens.length).toBe(1); + expect(new Date(tokens[0]!.expires_at).getTime()).toBeGreaterThan(Date.now()); + }); + }); + + describe('rotateRefreshToken', () => { + test('happy path: rotates and returns a new pair', async () => { + const sessionId = await createDeviceSession({ userId: testUserId }); + const { refreshToken } = await issueSessionCredentials(fakeUser, sessionId); + + const result = await rotateRefreshToken(refreshToken); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.token).toBeDefined(); + expect(result.refreshToken).toBeDefined(); + expect(result.refreshToken).not.toBe(refreshToken); + expect(result.expiresIn).toBe(60 * 60); + + // Old refresh token is consumed + const tokens = await db + .select() + .from(device_refresh_tokens) + .where(eq(device_refresh_tokens.device_session_id, sessionId)); + + const oldToken = tokens.find(t => t.consumed_at !== null); + expect(oldToken).toBeDefined(); + + // New refresh token exists + expect(tokens.length).toBe(2); + } + }); + + test('unknown refresh token returns INVALID_REFRESH_TOKEN without revocation', async () => { + const sessionId = await createDeviceSession({ userId: testUserId }); + await issueSessionCredentials(fakeUser, sessionId); + + const result = await rotateRefreshToken('bogus-token-12345'); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toBe('INVALID_REFRESH_TOKEN'); + } + + // Session must NOT be revoked + const [session] = await db + .select() + .from(device_sessions) + .where(eq(device_sessions.id, sessionId)); + expect(session!.revoked_at).toBeNull(); + }); + + test('reused refresh token revokes the session', async () => { + const sessionId = await createDeviceSession({ userId: testUserId }); + const { refreshToken } = await issueSessionCredentials(fakeUser, sessionId); + + // First rotation succeeds + const first = await rotateRefreshToken(refreshToken); + expect(first.ok).toBe(true); + + // Second use of the SAME refresh token must fail and revoke + const second = await rotateRefreshToken(refreshToken); + expect(second.ok).toBe(false); + if (!second.ok) { + expect(second.error).toBe('INVALID_REFRESH_TOKEN'); + } + + // Session must be revoked with the right reason + const [session] = await db + .select() + .from(device_sessions) + .where(eq(device_sessions.id, sessionId)); + expect(session!.revoked_at).not.toBeNull(); + expect(session!.revoked_reason).toBe('refresh_reuse_detected'); + }); + + test('refresh for a revoked session returns SESSION_REVOKED', async () => { + const sessionId = await createDeviceSession({ userId: testUserId }); + const { refreshToken } = await issueSessionCredentials(fakeUser, sessionId); + + await revokeDeviceSession(sessionId, 'test-revocation'); + + const result = await rotateRefreshToken(refreshToken); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toBe('SESSION_REVOKED'); + } + }); + + test('concurrent revocation during rotation is refused and issues no replacement pair', async () => { + const sessionId = await createDeviceSession({ userId: testUserId }); + const { refreshToken } = await issueSessionCredentials(fakeUser, sessionId); + + // Real concurrency: run the revoke as a separate transaction that takes + // the session row lock and stays open. Then start the rotation: its + // pre-transaction checks read the pre-revoke state, and its + // in-transaction `FOR UPDATE` recheck blocks on the held row lock. We + // commit the revoke only after the rotation is observed blocked, so the + // recheck under the lock is what must refuse and no replacement pair can + // be issued after the revoke wins. + let signalLockHeld: () => void = () => {}; + const lockHeld = new Promise(resolve => { + signalLockHeld = resolve; + }); + let releaseRevoke: () => void = () => {}; + const revokeGate = new Promise(resolve => { + releaseRevoke = resolve; + }); + + const revokeTx = db.transaction(async tx => { + await tx + .update(device_sessions) + .set({ + revoked_at: new Date().toISOString(), + revoked_reason: 'concurrent-revoke-test', + }) + .where(eq(device_sessions.id, sessionId)); + signalLockHeld(); + await revokeGate; + }); + + try { + // Wait until the revoke transaction holds the session row lock. + await lockHeld; + + // Start the rotation while the revoke is still uncommitted. + const rotatePromise = rotateRefreshToken(refreshToken); + + // Deterministic barrier: the rotation cannot pass its `FOR UPDATE` + // recheck until the revoke commits, so poll until the rotation is + // observed waiting on the session row lock inside its transaction. + let rotationBlocked = false; + for (let attempt = 0; attempt < 200 && !rotationBlocked; attempt++) { + const { + rows: [{ blocked }], + } = await db.execute<{ blocked: number }>(sql` + SELECT count(*)::int AS blocked + FROM pg_stat_activity + WHERE datname = current_database() + AND pid <> pg_backend_pid() + AND state = 'active' + AND wait_event_type = 'Lock' + AND query ILIKE '%device_sessions%' + `); + rotationBlocked = blocked > 0; + if (!rotationBlocked) { + await new Promise(resolve => setTimeout(resolve, 10)); + } + } + expect(rotationBlocked).toBe(true); + + // Commit the revoke; the rotation's locked recheck then refuses. + releaseRevoke(); + + const [result] = await Promise.all([rotatePromise, revokeTx]); + + // The rotation must refuse instead of issuing a replacement pair. + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toBe('SESSION_REVOKED'); + } + + // No replacement pair was issued: the old token is not consumed. + const tokens = await db + .select() + .from(device_refresh_tokens) + .where(eq(device_refresh_tokens.device_session_id, sessionId)); + expect(tokens).toHaveLength(1); + expect(tokens[0]!.consumed_at).toBeNull(); + + // The session is revoked. + const [session] = await db + .select() + .from(device_sessions) + .where(eq(device_sessions.id, sessionId)); + expect(session!.revoked_at).not.toBeNull(); + } finally { + // Always commit the revoke so the held row lock is released, even when + // an assertion fails. This keeps the afterEach cleanup unblocked. + releaseRevoke(); + await revokeTx.catch(() => {}); + } + }); + + test('refresh for a blocked user returns USER_BLOCKED', async () => { + const sessionId = await createDeviceSession({ userId: testUserId }); + const { refreshToken } = await issueSessionCredentials(fakeUser, sessionId); + + // Block the user + await db + .update(kilocode_users) + .set({ blocked_reason: 'manual block' }) + .where(eq(kilocode_users.id, testUserId)); + + const result = await rotateRefreshToken(refreshToken); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toBe('USER_BLOCKED'); + } + }); + + test('unblock and retry: blocked refresh does not consume token, retry succeeds after unblock', async () => { + const sessionId = await createDeviceSession({ userId: testUserId }); + const { refreshToken } = await issueSessionCredentials(fakeUser, sessionId); + + // Block the user + await db + .update(kilocode_users) + .set({ blocked_reason: 'manual block' }) + .where(eq(kilocode_users.id, testUserId)); + + // Attempt refresh while blocked — must refuse but NOT consume + const blocked = await rotateRefreshToken(refreshToken); + expect(blocked.ok).toBe(false); + if (!blocked.ok) { + expect(blocked.error).toBe('USER_BLOCKED'); + } + + // Verify the token is NOT consumed + const [tokenAfterBlock] = await db + .select() + .from(device_refresh_tokens) + .where(eq(device_refresh_tokens.device_session_id, sessionId)); + expect(tokenAfterBlock!.consumed_at).toBeNull(); + + // Unblock the user + await db + .update(kilocode_users) + .set({ blocked_reason: null }) + .where(eq(kilocode_users.id, testUserId)); + + // Retry with the same token — must succeed + const unblocked = await rotateRefreshToken(refreshToken); + expect(unblocked.ok).toBe(true); + if (unblocked.ok) { + expect(unblocked.token).toBeDefined(); + expect(unblocked.refreshToken).not.toBe(refreshToken); + } + + // Verify session is NOT revoked (no false reuse detection) + const [session] = await db + .select() + .from(device_sessions) + .where(eq(device_sessions.id, sessionId)); + expect(session!.revoked_at).toBeNull(); + }); + + test('each rotation issues a fresh 30-day refresh token', async () => { + const sessionId = await createDeviceSession({ userId: testUserId }); + const { refreshToken } = await issueSessionCredentials(fakeUser, sessionId); + + const result = await rotateRefreshToken(refreshToken); + expect(result.ok).toBe(true); + if (result.ok) { + // The new refresh token must have a 30-day expiry + const tokens = await db + .select() + .from(device_refresh_tokens) + .where(eq(device_refresh_tokens.device_session_id, sessionId)); + + const newToken = tokens.find(t => t.consumed_at === null); + expect(newToken).toBeDefined(); + if (newToken) { + const expiresIn = new Date(newToken.expires_at).getTime() - Date.now(); + const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000; + expect(expiresIn).toBeGreaterThan(thirtyDaysMs - 5000); // within 5s + expect(expiresIn).toBeLessThan(thirtyDaysMs + 5000); + } + } + }); + + test('replacement issuance failure rolls back the consume and keeps the old token usable', async () => { + const sessionId = await createDeviceSession({ userId: testUserId }); + const { refreshToken } = await issueSessionCredentials(fakeUser, sessionId); + + // Force the replacement refresh-token insert to fail inside the rotation + // transaction. The real transaction then rolls back the consume. + const originalTransaction = db.transaction.bind(db); + const transactionSpy = jest.spyOn(db, 'transaction').mockImplementation((async ( + callback: (tx: unknown) => unknown + ) => { + return originalTransaction(async tx => { + const originalInsert = tx.insert.bind(tx); + tx.insert = table => { + if (getTableName(table) === getTableName(device_refresh_tokens)) { + throw new Error('synthetic refresh issuance failure'); + } + return originalInsert(table); + }; + return callback(tx); + }); + }) as unknown as typeof db.transaction); + + try { + await expect(rotateRefreshToken(refreshToken)).rejects.toThrow( + 'synthetic refresh issuance failure' + ); + } finally { + transactionSpy.mockRestore(); + } + + // The old token must NOT be consumed by the failed rotation. + const tokensAfterFailure = await db + .select() + .from(device_refresh_tokens) + .where(eq(device_refresh_tokens.device_session_id, sessionId)); + expect(tokensAfterFailure).toHaveLength(1); + expect(tokensAfterFailure[0]!.consumed_at).toBeNull(); + + // The session must not be revoked by the failed rotation. + const [sessionAfterFailure] = await db + .select() + .from(device_sessions) + .where(eq(device_sessions.id, sessionId)); + expect(sessionAfterFailure!.revoked_at).toBeNull(); + + // The same refresh token must now rotate successfully — proves recovery. + const retry = await rotateRefreshToken(refreshToken); + expect(retry.ok).toBe(true); + if (retry.ok) { + expect(retry.refreshToken).not.toBe(refreshToken); + } + }); + }); +}); diff --git a/apps/web/src/lib/auth/device-sessions.ts b/apps/web/src/lib/auth/device-sessions.ts new file mode 100644 index 0000000000..8d4f9dab18 --- /dev/null +++ b/apps/web/src/lib/auth/device-sessions.ts @@ -0,0 +1,327 @@ +import 'server-only'; +import { db } from '@/lib/drizzle'; +import { device_sessions, device_refresh_tokens, kilocode_users } from '@kilocode/db/schema'; +import type { User } from '@kilocode/db/schema'; +import { eq, and, isNull, gt } from 'drizzle-orm'; +import { generateApiToken, TOKEN_EXPIRY } from '@/lib/tokens'; +import { createHash, randomBytes } from 'node:crypto'; +import { persistAttestedKeyTx, type VerifyAdmissionOk } from './native-admission'; + +const REFRESH_TOKEN_BYTES = 32; + +function hashToken(token: string): string { + return createHash('sha256').update(token).digest('hex'); +} + +function generateRefreshToken(): string { + return randomBytes(REFRESH_TOKEN_BYTES).toString('base64url'); +} + +/** + * Create a new device session. + * @returns the created session ID. + */ +export async function createDeviceSession(params: { + userId: string; + userAgent?: string; + deviceAuthRequestId?: string; +}): Promise { + const [session] = await db + .insert(device_sessions) + .values({ + kilo_user_id: params.userId, + user_agent: params.userAgent, + device_auth_request_id: params.deviceAuthRequestId, + }) + .returning({ id: device_sessions.id }); + + if (!session) { + throw new Error('Failed to create device session'); + } + + return session.id; +} + +/** + * Issue an access token + refresh token pair for a device session. + * The access token is short-lived (one hour). The refresh token is 30 days. + */ +export async function issueSessionCredentials( + user: User, + deviceSessionId: string +): Promise<{ token: string; refreshToken: string; expiresIn: number }> { + const accessToken = generateApiToken( + user, + { deviceSessionId }, + { expiresIn: TOKEN_EXPIRY.oneHour } + ); + + const refreshToken = generateRefreshToken(); + const tokenHash = hashToken(refreshToken); + const expiresAt = new Date(Date.now() + TOKEN_EXPIRY.thirtyDays * 1000).toISOString(); + + await db.insert(device_refresh_tokens).values({ + token_hash: tokenHash, + device_session_id: deviceSessionId, + expires_at: expiresAt, + }); + + return { + token: accessToken, + refreshToken, + expiresIn: TOKEN_EXPIRY.oneHour, + }; +} + +/** + * Rotate a refresh token. On success, returns a new token pair. + * On failure, returns null with an error code. + * + * Precondition checks (session validity, user blocked_reason) happen BEFORE + * the atomic consume, so a blocked refusal does not consume the token. + * Reuse-detection: if the refresh token was already consumed (consumed_at is set), + * the session is revoked because replay implies theft. + * An unknown or expired token is refused without revocation. + * + * The session validity check is repeated INSIDE the transaction under a row + * lock, so a session revoked concurrently with the rotation cannot receive a + * replacement pair. + * + * The consume and the replacement issuance run in one transaction. If issuing + * the replacement fails, the transaction rolls back so the old token stays + * usable instead of leaving the client with a permanently dead refresh path. + */ +export async function rotateRefreshToken( + refreshToken: string +): Promise< + | { ok: true; token: string; refreshToken: string; expiresIn: number } + | { ok: false; error: 'INVALID_REFRESH_TOKEN' | 'SESSION_REVOKED' | 'USER_BLOCKED' } +> { + const tokenHash = hashToken(refreshToken); + const now = new Date().toISOString(); + + // Step 1: Look up the token row to check validity and reuse BEFORE consuming. + const [row] = await db + .select() + .from(device_refresh_tokens) + .where(eq(device_refresh_tokens.token_hash, tokenHash)) + .limit(1); + + // Unknown token: refuse without revocation. + if (!row) { + return { ok: false, error: 'INVALID_REFRESH_TOKEN' }; + } + + // Expired token: refuse without revocation. + if (new Date(row.expires_at) <= new Date()) { + return { ok: false, error: 'INVALID_REFRESH_TOKEN' }; + } + + // Reused (consumed_at is set): refuse AND revoke the session. + if (row.consumed_at) { + await db + .update(device_sessions) + .set({ + revoked_at: now, + revoked_reason: 'refresh_reuse_detected', + }) + .where(eq(device_sessions.id, row.device_session_id)); + return { ok: false, error: 'INVALID_REFRESH_TOKEN' }; + } + + // Step 2: Check parent session validity BEFORE consuming. + const [session] = await db + .select() + .from(device_sessions) + .where(eq(device_sessions.id, row.device_session_id)) + .limit(1); + + if (!session) { + return { ok: false, error: 'INVALID_REFRESH_TOKEN' }; + } + + if (session.revoked_at) { + return { ok: false, error: 'SESSION_REVOKED' }; + } + + // Step 3: Check user blocked_reason BEFORE consuming. + const [user] = await db + .select({ id: kilocode_users.id, blocked_reason: kilocode_users.blocked_reason }) + .from(kilocode_users) + .where(eq(kilocode_users.id, session.kilo_user_id)) + .limit(1); + + if (!user) { + return { ok: false, error: 'INVALID_REFRESH_TOKEN' }; + } + + if (user.blocked_reason) { + return { ok: false, error: 'USER_BLOCKED' }; + } + + // Steps 4-8: Consume the old token and issue the replacement pair in one + // transaction. If issuing the replacement fails, the transaction rolls back + // and the old token remains usable, so the client can retry with it. + const outcome = await db.transaction(async tx => { + // Fetch the full user for token generation BEFORE consuming, so a missing + // user refuses without burning the token. + const [fullUser] = await tx + .select() + .from(kilocode_users) + .where(eq(kilocode_users.id, session.kilo_user_id)) + .limit(1); + + if (!fullUser) { + return { kind: 'missing_user' } as const; + } + + // Step 4: Lock the parent session row and recheck revoked_at INSIDE the + // transaction, before consuming. The pre-transaction session check above + // can be raced by a revokeDeviceSession that commits between that read and + // this point; holding the session row lock until this transaction commits + // closes the gap — the revoke either commits first and this recheck refuses, + // or it blocks until this rotation commits. + const [lockedSession] = await tx + .select() + .from(device_sessions) + .where(eq(device_sessions.id, session.id)) + .for('update') + .limit(1); + + if (!lockedSession) { + return { kind: 'missing_session' } as const; + } + + if (lockedSession.revoked_at) { + return { kind: 'session_revoked' } as const; + } + + // Step 5: Atomic consume — only after the in-transaction revocation check. + const [consumed] = await tx + .update(device_refresh_tokens) + .set({ consumed_at: now }) + .where( + and( + eq(device_refresh_tokens.token_hash, tokenHash), + isNull(device_refresh_tokens.consumed_at), + gt(device_refresh_tokens.expires_at, now) + ) + ) + .returning(); + + // Step 6: If consume failed, a concurrent request consumed the token. + // We already confirmed the token was not reused at Step 1, so this is + // a race, not theft. Refuse without revocation. + if (!consumed) { + return { kind: 'consume_lost' } as const; + } + + // Step 7: Update last_seen_at on the session. + await tx + .update(device_sessions) + .set({ last_seen_at: now }) + .where(eq(device_sessions.id, consumed.device_session_id)); + + // Step 8: Issue the replacement pair in the same transaction. + const accessToken = generateApiToken( + fullUser, + { deviceSessionId: lockedSession.id }, + { expiresIn: TOKEN_EXPIRY.oneHour } + ); + + const newRefreshToken = generateRefreshToken(); + const newExpiresAt = new Date(Date.now() + TOKEN_EXPIRY.thirtyDays * 1000).toISOString(); + + await tx.insert(device_refresh_tokens).values({ + token_hash: hashToken(newRefreshToken), + device_session_id: lockedSession.id, + expires_at: newExpiresAt, + }); + + return { + kind: 'ok', + pair: { token: accessToken, refreshToken: newRefreshToken, expiresIn: TOKEN_EXPIRY.oneHour }, + } as const; + }); + + if (outcome.kind === 'session_revoked') { + return { ok: false, error: 'SESSION_REVOKED' }; + } + + if (outcome.kind !== 'ok') { + return { ok: false, error: 'INVALID_REFRESH_TOKEN' }; + } + + return { ok: true, ...outcome.pair }; +} + +/** + * Create a device session and persist the attested key in a single transaction. + * + * Bind key persistence and session creation atomically so that a partial failure + * never leaves one committed without the other when supportsRefresh is true. + * + * Returns the session ID and credential pair on success. + * Throws KeyCollisionError if the key already belongs to a different user. + */ +export async function createDeviceSessionWithAttestedKey(params: { + userId: string; + userAgent?: string; + user: User; + verification: VerifyAdmissionOk; +}): Promise<{ token: string; refreshToken: string; expiresIn: number; sessionId: string }> { + return await db.transaction(async tx => { + // Persist the attested key inside the transaction + await persistAttestedKeyTx(tx, params.userId, params.verification); + + // Create the device session + const [session] = await tx + .insert(device_sessions) + .values({ + kilo_user_id: params.userId, + user_agent: params.userAgent, + }) + .returning({ id: device_sessions.id }); + + if (!session) { + throw new Error('Failed to create device session'); + } + + // Issue credentials + const accessToken = generateApiToken( + params.user, + { deviceSessionId: session.id }, + { expiresIn: TOKEN_EXPIRY.oneHour } + ); + + const refreshToken = generateRefreshToken(); + const tokenHash = hashToken(refreshToken); + const expiresAt = new Date(Date.now() + TOKEN_EXPIRY.thirtyDays * 1000).toISOString(); + + await tx.insert(device_refresh_tokens).values({ + token_hash: tokenHash, + device_session_id: session.id, + expires_at: expiresAt, + }); + + return { + token: accessToken, + refreshToken, + expiresIn: TOKEN_EXPIRY.oneHour, + sessionId: session.id, + }; + }); +} + +/** + * Revoke a device session and all its refresh tokens. + */ +export async function revokeDeviceSession(sessionId: string, reason: string): Promise { + await db + .update(device_sessions) + .set({ + revoked_at: new Date().toISOString(), + revoked_reason: reason, + }) + .where(eq(device_sessions.id, sessionId)); +} diff --git a/apps/web/src/lib/auth/magic-link-tokens.test.ts b/apps/web/src/lib/auth/magic-link-tokens.test.ts index a189930682..7d6bfb2d84 100644 --- a/apps/web/src/lib/auth/magic-link-tokens.test.ts +++ b/apps/web/src/lib/auth/magic-link-tokens.test.ts @@ -2,16 +2,40 @@ import { describe, it, expect, beforeEach } from '@jest/globals'; import { createMagicLinkToken, createSignInCode, + consumeSignInCode, + commitSignInCode, deleteSignInCode, getMagicLinkUrl, + releaseSignInCode, + reserveSignInCode, verifyAndConsumeMagicLinkToken, - verifyAndConsumeSignInCode, } from './magic-link-tokens'; import { db } from '@/lib/drizzle'; import { sql, eq, and } from 'drizzle-orm'; import { magic_link_tokens } from '@kilocode/db/schema'; import { createHash } from 'crypto'; +/** + * Reserve then immediately commit a sign-in code — the no-settlement shape the + * production route no longer uses. Kept here so the reserve/commit/release + * contract stays covered without shipping a caller-less wrapper. + */ +async function verifyAndConsumeSignInCode( + email: string, + code: string, + challengeId?: string +): Promise<'ok' | 'invalid' | 'too_many_attempts'> { + const result = await reserveSignInCode(email, code, challengeId); + if (result !== 'ok') { + return result === 'in_progress' ? 'invalid' : result; + } + if (!(await commitSignInCode(email, code, challengeId))) { + await releaseSignInCode(email, code, challengeId); + return 'invalid'; + } + return 'ok'; +} + describe('Magic Link Tokens', () => { const testEmail = 'test@example.com'; @@ -123,10 +147,14 @@ describe('Magic Link Tokens', () => { return rows[0]; }; - it('returns a 6-digit zero-padded code and stores its hash', async () => { - const code = await createSignInCode(testEmail); + it('returns a 6-digit zero-padded code, stores its hash, and returns a challenge ID', async () => { + const { code, challengeId } = await createSignInCode(testEmail); expect(code).toMatch(/^\d{6}$/); + expect(challengeId).toBeDefined(); + expect(challengeId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + ); const row = await rowFor(testEmail); expect(row).toBeDefined(); @@ -136,6 +164,7 @@ describe('Magic Link Tokens', () => { expect(row?.purpose).toBe('sign_in_code'); expect(row?.consumed_at).toBeNull(); expect(row?.attempts).toBe(0); + expect(row?.challenge_id).toBe(challengeId); }); it('lowercases the email before hashing and storing', async () => { @@ -151,11 +180,17 @@ describe('Magic Link Tokens', () => { }); it('uses one live code and attempt budget for aliases of the same mailbox', async () => { - const firstCode = await createSignInCode('te.st+first@gmail.com'); - const secondCode = await createSignInCode('test@gmail.com'); - - expect(await verifyAndConsumeSignInCode('te.st+first@gmail.com', firstCode)).toBe('invalid'); - expect(await verifyAndConsumeSignInCode('test+second@googlemail.com', secondCode)).toBe('ok'); + const { code: firstCode, challengeId: firstChallenge } = + await createSignInCode('te.st+first@gmail.com'); + const { code: secondCode, challengeId: secondChallenge } = + await createSignInCode('test@gmail.com'); + + expect( + await verifyAndConsumeSignInCode('te.st+first@gmail.com', firstCode, firstChallenge) + ).toBe('invalid'); + expect( + await verifyAndConsumeSignInCode('test+second@googlemail.com', secondCode, secondChallenge) + ).toBe('ok'); }); it('sets an expiry approximately 10 minutes out', async () => { @@ -169,7 +204,7 @@ describe('Magic Link Tokens', () => { it('deletes prior unconsumed rows for the email so only one code is live', async () => { await createSignInCode(testEmail); - const secondCode = await createSignInCode(testEmail); + const { code: secondCode, challengeId: secondChallenge } = await createSignInCode(testEmail); const rows = await db .select() @@ -177,12 +212,12 @@ describe('Magic Link Tokens', () => { .where(eq(magic_link_tokens.email, testEmail)); expect(rows).toHaveLength(1); - expect(await verifyAndConsumeSignInCode(testEmail, secondCode)).toBe('ok'); + expect(await verifyAndConsumeSignInCode(testEmail, secondCode, secondChallenge)).toBe('ok'); }); it('does not delete already-consumed rows for the email', async () => { - const firstCode = await createSignInCode(testEmail); - await verifyAndConsumeSignInCode(testEmail, firstCode); + const { code: firstCode, challengeId: firstChallenge } = await createSignInCode(testEmail); + await verifyAndConsumeSignInCode(testEmail, firstCode, firstChallenge); await createSignInCode(testEmail); @@ -195,17 +230,17 @@ describe('Magic Link Tokens', () => { it('does not delete or select browser magic-link tokens', async () => { const magicLink = await createMagicLinkToken(testEmail); - const code = await createSignInCode(testEmail); + const { code, challengeId } = await createSignInCode(testEmail); - expect(await verifyAndConsumeSignInCode(testEmail, code)).toBe('ok'); + expect(await verifyAndConsumeSignInCode(testEmail, code, challengeId)).toBe('ok'); expect(await verifyAndConsumeMagicLinkToken(magicLink.plaintext_token)).not.toBeNull(); }); it('serializes concurrent issuance so only the newest code remains live', async () => { - const [firstCode, secondCode] = await Promise.all([ - createSignInCode(testEmail), - createSignInCode(testEmail), - ]); + const [ + { code: firstCode, challengeId: firstChallenge }, + { code: secondCode, challengeId: secondChallenge }, + ] = await Promise.all([createSignInCode(testEmail), createSignInCode(testEmail)]); const rows = await db .select() .from(magic_link_tokens) @@ -215,8 +250,8 @@ describe('Magic Link Tokens', () => { expect(rows).toHaveLength(1); const results = await Promise.all([ - verifyAndConsumeSignInCode(testEmail, firstCode), - verifyAndConsumeSignInCode(testEmail, secondCode), + verifyAndConsumeSignInCode(testEmail, firstCode, firstChallenge), + verifyAndConsumeSignInCode(testEmail, secondCode, secondChallenge), ]); expect(results.toSorted()).toEqual(['invalid', 'ok']); }); @@ -224,9 +259,9 @@ describe('Magic Link Tokens', () => { describe('verifyAndConsumeSignInCode', () => { it('consumes a correct code and returns ok', async () => { - const code = await createSignInCode(testEmail); + const { code, challengeId } = await createSignInCode(testEmail); - const result = await verifyAndConsumeSignInCode(testEmail, code); + const result = await verifyAndConsumeSignInCode(testEmail, code, challengeId); expect(result).toBe('ok'); const rows = await db @@ -237,15 +272,15 @@ describe('Magic Link Tokens', () => { }); it('is case-insensitive on email', async () => { - const code = await createSignInCode('Test@Example.com'); - const result = await verifyAndConsumeSignInCode('TEST@EXAMPLE.COM', code); + const { code, challengeId } = await createSignInCode('Test@Example.com'); + const result = await verifyAndConsumeSignInCode('TEST@EXAMPLE.COM', code, challengeId); expect(result).toBe('ok'); }); it('increments attempts and returns invalid on wrong code', async () => { - await createSignInCode(testEmail); + const { challengeId } = await createSignInCode(testEmail); - const result = await verifyAndConsumeSignInCode(testEmail, '000000'); + const result = await verifyAndConsumeSignInCode(testEmail, '000000', challengeId); expect(result).toBe('invalid'); const rows = await db @@ -257,20 +292,20 @@ describe('Magic Link Tokens', () => { }); it('returns too_many_attempts on the 6th attempt even with the correct code', async () => { - const code = await createSignInCode(testEmail); + const { code, challengeId } = await createSignInCode(testEmail); const wrongCode = code === '000000' ? '111111' : '000000'; for (let i = 0; i < 5; i++) { - const result = await verifyAndConsumeSignInCode(testEmail, wrongCode); + const result = await verifyAndConsumeSignInCode(testEmail, wrongCode, challengeId); expect(result).toBe('invalid'); } - const result = await verifyAndConsumeSignInCode(testEmail, code); + const result = await verifyAndConsumeSignInCode(testEmail, code, challengeId); expect(result).toBe('too_many_attempts'); }); it('never consumes a correct code once the attempt budget is exceeded (racing increments)', async () => { - const code = await createSignInCode(testEmail); + const { code, challengeId } = await createSignInCode(testEmail); // Simulate concurrent wrong guesses racing the pre-check past the // budget: force attempts beyond the max directly. await db @@ -278,7 +313,7 @@ describe('Magic Link Tokens', () => { .set({ attempts: 6 }) .where(eq(magic_link_tokens.email, testEmail)); - const result = await verifyAndConsumeSignInCode(testEmail, code); + const result = await verifyAndConsumeSignInCode(testEmail, code, challengeId); expect(result).not.toBe('ok'); expect(result).toBe('too_many_attempts'); @@ -308,11 +343,11 @@ describe('Magic Link Tokens', () => { }); it('returns invalid for an already-consumed code', async () => { - const code = await createSignInCode(testEmail); - const first = await verifyAndConsumeSignInCode(testEmail, code); + const { code, challengeId } = await createSignInCode(testEmail); + const first = await verifyAndConsumeSignInCode(testEmail, code, challengeId); expect(first).toBe('ok'); - const second = await verifyAndConsumeSignInCode(testEmail, code); + const second = await verifyAndConsumeSignInCode(testEmail, code, challengeId); expect(second).toBe('invalid'); }); @@ -322,14 +357,437 @@ describe('Magic Link Tokens', () => { }); }); + describe('reserveSignInCode / commitSignInCode / releaseSignInCode', () => { + it('reserves a code without consuming it', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + + const result = await reserveSignInCode(testEmail, code, challengeId); + expect(result).toBe('ok'); + + const [row] = await db + .select() + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(row?.consumed_at).toBeNull(); + expect(row?.reserved_until).not.toBeNull(); + }); + + it('commits a reserved code', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + const reserveResult = await reserveSignInCode(testEmail, code, challengeId); + expect(reserveResult).toBe('ok'); + + const committed = await commitSignInCode(testEmail, code, challengeId); + expect(committed).toBe(true); + + const [row] = await db + .select() + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(row?.consumed_at).not.toBeNull(); + }); + + it('releases a reserved code without incrementing attempts', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + await reserveSignInCode(testEmail, code, challengeId); + + // Verify a wrong guess does NOT increment attempts + const [before] = await db + .select({ attempts: magic_link_tokens.attempts }) + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(before?.attempts).toBe(0); + + await releaseSignInCode(testEmail, code, challengeId); + + const [after] = await db + .select({ + attempts: magic_link_tokens.attempts, + reserved_until: magic_link_tokens.reserved_until, + }) + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(after?.attempts).toBe(0); + expect(after?.reserved_until).toBeNull(); + }); + + it('released code remains usable (failed settlement does not cost attempts)', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + await reserveSignInCode(testEmail, code, challengeId); + await releaseSignInCode(testEmail, code, challengeId); + + // Code should be usable again + const result = await reserveSignInCode(testEmail, code, challengeId); + expect(result).toBe('ok'); + + // And should not have any attempts consumed + const [row] = await db + .select({ attempts: magic_link_tokens.attempts }) + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(row?.attempts).toBe(0); + }); + + it('blocks a second reservation on the same code with in_progress', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + + const first = await reserveSignInCode(testEmail, code, challengeId); + expect(first).toBe('ok'); + + const second = await reserveSignInCode(testEmail, code, challengeId); + expect(second).toBe('in_progress'); + + // The code remains unconsumed + const [row] = await db + .select() + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(row?.consumed_at).toBeNull(); + }); + + it('two concurrent reservations yield exactly one ok', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + + const results = await Promise.all([ + reserveSignInCode(testEmail, code, challengeId), + reserveSignInCode(testEmail, code, challengeId), + ]); + const sorted = results.toSorted(); + expect(sorted).toEqual(['in_progress', 'ok']); + }); + + it('reservation becomes usable after expiry', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + + await reserveSignInCode(testEmail, code, challengeId); + + // Force the reservation to expire + await db + .update(magic_link_tokens) + .set({ reserved_until: new Date(Date.now() - 60_000).toISOString() }) + .where(eq(magic_link_tokens.email, testEmail)); + + const result = await reserveSignInCode(testEmail, code, challengeId); + expect(result).toBe('ok'); + }); + + it('commitSignInCode fails after reservation expires', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + await reserveSignInCode(testEmail, code, challengeId); + + // Force the reservation to expire + await db + .update(magic_link_tokens) + .set({ reserved_until: new Date(Date.now() - 60_000).toISOString() }) + .where(eq(magic_link_tokens.email, testEmail)); + + const committed = await commitSignInCode(testEmail, code, challengeId); + expect(committed).toBe(false); + + // Code should still be unconsumed + const [row] = await db + .select() + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(row?.consumed_at).toBeNull(); + }); + + it('consumeSignInCode works without a reservation', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + await reserveSignInCode(testEmail, code, challengeId); + + // Force the reservation to expire + await db + .update(magic_link_tokens) + .set({ reserved_until: new Date(Date.now() - 60_000).toISOString() }) + .where(eq(magic_link_tokens.email, testEmail)); + + // consumeSignInCode should still work without a live reservation + const consumed = await consumeSignInCode(testEmail, code, challengeId); + expect(consumed).toBe(true); + + const [row] = await db + .select() + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(row?.consumed_at).not.toBeNull(); + }); + + it('consumeSignInCode prevents a second consume', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + await reserveSignInCode(testEmail, code, challengeId); + + const first = await consumeSignInCode(testEmail, code, challengeId); + expect(first).toBe(true); + + const second = await consumeSignInCode(testEmail, code, challengeId); + expect(second).toBe(false); + }); + + describe('challenge-keyed budget isolation', () => { + it('five wrong guesses against challenge A do not spend challenge B budget', async () => { + const { code: codeA, challengeId: challengeA } = await createSignInCode(testEmail); + const { code: codeB, challengeId: challengeB } = + await createSignInCode('other@example.com'); + + // Spend challenge A's budget with wrong guesses. + const wrongCode = codeA === '000000' ? '111111' : '000000'; + for (let i = 0; i < 5; i++) { + const result = await reserveSignInCode(testEmail, wrongCode, challengeA); + expect(result).toBe('invalid'); + } + + // Challenge A is now exhausted. + const exhausted = await reserveSignInCode(testEmail, wrongCode, challengeA); + expect(exhausted).toBe('too_many_attempts'); + + // Challenge B is untouched — a wrong guess succeeds (returns 'invalid', + // not 'too_many_attempts', and doesn't find a row since there's a + // different challenge). + const bResult = await reserveSignInCode('other@example.com', wrongCode, challengeB); + expect(bResult).toBe('invalid'); + + // The correct code for B still verifies through verifyAndConsumeSignInCode + // using the challenge. + const verifyResult = await verifyAndConsumeSignInCode( + 'other@example.com', + codeB, + challengeB + ); + expect(verifyResult).toBe('ok'); + }); + + it('commitSignInCode scoped to challenge', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + await reserveSignInCode(testEmail, code, challengeId); + + // Commit with the right challenge succeeds. + const committed = await commitSignInCode(testEmail, code, challengeId); + expect(committed).toBe(true); + }); + + it('commitSignInCode with wrong challenge fails', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + await reserveSignInCode(testEmail, code, challengeId); + + // Commit with a different challenge fails. + const committed = await commitSignInCode( + testEmail, + code, + '00000000-0000-0000-0000-000000000000' + ); + expect(committed).toBe(false); + + // Code should still be unconsumed. + const [row] = await db + .select() + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(row?.consumed_at).toBeNull(); + }); + + it('releaseSignInCode scoped to challenge', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + await reserveSignInCode(testEmail, code, challengeId); + + await releaseSignInCode(testEmail, code, challengeId); + + const [row] = await db + .select({ + reserved_until: magic_link_tokens.reserved_until, + }) + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(row?.reserved_until).toBeNull(); + }); + + it('consumeSignInCode scoped to challenge', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + + const consumed = await consumeSignInCode(testEmail, code, challengeId); + expect(consumed).toBe(true); + + const [row] = await db + .select() + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(row?.consumed_at).not.toBeNull(); + }); + + it('challengeId is a UUID not derivable from the email or code', async () => { + const first = await createSignInCode(testEmail); + const second = await createSignInCode(testEmail); + + // Challenge IDs are unique across issuances. + expect(first.challengeId).not.toBe(second.challengeId); + // Challenge IDs are not the email. + expect(first.challengeId).not.toBe(testEmail); + // Challenge IDs are not the code. + expect(first.challengeId).not.toBe(first.code); + }); + + it('R6: no-challenge wrong guesses spend the challenge-bound row budget (same email)', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + const wrongCode = code === '000000' ? '111111' : '000000'; + + // Five wrong guesses without challengeId — the legacy email-keyed path + // finds the challenge-bearing row and increments its attempts. + for (let i = 0; i < 5; i++) { + const result = await reserveSignInCode(testEmail, wrongCode); + expect(result).toBe('invalid'); + } + + // The challenge-bound row has spent its whole budget. + const [row] = await db + .select({ attempts: magic_link_tokens.attempts }) + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(row?.attempts).toBe(5); + + // The correct code with the real challengeId is now locked out. + const verifyResult = await verifyAndConsumeSignInCode(testEmail, code, challengeId); + expect(verifyResult).toBe('too_many_attempts'); + }); + + it('legacy no-challenge caller can settle a challenge-bound row by email', async () => { + const { code } = await createSignInCode(testEmail); + + // The legacy email-keyed path finds the challenge-bearing row and + // consumes it even though the client sends no challengeId. + const result = await verifyAndConsumeSignInCode(testEmail, code); + expect(result).toBe('ok'); + + const [row] = await db + .select({ consumed_at: magic_link_tokens.consumed_at }) + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(row?.consumed_at).not.toBeNull(); + }); + + it('returns invalid for a challenge ID that matches no row', async () => { + const { code } = await createSignInCode(testEmail); + + // Use a challenge ID that does not exist in the database. + const result = await reserveSignInCode( + testEmail, + code, + '00000000-0000-0000-0000-000000000000' + ); + expect(result).toBe('invalid'); + + // The existing row must remain untouched (no attempts, not consumed). + const [row] = await db + .select({ + attempts: magic_link_tokens.attempts, + consumed_at: magic_link_tokens.consumed_at, + }) + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(row?.attempts).toBe(0); + expect(row?.consumed_at).toBeNull(); + }); + it('legacy null-challenge row: email-keyed path still increments attempts', async () => { + // Manually insert a row without challenge_id to simulate a legacy rollout row. + const code = '999999'; + const token_hash = createHash('sha256').update(`${testEmail}:${code}`).digest('hex'); + await db.insert(magic_link_tokens).values({ + token_hash, + email: testEmail, + purpose: 'sign_in_code', + expires_at: new Date(Date.now() + 10 * 60_000).toISOString(), + challenge_id: null, + }); + + // A wrong guess without challengeId hits the legacy path and increments attempts. + const first = await reserveSignInCode(testEmail, '000000'); + expect(first).toBe('invalid'); + + const [row] = await db + .select({ attempts: magic_link_tokens.attempts }) + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(row?.attempts).toBe(1); + }); + }); + }); + + describe('reservation lapse — one account, one session', () => { + it('with the same code, two sequential settle-attempts produce one consumed code', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + + // First: reserve, then consume unconditionally (simulating a lapse) + const firstReserve = await reserveSignInCode(testEmail, code, challengeId); + expect(firstReserve).toBe('ok'); + + await consumeSignInCode(testEmail, code, challengeId); + + // Second attempt: code should be consumed, so reserve returns 'invalid' + const secondReserve = await reserveSignInCode(testEmail, code, challengeId); + expect(secondReserve).toBe('invalid'); + }); + + it('settles once, consumes after a lapse, and creates one session across two attempts', async () => { + const { code, challengeId } = await createSignInCode(testEmail); + const settledAccounts: string[] = []; + const issuedSessions: string[] = []; + + // 1. Reserve the code. + const reserve = await reserveSignInCode(testEmail, code, challengeId); + expect(reserve).toBe('ok'); + + // Settlement is idempotent for an existing account. Model the completed + // settlement before the reservation lapses, as the native route does. + settledAccounts.push(testEmail); + + // 2. Force the reservation to expire (simulate mid-settlement timeout). + await db + .update(magic_link_tokens) + .set({ reserved_until: new Date(Date.now() - 60_000).toISOString() }) + .where(eq(magic_link_tokens.email, testEmail)); + + // 3. Commit fails because the reservation has lapsed. + const committed = await commitSignInCode(testEmail, code, challengeId); + expect(committed).toBe(false); + + // Code must still be unconsumed (commit does not touch consumed_at on failure). + const [afterCommit] = await db + .select({ consumed_at: magic_link_tokens.consumed_at }) + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(afterCommit?.consumed_at).toBeNull(); + + // 4. Consume unconditionally — the user is legitimately settled. + const consumed = await consumeSignInCode(testEmail, code, challengeId); + expect(consumed).toBe(true); + issuedSessions.push(testEmail); + + // Code is now marked as consumed. + const [afterConsume] = await db + .select({ consumed_at: magic_link_tokens.consumed_at }) + .from(magic_link_tokens) + .where(eq(magic_link_tokens.email, testEmail)); + expect(afterConsume?.consumed_at).not.toBeNull(); + + // 5. A second attempt cannot settle or issue another session. + const secondReserve = await reserveSignInCode(testEmail, code, challengeId); + expect(secondReserve).toBe('invalid'); + + // The consumed code must not be re-usable. + const secondConsume = await consumeSignInCode(testEmail, code, challengeId); + expect(secondConsume).toBe(false); + expect(settledAccounts).toEqual([testEmail]); + expect(issuedSessions).toEqual([testEmail]); + }); + }); + describe('deleteSignInCode', () => { it('deletes only the matching sign-in code', async () => { const magicLink = await createMagicLinkToken(testEmail); - const code = await createSignInCode(testEmail); + const { code, challengeId } = await createSignInCode(testEmail); await deleteSignInCode(testEmail, code); - expect(await verifyAndConsumeSignInCode(testEmail, code)).toBe('invalid'); + expect(await verifyAndConsumeSignInCode(testEmail, code, challengeId)).toBe('invalid'); expect(await verifyAndConsumeMagicLinkToken(magicLink.plaintext_token)).not.toBeNull(); }); }); diff --git a/apps/web/src/lib/auth/magic-link-tokens.ts b/apps/web/src/lib/auth/magic-link-tokens.ts index 12ab0462a7..f80b2a5975 100644 --- a/apps/web/src/lib/auth/magic-link-tokens.ts +++ b/apps/web/src/lib/auth/magic-link-tokens.ts @@ -4,11 +4,14 @@ import { magic_link_tokens } from '@kilocode/db/schema'; import * as z from 'zod'; import 'server-only'; import { NEXTAUTH_SECRET, NEXTAUTH_URL } from '@/lib/config.server'; -import { randomBytes, randomInt, createHash, createHmac } from 'crypto'; +import { randomBytes, randomInt, randomUUID, createHash, createHmac } from 'crypto'; import { normalizeEmail } from '@/lib/utils'; +import { captureMessage } from '@sentry/nextjs'; const SIGN_IN_CODE_EXPIRY_MINUTES = 10; const SIGN_IN_CODE_MAX_ATTEMPTS = 5; +// Reservations expire on their own after two minutes — no cleanup job is needed. +const SIGN_IN_CODE_RESERVATION_MINUTES = 2; function hashSignInCode(email: string, code: string): string { return createHmac('sha256', NEXTAUTH_SECRET).update(`${email}:${code}`).digest('hex'); @@ -102,13 +105,20 @@ export async function verifyAndConsumeMagicLinkToken( * The stored hash is an HMAC keyed by the server secret, so a database leak * does not permit offline enumeration of the six-digit code space. * + * A random opaque challenge_id is generated alongside the code. The client + * must present it when verifying, so that attempts against challenge A do + * not spend challenge B's budget. + * * @param email - The email address to send the code to (case-insensitive) - * @returns The plaintext 6-digit code (for sending in email) + * @returns The plaintext 6-digit code and an opaque challenge identifier */ -export async function createSignInCode(email: string): Promise { +export async function createSignInCode( + email: string +): Promise<{ code: string; challengeId: string }> { const normalizedEmail = normalizeEmail(email); const code = String(randomInt(0, 1_000_000)).padStart(6, '0'); const token_hash = hashSignInCode(normalizedEmail, code); + const challengeId = randomUUID(); const expires_at = new Date(Date.now() + SIGN_IN_CODE_EXPIRY_MINUTES * 60 * 1000).toISOString(); await db.transaction(async tx => { @@ -124,43 +134,60 @@ export async function createSignInCode(email: string): Promise { isNull(magic_link_tokens.consumed_at) ) ); - await tx - .insert(magic_link_tokens) - .values({ token_hash, email: normalizedEmail, expires_at, purpose: 'sign_in_code' }); + await tx.insert(magic_link_tokens).values({ + token_hash, + email: normalizedEmail, + expires_at, + purpose: 'sign_in_code', + challenge_id: challengeId, + }); }); - return code; + return { code, challengeId }; } -export type VerifySignInCodeResult = 'ok' | 'invalid' | 'too_many_attempts'; +export type ReserveSignInCodeResult = 'ok' | 'invalid' | 'too_many_attempts' | 'in_progress'; /** - * Verify and consume an email sign-in code atomically, scoped by email. + * Reserve a sign-in code for settlement. Does not consume the code. + * A live reservation blocks concurrent callers, who receive 'in_progress'. + * A failed settlement must call releaseSignInCode so the code stays usable. + * + * When challengeId is supplied, all lookups, the pre-check, and the increment + * are keyed by challenge_id instead of email. This isolates attempt budgets: + * guesses against challenge A do not spend challenge B's budget. * - * Attempt limiting is checked BEFORE the hash comparison: once the live - * code for an email has recorded 5+ failed attempts, this returns - * 'too_many_attempts' even for the correct code. A mismatch increments - * attempts on the email's unconsumed row(s) and returns 'invalid'; so does - * an expired, already-consumed, or nonexistent code. + * When challengeId is absent, the legacy email-keyed path is used for + * shipped clients that do not yet send a challenge. The email-keyed path + * matches the live sign-in code row for the email whether or not it carries + * a challenge_id, so old clients can reserve codes minted by new clients. + * + * Returns 'ok' when the code is reserved, 'invalid' for a wrong/expired/consumed + * code, 'too_many_attempts' when the attempt budget is exhausted, and + * 'in_progress' when another caller already holds the reservation. */ -export async function verifyAndConsumeSignInCode( +export async function reserveSignInCode( email: string, - code: string -): Promise { + code: string, + challengeId?: string +): Promise { const normalizedEmail = normalizeEmail(email); - const [row] = await db - .select() - .from(magic_link_tokens) - .where( - and( - eq(magic_link_tokens.email, normalizedEmail), + const lookupWhere = challengeId + ? and( + eq(magic_link_tokens.challenge_id, challengeId), eq(magic_link_tokens.purpose, 'sign_in_code'), isNull(magic_link_tokens.consumed_at), sql`${magic_link_tokens.expires_at} > NOW()` ) - ) - .limit(1); + : and( + eq(magic_link_tokens.email, normalizedEmail), + eq(magic_link_tokens.purpose, 'sign_in_code'), + isNull(magic_link_tokens.consumed_at), + sql`${magic_link_tokens.expires_at} > NOW()` + ); + + const [row] = await db.select().from(magic_link_tokens).where(lookupWhere).limit(1); if (!row) { return 'invalid'; @@ -172,13 +199,11 @@ export async function verifyAndConsumeSignInCode( const token_hash = hashSignInCode(normalizedEmail, code); if (row.token_hash === token_hash) { - // attempts < MAX is re-checked here atomically: the early read above is - // only a fast path, and two concurrent wrong guesses can race it past - // the budget. This predicate guarantees an over-budget code can never - // consume regardless of racing increments. - const consumed = await db + const reserved = await db .update(magic_link_tokens) - .set({ consumed_at: sql`NOW()` }) + .set({ + reserved_until: sql`NOW() + interval '${sql.raw(String(SIGN_IN_CODE_RESERVATION_MINUTES))} minutes'`, + }) .where( and( eq(magic_link_tokens.token_hash, token_hash), @@ -186,17 +211,21 @@ export async function verifyAndConsumeSignInCode( eq(magic_link_tokens.purpose, 'sign_in_code'), isNull(magic_link_tokens.consumed_at), sql`${magic_link_tokens.expires_at} > NOW()`, - sql`${magic_link_tokens.attempts} < ${SIGN_IN_CODE_MAX_ATTEMPTS}` + sql`${magic_link_tokens.attempts} < ${SIGN_IN_CODE_MAX_ATTEMPTS}`, + sql`(${magic_link_tokens.reserved_until} IS NULL OR ${magic_link_tokens.reserved_until} < NOW())` ) ) .returning(); - if (consumed[0]) { + if (reserved[0]) { return 'ok'; } const [current] = await db - .select({ attempts: magic_link_tokens.attempts }) + .select({ + attempts: magic_link_tokens.attempts, + reserved_until: magic_link_tokens.reserved_until, + }) .from(magic_link_tokens) .where( and( @@ -207,26 +236,153 @@ export async function verifyAndConsumeSignInCode( ) ) .limit(1); - return current && current.attempts >= SIGN_IN_CODE_MAX_ATTEMPTS - ? 'too_many_attempts' - : 'invalid'; + + if (!current) { + return 'invalid'; + } + if (current.attempts >= SIGN_IN_CODE_MAX_ATTEMPTS) { + return 'too_many_attempts'; + } + if (current.reserved_until && new Date(current.reserved_until) > new Date()) { + return 'in_progress'; + } + return 'invalid'; } - await db - .update(magic_link_tokens) - .set({ attempts: sql`${magic_link_tokens.attempts} + 1` }) - .where( - and( - eq(magic_link_tokens.email, normalizedEmail), + // Increment attempts: key by challenge_id when available, otherwise by email. + // The legacy email-keyed path matches the live row regardless of challenge_id, + // so a no-challenge caller cannot bypass the budget of a challenge-bound row. + const incrementWhere = challengeId + ? and( + eq(magic_link_tokens.challenge_id, challengeId), eq(magic_link_tokens.purpose, 'sign_in_code'), isNull(magic_link_tokens.consumed_at), sql`${magic_link_tokens.attempts} < ${SIGN_IN_CODE_MAX_ATTEMPTS}` ) - ); + : and( + eq(magic_link_tokens.email, normalizedEmail), + eq(magic_link_tokens.purpose, 'sign_in_code'), + isNull(magic_link_tokens.consumed_at), + sql`${magic_link_tokens.attempts} < ${SIGN_IN_CODE_MAX_ATTEMPTS}` + ); + + await db + .update(magic_link_tokens) + .set({ attempts: sql`${magic_link_tokens.attempts} + 1` }) + .where(incrementWhere); + + // ponytail: remove legacy counter and no-challenge path when + // native_signin_code_legacy_no_challenge_count drains to 0 for + // 7 consecutive days — all shipped clients must send a challengeId. + if (!challengeId) { + captureMessage('native_signin_code_legacy_no_challenge_count: 1'); + } return 'invalid'; } +/** + * Commit a reserved sign-in code by setting consumed_at. + * Must be called only after a successful settlement. + * The reservation must still be live (reserved_until > NOW()). + * + * When challengeId is supplied, the commit is scoped to the challenge. + * + * Returns true when the code was committed, false when the reservation + * lapsed or the row was already consumed. + */ +export async function commitSignInCode( + email: string, + code: string, + challengeId?: string +): Promise { + const normalizedEmail = normalizeEmail(email); + const token_hash = hashSignInCode(normalizedEmail, code); + + const whereClauses = [ + eq(magic_link_tokens.token_hash, token_hash), + eq(magic_link_tokens.email, normalizedEmail), + eq(magic_link_tokens.purpose, 'sign_in_code'), + isNull(magic_link_tokens.consumed_at), + sql`${magic_link_tokens.reserved_until} > NOW()`, + ]; + if (challengeId) { + whereClauses.push(eq(magic_link_tokens.challenge_id, challengeId)); + } + + const committed = await db + .update(magic_link_tokens) + .set({ consumed_at: sql`NOW()`, reserved_until: null }) + .where(and(...whereClauses)) + .returning(); + + return committed.length > 0; +} + +/** + * Release a reserved sign-in code without consuming it or incrementing attempts. + * A settlement failure is not a wrong guess — it must not cost the user an attempt. + * + * When challengeId is supplied, the release is scoped to the challenge. + */ +export async function releaseSignInCode( + email: string, + code: string, + challengeId?: string +): Promise { + const normalizedEmail = normalizeEmail(email); + const token_hash = hashSignInCode(normalizedEmail, code); + + const whereClauses = [ + eq(magic_link_tokens.token_hash, token_hash), + eq(magic_link_tokens.email, normalizedEmail), + eq(magic_link_tokens.purpose, 'sign_in_code'), + isNull(magic_link_tokens.consumed_at), + ]; + if (challengeId) { + whereClauses.push(eq(magic_link_tokens.challenge_id, challengeId)); + } + + await db + .update(magic_link_tokens) + .set({ reserved_until: null }) + .where(and(...whereClauses)); +} + +/** + * Consume a sign-in code unconditionally, regardless of reservation status. + * Used when the reservation lapsed between settlement and commit — the user + * is legitimately settled and this prevents a second settlement with the same code. + * + * When challengeId is supplied, the consume is scoped to the challenge. + */ +export async function consumeSignInCode( + email: string, + code: string, + challengeId?: string +): Promise { + const normalizedEmail = normalizeEmail(email); + const token_hash = hashSignInCode(normalizedEmail, code); + + const whereClauses = [ + eq(magic_link_tokens.token_hash, token_hash), + eq(magic_link_tokens.email, normalizedEmail), + eq(magic_link_tokens.purpose, 'sign_in_code'), + isNull(magic_link_tokens.consumed_at), + ]; + if (challengeId) { + whereClauses.push(eq(magic_link_tokens.challenge_id, challengeId)); + } + + const consumed = await db + .update(magic_link_tokens) + .set({ consumed_at: sql`NOW()`, reserved_until: null }) + .where(and(...whereClauses)) + .returning(); + + return consumed.length > 0; +} + export async function deleteSignInCode(email: string, code: string): Promise { const normalizedEmail = normalizeEmail(email); await db diff --git a/apps/web/src/lib/auth/native-admission-apple.test.ts b/apps/web/src/lib/auth/native-admission-apple.test.ts new file mode 100644 index 0000000000..7f789d1d00 --- /dev/null +++ b/apps/web/src/lib/auth/native-admission-apple.test.ts @@ -0,0 +1,305 @@ +/** + * Focused tests for the Apple App Attest verifier (`native-admission-apple.ts`). + * + * The chain fixtures are real Apple App Attest material: + * - `REAL_LEAF_DER` is a credential certificate (credCert) and + * `REAL_INTERMEDIATE_DER` is its issuer "Apple App Attestation CA 1", + * captured from a real attestation. `X509Certificate.verify()` does not + * check validity windows, so the short fixture lifetimes do not matter. + * - `REAL_ROOT_DER` is Apple's published "Apple App Attestation Root CA". + * - `EVIL_ROOT_DER` / `EVIL_LEAF_DER` are a self-signed root and a leaf it + * signs; their signatures verify but they are not Apple's. + */ +import { describe, test, expect } from '@jest/globals'; +import { createHash } from 'node:crypto'; +import { + appAttestClientDataHash, + verifyAppleAttestation, + extractAppleAttestNonce, + parseAppleAttestNonceExtension, +} from './native-admission-apple'; + +jest.mock('@sentry/nextjs', () => ({ + captureMessage: jest.fn(), +})); + +jest.mock('@/lib/config.server', () => ({ + APPLE_TEAM_ID: 'WRPHYY66V6', + APPLE_APP_BUNDLE_ID: 'com.reelreel.app.dev', +})); + +// Real Apple App Attest credential certificate (dev-signed build, 2026-07). +const REAL_LEAF_DER = Buffer.from( + 'MIID3TCCA2KgAwIBAgIGAZ9IpNaMMAoGCCqGSM49BAMCME8xIzAhBgNVBAMMGkFwcGxlIEFwcCBBdHRlc3RhdGlvbiBDQSAxMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTI2MDcwODIwNDk1MFoXDTI2MDcxMTIwNDk1MFowgZExSTBHBgNVBAMMQDFjODI3NDAwOTI5ZmZkOTRmMTg3YTcxZGFmNWY5Y2NlYTRlZDI4YmQ1NTk0YTRlZWNiMzEyY2E3N2M1OTA1YjYxGjAYBgNVBAsMEUFBQSBDZXJ0aWZpY2F0aW9uMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEp0c9CJJCNWYRhvcOBz0MldGUG+LQKk5dzNC1703zM0uZPHJ9ZSjyi/u5A8Xoc745s/2U8b4cfPrFKs+lF3C7wKOCAeUwggHhMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgTwMBQGA1UdJQQNMAsGCSqGSIb3Y2QEGDB9BgkqhkiG92NkCAUEcDBupAMCAQq/iTADAgEAv4kxAwIBAL+JMgMCAQC/iTMDAgEAv4k0IQQfN1JQSFlZNjZVNi5jb20ucmVhbHJlZWwuYXBwLmRldr+JNgMCAQS/iTcDAgEAv4k5AwIBAL+JOgMCAQC/iTsDAgEAqgMCAQAwgZwGCSqGSIb3Y2QIBwSBjjCBi7+KeAYEBDI2LjW/iFADAgEAv4p5CQQHMS4wLjIyM7+KewcEBTIzRjc3v4p8BgQEMjYuNb+KfQYEBDI2LjW/in4DAgEAv4sKDwQNMjMuNi43Ny4wLjAsML+LCw8EDTIzLjYuNzcuMC4wLDC/iwwPBA0yMy42Ljc3LjAuMCwwv4gCCgQIaXBob25lb3MwMwYJKoZIhvdjZAgCBCYwJKEiBCBcuc+AqQ9zUC7BfyqA5kdr0zQCVF1wd/JLoctSwifqOzBYBgkqhkiG92NkCAYESzBJo0cERTBDDAIxMTA9MAoMA29rZKEDAQH/MAkMAm9hoQMBAf8wCwwEb3NnbqEDAQH/MAsMBG9kZWyhAwEB/zAKDANvY2uhAwEB/zAKBggqhkjOPQQDAgNpADBmAjEA+im5HLrobxIOPTgeAebtPRJCEKYhd0bK2TPJ4+HieYLRJl7eKqq3GVz+3jAGlO2dAjEAuE52drtf8/eY+BwVmJ1LTayePv5Vv/3IjceVUSmGl1WhS76nsgFzlwHIi70JCnbS', + 'base64' +); + +// Real Apple App Attest intermediate "Apple App Attestation CA 1". +const REAL_INTERMEDIATE_DER = Buffer.from( + 'MIICQzCCAcigAwIBAgIQCbrF4bxAGtnUU5W8OBoIVDAKBggqhkjOPQQDAzBSMSYwJAYDVQQDDB1BcHBsZSBBcHAgQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODM5NTVaFw0zMDAzMTMwMDAwMDBaME8xIzAhBgNVBAMMGkFwcGxlIEFwcCBBdHRlc3RhdGlvbiBDQSAxMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAErls3oHdNebI1j0Dn0fImJvHCX+8XgC3qs4JqWYdP+NKtFSV4mqJmBBkSSLY8uWcGnpjTY71eNw+/oI4ynoBzqYXndG6jWaL2bynbMq9FXiEWWNVnr54mfrJhTcIaZs6Zo2YwZDASBgNVHRMBAf8ECDAGAQH/AgEAMB8GA1UdIwQYMBaAFKyREFMzvb5oQf+nDKnl+url5YqhMB0GA1UdDgQWBBQ+410cBBmpybQx+IR01uHhV3LjmzAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaQAwZgIxALu+iI1zjQUCz7z9Zm0JV1A1vNaHLD+EMEkmKe3R+RToeZkcmui1rvjTqFQz97YNBgIxAKs47dDMge0ApFLDukT5k2NlU/7MKX8utN+fXr5aSsq2mVxLgg35BDhveAe7WJQ5tw==', + 'base64' +); + +// Apple App Attestation Root CA (published by Apple). +const REAL_ROOT_DER = Buffer.from( + 'MIICITCCAaegAwIBAgIQC/O+DvHN0uD7jG5yH2IXmDAKBggqhkjOPQQDAzBSMSYwJAYDVQQDDB1BcHBsZSBBcHAgQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODMyNTNaFw00NTAzMTUwMDAwMDBaMFIxJjAkBgNVBAMMHUFwcGxlIEFwcCBBdHRlc3RhdGlvbiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERTHhmLW07ATaFQIEVwTtT4dyctdhNbJhFs/Ii2FdCgAHGbpphY3+d8qjuDngIN3WVhQUBHAoMeQ/cLiP1sOUtgjqK9auYen1mMEvRq9Sk3Jm5X8U62H+xTD3FE9TgS41o0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSskRBTM72+aEH/pwyp5frq5eWKoTAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIwQgFGnByvsiVbpTKwSga0kP0e8EeDS4+sQmTvb7vn53O5+FRXgeLhpJ06ysC5PrOyAjEAp5U4xDgEgllF7En3VcE3iexZZtKeYnpqtijVoyFraWVIyd/dganmrduC1bmTBGwD', + 'base64' +); + +// The 32-byte nonce embedded in REAL_LEAF_DER's nonce extension. +const REAL_LEAF_NONCE = Buffer.from( + '5cb9cf80a90f73502ec17f2a80e6476bd33402545d7077f24ba1cb52c227ea3b', + 'hex' +); + +// A self-signed "evil" root CA and a leaf it signs. The signatures verify, +// but the chain does not terminate at Apple's pinned root. +const EVIL_ROOT_DER = Buffer.from( + 'MIIBvDCCAWGgAwIBAgIUIxhcSnRbntcgs+evcUEOTTktwp8wCgYIKoZIzj0EAwIwMzEdMBsGA1UEAwwURXZpbCBBcHAgQXR0ZXN0IFJvb3QxEjAQBgNVBAoMCUV2aWwgSW5jLjAeFw0yNjA4MDUwMzIxMzFaFw0zNjA4MDIwMzIxMzFaMDMxHTAbBgNVBAMMFEV2aWwgQXBwIEF0dGVzdCBSb290MRIwEAYDVQQKDAlFdmlsIEluYy4wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATnRhkpT7LAa0PA1UNo9bfiGCMjI7RIG6DdfmfTk9J20LvwFTMQJR7uKUKUsScXumPTPFVCbtGgZoEP0g54oCZto1MwUTAdBgNVHQ4EFgQUFk31dBJCbk+jPpEAOcn9yBg5wW8wHwYDVR0jBBgwFoAUFk31dBJCbk+jPpEAOcn9yBg5wW8wDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNJADBGAiEAkmxKmg21yRda1YyW+xPq7yS06t1t+Xj2g7xM0ciynaUCIQDe5KxJREZiI6vcTEPGI7Nu1MB1N6UhMjm0LoADJbMnaA==', + 'base64' +); + +const EVIL_LEAF_DER = Buffer.from( + 'MIIBnzCCAUWgAwIBAgIUHFYSRgS0w/DiuO80iY+c7OnmhjwwCgYIKoZIzj0EAwIwMzEdMBsGA1UEAwwURXZpbCBBcHAgQXR0ZXN0IFJvb3QxEjAQBgNVBAoMCUV2aWwgSW5jLjAeFw0yNjA4MDUwMzIxMzFaFw0zNjA4MDIwMzIxMzFaMCgxEjAQBgNVBAMMCWV2aWwtbGVhZjESMBAGA1UECgwJRXZpbCBJbmMuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEl6PL3kSmTAq1Q7R8gxaI83CBx3g11xxSGC5F/+yTlma4TMHfbWeZd4eFZpiWgKDmydYwkmY8QbadBBuT8GwaHKNCMEAwHQYDVR0OBBYEFNzed4Bs2iyp5DAB825XHV7hMdBXMB8GA1UdIwQYMBaAFBZN9XQSQm5Poz6RADnJ/cgYOcFvMAoGCCqGSM49BAMCA0gAMEUCIFWqf+29jpyO3XSz5XAn91Y03Z8K0f6eNjE50yYH80LIAiEAt6erE0yuB+3k0wWPKAuabJs4WWPrk2BVYpvmYzZ71kU=', + 'base64' +); + +// ── CBOR encoding helpers (the verifier decodes CBOR, so the tests encode) ── + +function cborHead(major: number, length: number): Buffer { + if (length < 24) return Buffer.from([(major << 5) | length]); + if (length < 0x100) return Buffer.from([(major << 5) | 24, length]); + const out = Buffer.alloc(3); + out[0] = (major << 5) | 25; + out.writeUInt16BE(length, 1); + return out; +} + +function cborText(value: string): Buffer { + const bytes = Buffer.from(value, 'utf8'); + return Buffer.concat([cborHead(3, bytes.length), bytes]); +} + +function cborBytes(value: Buffer): Buffer { + return Buffer.concat([cborHead(2, value.length), value]); +} + +function cborInt(value: number): Buffer { + if (value >= 0) return cborHead(0, value); + return cborHead(1, -1 - value); +} + +function cborArray(items: Buffer[]): Buffer { + return Buffer.concat([cborHead(4, items.length), ...items]); +} + +function cborMap(entries: Array<[Buffer, Buffer]>): Buffer { + const body = Buffer.concat(entries.flatMap(([key, value]) => [key, value])); + return Buffer.concat([cborHead(5, entries.length), body]); +} + +/** Build a COSE EC2 P-256 key map with fixed-length coordinates. */ +function buildCoseKey(): Buffer { + return cborMap([ + [cborInt(1), cborInt(2)], // kty = EC2 + [cborInt(-1), cborInt(1)], // crv = P-256 + [cborInt(-2), cborBytes(Buffer.alloc(32, 0xaa))], + [cborInt(-3), cborBytes(Buffer.alloc(32, 0xbb))], + ]); +} + +function buildAuthData(opts: { rpIdHash: Buffer; credentialId: Buffer; coseKey: Buffer }): Buffer { + const credIdLen = Buffer.alloc(2); + credIdLen.writeUInt16BE(opts.credentialId.length); + return Buffer.concat([ + opts.rpIdHash, + Buffer.from([0x40]), // AT flag set + Buffer.from([0, 0, 0, 0]), // signCount + Buffer.alloc(16), // aaguid + credIdLen, + opts.credentialId, + opts.coseKey, + ]); +} + +function buildAttestation(x5c: Buffer[], authData: Buffer): string { + const attStmt = cborMap([[cborText('x5c'), cborArray(x5c.map(cborBytes))]]); + const object = cborMap([ + [cborText('fmt'), cborText('apple-appattest')], + [cborText('attStmt'), attStmt], + [cborText('authData'), cborBytes(authData)], + ]); + return object.toString('base64'); +} + +// ── clientDataHash convention ────────────────────────────────────────────── + +describe('appAttestClientDataHash', () => { + test('hashes the UTF-8 bytes of the challenge string', () => { + // `@expo/app-integrity` computes SHA256(Data(challenge.utf8)) before calling + // DCAppAttestService. Both sides must agree or every attestation and every + // assertion fails with a nonce or signature mismatch. + const challenge = 'c2VydmVyLWNoYWxsZW5nZQ'; + expect(appAttestClientDataHash(challenge)).toEqual( + createHash('sha256').update(Buffer.from(challenge, 'utf8')).digest() + ); + }); + + test('does not base64url-decode the challenge first', () => { + const challenge = 'c2VydmVyLWNoYWxsZW5nZQ'; + expect(appAttestClientDataHash(challenge)).not.toEqual( + createHash('sha256').update(Buffer.from(challenge, 'base64url')).digest() + ); + }); +}); + +// ── Certificate chain verification ───────────────────────────────────────── + +describe('verifyAppleAttestation certificate chain', () => { + const teamId = 'WRPHYY66V6'; + const bundleId = 'com.reelreel.app.dev'; + const rpIdHash = createHash('sha256').update(`${teamId}.${bundleId}`).digest(); + const credentialId = Buffer.from('0123456789abcdef'); + const keyId = credentialId.toString('base64url'); + const challenge = 'c2VydmVyLWNoYWxsZW5nZQ'; // arbitrary base64url bytes + + function attestationFor(x5c: Buffer[]): string { + const authData = buildAuthData({ rpIdHash, credentialId, coseKey: buildCoseKey() }); + return buildAttestation(x5c, authData); + } + + test('accepts the real Apple [leaf, intermediate] chain shape', async () => { + const result = await verifyAppleAttestation( + attestationFor([REAL_LEAF_DER, REAL_INTERMEDIATE_DER]), + challenge, + keyId, + bundleId + ); + + // A real Apple chain must pass the certificate-chain, RP ID, flags, and + // credential-ID checks. The fixture's nonce is fixed and cannot be + // reproduced from new authData, so the verifier must stop at + // NONCE_MISMATCH — never at CERT_CHAIN_INVALID. + expect(result).toEqual({ ok: false, error: 'NONCE_MISMATCH' }); + }); + + test('accepts the real Apple [leaf, intermediate, root] chain shape', async () => { + const result = await verifyAppleAttestation( + attestationFor([REAL_LEAF_DER, REAL_INTERMEDIATE_DER, REAL_ROOT_DER]), + challenge, + keyId, + bundleId + ); + expect(result).toEqual({ ok: false, error: 'NONCE_MISMATCH' }); + }); + + test('rejects a chain that terminates at an unpinned root', async () => { + const result = await verifyAppleAttestation( + attestationFor([EVIL_LEAF_DER, EVIL_ROOT_DER]), + challenge, + keyId, + bundleId + ); + expect(result).toEqual({ ok: false, error: 'CERT_CHAIN_INVALID' }); + }); + + test('rejects a chain whose signatures do not verify', async () => { + // The real leaf is signed by the intermediate, not by the root. + const result = await verifyAppleAttestation( + attestationFor([REAL_LEAF_DER, REAL_ROOT_DER]), + challenge, + keyId, + bundleId + ); + expect(result).toEqual({ ok: false, error: 'CERT_CHAIN_INVALID' }); + }); +}); + +// ── Nonce extension parsing ──────────────────────────────────────────────── + +describe('extractAppleAttestNonce', () => { + test('extracts the nested nonce from the real Apple credential certificate', () => { + expect(extractAppleAttestNonce(REAL_LEAF_DER)).toEqual(REAL_LEAF_NONCE); + }); + + test('returns null when the certificate carries no nonce extension', () => { + // Apple's root CA is a real certificate without the App Attest extension. + expect(extractAppleAttestNonce(REAL_ROOT_DER)).toBeNull(); + }); + + test('returns null when the input is not a certificate', () => { + expect(extractAppleAttestNonce(Buffer.from([0x30, 0x00]))).toBeNull(); + expect(extractAppleAttestNonce(Buffer.alloc(64, 0xab))).toBeNull(); + }); + + test('returns null for a nonce-shaped byte run outside the extension', () => { + // The OID bytes appear verbatim, but there is no X.509 structure around + // them, so an OID-scanning parser would match and this one must not. + const nonce = Buffer.alloc(32, 0xab); + const oidBytes = Buffer.from([ + 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x63, 0x64, 0x08, 0x02, + ]); + expect( + extractAppleAttestNonce(Buffer.concat([oidBytes, nonceExtensionValue(nonce)])) + ).toBeNull(); + }); +}); + +/** `SEQUENCE { [1] EXPLICIT OCTET STRING nonce }` — the value Apple emits. */ +function nonceExtensionValue(nonce: Buffer): Buffer { + const octet = Buffer.concat([Buffer.from([0x04, nonce.length]), nonce]); + const tagged = Buffer.concat([Buffer.from([0xa1, octet.length]), octet]); + return Buffer.concat([Buffer.from([0x30, tagged.length]), tagged]); +} + +describe('parseAppleAttestNonceExtension', () => { + test('extracts the nested nonce', () => { + const nonce = Buffer.alloc(32, 0xab); + expect(parseAppleAttestNonceExtension(nonceExtensionValue(nonce))).toEqual(nonce); + }); + + test('returns null when the [1] EXPLICIT context tag is missing', () => { + // SEQUENCE { OCTET STRING nonce } without the [1] wrapper. + const nonce = Buffer.alloc(32, 0xcd); + const octet = Buffer.concat([Buffer.from([0x04, nonce.length]), nonce]); + const seq = Buffer.concat([Buffer.from([0x30, octet.length]), octet]); + expect(parseAppleAttestNonceExtension(seq)).toBeNull(); + }); + + test('returns null when the [1] tag is primitive instead of constructed', () => { + const nonce = Buffer.alloc(32, 0xab); + const octet = Buffer.concat([Buffer.from([0x04, nonce.length]), nonce]); + const seq = Buffer.concat([ + Buffer.from([0x30, octet.length + 2]), + Buffer.from([0x81, octet.length]), + octet, + ]); + expect(parseAppleAttestNonceExtension(seq)).toBeNull(); + }); + + test('returns null when the outer sequence holds a second element', () => { + const nonce = Buffer.alloc(32, 0xab); + const octet = Buffer.concat([Buffer.from([0x04, nonce.length]), nonce]); + const tagged = Buffer.concat([Buffer.from([0xa1, octet.length]), octet]); + const seq = Buffer.concat([ + Buffer.from([0x30, tagged.length + 2]), + tagged, + Buffer.from([0x05, 0x00]), + ]); + expect(parseAppleAttestNonceExtension(seq)).toBeNull(); + }); + + test('returns null when bytes trail the outer sequence', () => { + const value = nonceExtensionValue(Buffer.alloc(32, 0xab)); + expect( + parseAppleAttestNonceExtension(Buffer.concat([value, Buffer.from([0x05, 0x00])])) + ).toBeNull(); + }); + + test('returns null for a truncated length', () => { + // The outer SEQUENCE claims more bytes than the buffer holds. + expect( + parseAppleAttestNonceExtension(Buffer.from([0x30, 0x1c, 0xa1, 0x1a, 0x04, 0x18])) + ).toBeNull(); + }); +}); diff --git a/apps/web/src/lib/auth/native-admission-apple.ts b/apps/web/src/lib/auth/native-admission-apple.ts new file mode 100644 index 0000000000..fdf4d577ea --- /dev/null +++ b/apps/web/src/lib/auth/native-admission-apple.ts @@ -0,0 +1,369 @@ +import 'server-only'; +import { createHash, createPublicKey, createVerify, X509Certificate } from 'node:crypto'; +import { decode as decodeCbor } from 'cbor2'; +import { AsnParser } from '@peculiar/asn1-schema'; +import { Certificate } from '@peculiar/asn1-x509'; +import * as asn1js from 'asn1js'; +import { APPLE_APP_BUNDLE_ID, APPLE_TEAM_ID } from '@/lib/config.server'; +import { captureMessage } from '@sentry/nextjs'; + +/** + * Apple App Attest attestation verifier. + * + * Verifies attestation objects (key registration) and assertions (per-request + * authentication) produced by the DeviceCheck App Attest service. + * + * https://developer.apple.com/documentation/devicecheck/validating-apps-that-connect-to-your-server + * + * CBOR, X.509, and ASN.1 decoding are delegated to `cbor2`, `@peculiar/asn1-x509`, + * and `asn1js`. Only the App Attest rules live here. + */ + +/** + * Decode CBOR into a Map. `preferMap` keeps string-keyed maps as Maps, matching + * the integer-keyed COSE maps, so one accessor shape covers both. `decode` + * throws on truncated input and on trailing bytes, so every malformed payload + * fails closed. + */ +function decodeCborMap(buf: Buffer): Map | null { + try { + const value: unknown = decodeCbor(buf, { preferMap: true }); + return value instanceof Map ? (value as Map) : null; + } catch { + return null; + } +} + +/** CBOR byte strings decode to Uint8Array; normalize to Buffer without copying. */ +function asBuffer(value: unknown): Buffer | null { + if (Buffer.isBuffer(value)) return value; + if (value instanceof Uint8Array) return Buffer.from(value.buffer, value.byteOffset, value.length); + return null; +} + +// Apple App Attestation Root CA, published at +// https://www.apple.com/certificateauthority/Apple_App_Attestation_Root_CA.pem +// The chain must terminate at this certificate; it is pinned by SHA-256 +// fingerprint of the DER body. +const APPLE_APP_ATTEST_ROOT_PEM = `-----BEGIN CERTIFICATE----- +MIICITCCAaegAwIBAgIQC/O+DvHN0uD7jG5yH2IXmDAKBggqhkjOPQQDAzBSMSYw +JAYDVQQDDB1BcHBsZSBBcHAgQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwK +QXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODMyNTNa +Fw00NTAzMTUwMDAwMDBaMFIxJjAkBgNVBAMMHUFwcGxlIEFwcCBBdHRlc3RhdGlv +biBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9y +bmlhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERTHhmLW07ATaFQIEVwTtT4dyctdh +NbJhFs/Ii2FdCgAHGbpphY3+d8qjuDngIN3WVhQUBHAoMeQ/cLiP1sOUtgjqK9au +Yen1mMEvRq9Sk3Jm5X8U62H+xTD3FE9TgS41o0IwQDAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBSskRBTM72+aEH/pwyp5frq5eWKoTAOBgNVHQ8BAf8EBAMCAQYw +CgYIKoZIzj0EAwMDaAAwZQIwQgFGnByvsiVbpTKwSga0kP0e8EeDS4+sQmTvb7vn +53O5+FRXgeLhpJ06ysC5PrOyAjEAp5U4xDgEgllF7En3VcE3iexZZtKeYnpqtijV +oyFraWVIyd/dganmrduC1bmTBGwD +-----END CERTIFICATE-----`; + +const ROOT_CA_SHA256 = '1cb9823ba28ba6ad2d33a006941de2ae4f513ef1d4e831b9f7e0fa7b6242c932'; + +// Parse the pinned root once. Fail loudly if the embedded PEM drifts from the +// pinned fingerprint, otherwise a corrupted pin silently rejects all +// attestations. +const APPLE_APP_ATTEST_ROOT_CERT = (() => { + const cert = new X509Certificate(APPLE_APP_ATTEST_ROOT_PEM); + const fingerprint = createHash('sha256').update(cert.raw).digest('hex'); + if (fingerprint !== ROOT_CA_SHA256) { + throw new Error('Embedded Apple App Attest root CA fingerprint mismatch'); + } + return cert; +})(); + +export type AppleAttestError = + | 'INVALID_ATTEST_FORMAT' + | 'CERT_CHAIN_INVALID' + | 'RP_ID_MISMATCH' + | 'NONCE_MISMATCH' + | 'KEY_ID_MISMATCH'; + +export type AppleAssertionError = 'INVALID_ASSERTION' | 'KEY_NOT_FOUND'; + +/** + * The clientDataHash App Attest binds a challenge with. + * + * `@expo/app-integrity` passes `Data(challenge.utf8)` through SHA-256 before + * handing it to `DCAppAttestService`, so both the attestation nonce and the + * assertion signature are over the hash of the challenge string's bytes. This + * is the single definition of that convention — the assertion caller uses it + * too, so the two paths cannot drift apart. + */ +export function appAttestClientDataHash(challenge: string): Buffer { + return createHash('sha256').update(challenge, 'utf8').digest(); +} + +/** + * Verify an Apple App Attest attestation object and return the extracted + * credential ID and DER-encoded SPKI public key. + * + * Attestation bytes are base64-encoded, as received from the mobile client. + */ +export async function verifyAppleAttestation( + attestationBase64: string, + challenge: string, + expectedKeyId: string, + bundleId: string = APPLE_APP_BUNDLE_ID +): Promise< + | { ok: true; credentialId: Buffer; publicKeySpkiBase64: string } + | { ok: false; error: AppleAttestError } +> { + let buf: Buffer; + try { + buf = Buffer.from(attestationBase64, 'base64'); + } catch { + return { ok: false, error: 'INVALID_ATTEST_FORMAT' }; + } + + const attestMap = decodeCborMap(buf); + if (!attestMap) return { ok: false, error: 'INVALID_ATTEST_FORMAT' }; + if (attestMap.get('fmt') !== 'apple-appattest') + return { ok: false, error: 'INVALID_ATTEST_FORMAT' }; + + const authData = asBuffer(attestMap.get('authData')); + if (!authData || authData.length < 37) return { ok: false, error: 'INVALID_ATTEST_FORMAT' }; + + const attStmt = attestMap.get('attStmt'); + if (!(attStmt instanceof Map)) return { ok: false, error: 'INVALID_ATTEST_FORMAT' }; + const x5c = attStmt.get('x5c'); + if (!Array.isArray(x5c) || x5c.length < 2) return { ok: false, error: 'INVALID_ATTEST_FORMAT' }; + const chainDer = x5c.map(entry => asBuffer(entry)); + if (chainDer.some(entry => entry === null)) { + return { ok: false, error: 'INVALID_ATTEST_FORMAT' }; + } + const chainBuffers = chainDer as Buffer[]; + const credCertBuf = chainBuffers[0]; + + // Certificate chain verification. Every certificate must be signed by the + // next one in the array (leaf → intermediate → … → root) and the chain must + // terminate at Apple's pinned App Attest root. Apple emits x5c as either + // [leaf, intermediate, root] or [leaf, intermediate], so the terminal + // certificate must either be the pinned root itself or be signed by it. + let chain: X509Certificate[]; + try { + chain = chainBuffers.map(der => new X509Certificate(der)); + } catch { + return { ok: false, error: 'CERT_CHAIN_INVALID' }; + } + + try { + for (let i = 0; i < chain.length - 1; i++) { + if (!chain[i].verify(chain[i + 1].publicKey)) { + return { ok: false, error: 'CERT_CHAIN_INVALID' }; + } + } + } catch { + return { ok: false, error: 'CERT_CHAIN_INVALID' }; + } + + const terminalCert = chain[chain.length - 1]; + const terminalFingerprint = createHash('sha256').update(terminalCert.raw).digest('hex'); + if (terminalFingerprint !== ROOT_CA_SHA256) { + try { + if (!terminalCert.verify(APPLE_APP_ATTEST_ROOT_CERT.publicKey)) { + captureMessage(`apple_attest_unknown_ca: ${terminalFingerprint}`); + return { ok: false, error: 'CERT_CHAIN_INVALID' }; + } + } catch { + captureMessage(`apple_attest_unknown_ca: ${terminalFingerprint}`); + return { ok: false, error: 'CERT_CHAIN_INVALID' }; + } + } + + // RP ID hash check — authData bytes 0-31 + // The RP ID is the full Apple App ID: TeamID.BundleID. + const appId = `${APPLE_TEAM_ID}.${bundleId}`; + const expectedRpIdHash = createHash('sha256').update(appId).digest(); + if (!authData.subarray(0, 32).equals(expectedRpIdHash)) { + return { ok: false, error: 'RP_ID_MISMATCH' }; + } + + // Nonce check. Apple's nonce is SHA256(authData || clientDataHash). + // `@expo/app-integrity` computes clientDataHash as SHA256 over the UTF-8 + // bytes of the challenge string it was handed, so hash the same bytes here. + // The challenge is base64url ASCII, so the string is the wire form. + const clientDataHash = appAttestClientDataHash(challenge); + + // Apple nonce = SHA256(authData || clientDataHash) + const expectedNonce = createHash('sha256') + .update(Buffer.concat([authData, clientDataHash])) + .digest('hex'); + + const nonceExt = extractAppleAttestNonce(credCertBuf); + if (!nonceExt || nonceExt.toString('hex') !== expectedNonce) { + captureMessage( + `apple_attest_nonce_mismatch: expected=${expectedNonce.substring(0, 16)}... got=${nonceExt?.toString('hex').substring(0, 16) ?? 'null'}...` + ); + return { ok: false, error: 'NONCE_MISMATCH' }; + } + + // Extract credential ID from authData + const flagsByte = authData[32]; + if (flagsByte === undefined) return { ok: false, error: 'INVALID_ATTEST_FORMAT' }; + const flags = flagsByte; + if (!(flags & 0x40)) return { ok: false, error: 'INVALID_ATTEST_FORMAT' }; // AT flag + + let pos = 37; // rpIdHash(32) + flags(1) + signCount(4) + pos += 16; // aaguid + const credIdLen = authData.readUInt16BE(pos); + pos += 2; + const credentialId = authData.subarray(pos, pos + credIdLen); + pos += credIdLen; + + // Verify keyId matches credential ID + if (credentialId.toString('base64url') !== expectedKeyId) { + return { ok: false, error: 'KEY_ID_MISMATCH' }; + } + + // Extract COSE public key and export it as SPKI DER + const coseKeyBuf = authData.subarray(pos); + let publicKeySpkiBase64: string; + try { + publicKeySpkiBase64 = coseKeyToPublicKey(coseKeyBuf) + .export({ type: 'spki', format: 'der' }) + .toString('base64'); + } catch { + return { ok: false, error: 'INVALID_ATTEST_FORMAT' }; + } + + return { ok: true, credentialId, publicKeySpkiBase64 }; +} + +/** Apple App Attest nonce extension. */ +const APPLE_ATTEST_NONCE_OID = '1.2.840.113635.100.8.2'; + +/** asn1js tag classes are 1-indexed: 1 = universal, 3 = context-specific. */ +const ASN1_CLASS_CONTEXT = 3; + +/** + * Decode the Apple nonce extension's value: `SEQUENCE { [1] EXPLICIT OCTET STRING }`. + * + * Exported so the grammar can be unit tested without re-signing a certificate. + * Every level must be exactly the structure Apple emits, and no trailing bytes + * are tolerated, so a malformed value returns null instead of a partial read. + */ +export function parseAppleAttestNonceExtension(extnValue: Buffer): Buffer | null { + const { result, offset } = asn1js.fromBER(extnValue); + if (offset === -1 || offset !== extnValue.length) return null; + + if (!(result instanceof asn1js.Sequence)) return null; + const sequenceItems = result.valueBlock.value; + if (sequenceItems.length !== 1) return null; + + const tagged = sequenceItems[0]; + if (!(tagged instanceof asn1js.Constructed)) return null; + if (tagged.idBlock.tagClass !== ASN1_CLASS_CONTEXT || tagged.idBlock.tagNumber !== 1) return null; + + const taggedItems = tagged.valueBlock.value; + if (taggedItems.length !== 1) return null; + + const nonce = taggedItems[0]; + if (!(nonce instanceof asn1js.OctetString)) return null; + + return Buffer.from(nonce.valueBlock.valueHexView); +} + +/** + * Extract the nonce from the Apple App Attest credential certificate. + * + * The certificate is parsed as X.509 and the extension is located by OID, so a + * nonce-shaped byte run elsewhere in the certificate cannot be mistaken for the + * extension. Returns null when the certificate, the extension, or the nested + * value is missing or malformed. + */ +export function extractAppleAttestNonce(certDer: Buffer): Buffer | null { + try { + const certificate = AsnParser.parse(certDer, Certificate); + const extension = certificate.tbsCertificate.extensions?.find( + candidate => candidate.extnID === APPLE_ATTEST_NONCE_OID + ); + if (!extension) return null; + return parseAppleAttestNonceExtension(Buffer.from(extension.extnValue.buffer)); + } catch { + return null; + } +} + +/** + * Convert a COSE EC2 P-256 key (CBOR map with integer labels) to a public key. + * + * The COSE coordinates go through a JWK, so Node builds the SPKI encoding and + * validates the point instead of this module hand-writing DER. + */ +function coseKeyToPublicKey(coseKeyBuf: Buffer) { + const coseKey = decodeCborMap(coseKeyBuf); + if (!coseKey) throw new Error('not a map'); + + // COSE labels: 1=kty, -1=crv, -2=x, -3=y (all integers) + if (coseKey.get(1) !== 2) throw new Error('not EC2'); + if (coseKey.get(-1) !== 1) throw new Error('not P-256'); + const x = asBuffer(coseKey.get(-2)); + const y = asBuffer(coseKey.get(-3)); + if (!x || !y) throw new Error('missing coordinates'); + if (x.length !== 32 || y.length !== 32) throw new Error('invalid coordinate length'); + + return createPublicKey({ + key: { + kty: 'EC', + crv: 'P-256', + x: x.toString('base64url'), + y: y.toString('base64url'), + }, + format: 'jwk', + }); +} + +// ── Assertion verification ──────────────────────────────────────────────── + +/** + * Verify an Apple App Attest assertion using a previously stored public key. + * + * Returns the sign count from the authenticator data so the caller can + * enforce a strictly increasing counter. + */ +export async function verifyAppleAssertion( + keyId: string, + clientDataHash: Buffer, + assertionBase64: string, + publicKeySpkiBase64: string +): Promise<{ ok: true; signCount: number } | { ok: false; error: AppleAssertionError }> { + let assertionBuf: Buffer; + try { + assertionBuf = Buffer.from(assertionBase64, 'base64'); + } catch { + return { ok: false, error: 'INVALID_ASSERTION' }; + } + + const assertionMap = decodeCborMap(assertionBuf); + if (!assertionMap) return { ok: false, error: 'INVALID_ASSERTION' }; + + const signature = asBuffer(assertionMap.get('signature')); + const authenticatorData = asBuffer(assertionMap.get('authenticatorData')); + if (!signature || !authenticatorData) { + return { ok: false, error: 'INVALID_ASSERTION' }; + } + + // Extract sign count from authenticatorData (bytes 33-36, big-endian uint32) + if (authenticatorData.length < 37) { + return { ok: false, error: 'INVALID_ASSERTION' }; + } + const signCount = authenticatorData.readUInt32BE(33); + + // Verify: signature over (authenticatorData || clientDataHash) + try { + const publicKeyDer = Buffer.from(publicKeySpkiBase64, 'base64'); + const verify = createVerify('sha256'); + verify.update(Buffer.concat([authenticatorData, clientDataHash])); + verify.end(); + if (!verify.verify({ key: publicKeyDer, format: 'der', type: 'spki' }, signature)) { + return { ok: false, error: 'INVALID_ASSERTION' }; + } + } catch { + return { ok: false, error: 'INVALID_ASSERTION' }; + } + + return { ok: true, signCount }; +} diff --git a/apps/web/src/lib/auth/native-admission-google.test.ts b/apps/web/src/lib/auth/native-admission-google.test.ts new file mode 100644 index 0000000000..49b45ed29d --- /dev/null +++ b/apps/web/src/lib/auth/native-admission-google.test.ts @@ -0,0 +1,275 @@ +/** + * Tests for findings #9 (cert digest enforcement), #12 (simulator bypass + * production guard), and the Play Integrity decode endpoint fix (the resource + * path must carry the configured package name, not the project number). + */ +import { describe, test, expect, beforeAll, afterAll, beforeEach } from '@jest/globals'; +import { createHash, generateKeyPairSync } from 'node:crypto'; +import { isProductionInternal, verifyPlayIntegrity } from './native-admission-google'; + +// Values are read lazily (via getters) so tests can vary the configuration. +const mockConfig = { + GOOGLE_PLAY_INTEGRITY_PACKAGE_NAME: 'com.kilocode.kiloapp', + GOOGLE_PLAY_INTEGRITY_PROJECT_NUMBER: '123456789', + GOOGLE_PLAY_INTEGRITY_SERVICE_ACCOUNT_KEY: '', + GOOGLE_PLAY_INTEGRITY_CERT_DIGESTS: 'test-cert-digest', +}; + +jest.mock('@/lib/config.server', () => ({ + get GOOGLE_PLAY_INTEGRITY_PACKAGE_NAME() { + return mockConfig.GOOGLE_PLAY_INTEGRITY_PACKAGE_NAME; + }, + get GOOGLE_PLAY_INTEGRITY_PROJECT_NUMBER() { + return mockConfig.GOOGLE_PLAY_INTEGRITY_PROJECT_NUMBER; + }, + get GOOGLE_PLAY_INTEGRITY_SERVICE_ACCOUNT_KEY() { + return mockConfig.GOOGLE_PLAY_INTEGRITY_SERVICE_ACCOUNT_KEY; + }, + get GOOGLE_PLAY_INTEGRITY_CERT_DIGESTS() { + return mockConfig.GOOGLE_PLAY_INTEGRITY_CERT_DIGESTS; + }, +})); +jest.mock('@sentry/nextjs', () => ({ + captureMessage: jest.fn(), +})); + +// ── Simulator bypass production guard (finding #12) ────────────────────── + +describe('isProductionInternal (internal guard)', () => { + const originalEnv = process.env.NODE_ENV; + + afterEach(() => { + Object.defineProperty(process.env, 'NODE_ENV', { + value: originalEnv, + writable: false, + }); + }); + + test('returns false in test environment', () => { + expect(isProductionInternal()).toBe(false); + }); + + test('returns true when NODE_ENV is production', () => { + Object.defineProperty(process.env, 'NODE_ENV', { + value: 'production', + writable: false, + }); + expect(isProductionInternal()).toBe(true); + }); +}); + +describe('verifyPlayIntegrity production bypass guard', () => { + const originalNodeEnv = process.env.NODE_ENV; + const originalBypass = process.env.NATIVE_ADMISSION_SIMULATOR_BYPASS; + + afterEach(() => { + Object.defineProperty(process.env, 'NODE_ENV', { + value: originalNodeEnv, + writable: false, + }); + if (originalBypass === undefined) { + delete process.env.NATIVE_ADMISSION_SIMULATOR_BYPASS; + } else { + process.env.NATIVE_ADMISSION_SIMULATOR_BYPASS = originalBypass; + } + }); + + test('bypass is NOT taken in production even with NATIVE_ADMISSION_SIMULATOR_BYPASS=true', async () => { + Object.defineProperty(process.env, 'NODE_ENV', { + value: 'production', + writable: false, + }); + process.env.NATIVE_ADMISSION_SIMULATOR_BYPASS = 'true'; + + // In production with bypass=true and no credentials configured, + // verifyPlayIntegrity must throw (fail-closed), NOT return the + // simulator bypass result. + await expect(verifyPlayIntegrity('fake-token', 'fake-challenge')).rejects.toThrow( + 'Google Play Integrity credentials not configured' + ); + }); + + test('bypass is taken in non-production with NATIVE_ADMISSION_SIMULATOR_BYPASS=true', async () => { + Object.defineProperty(process.env, 'NODE_ENV', { + value: 'development', + writable: false, + }); + process.env.NATIVE_ADMISSION_SIMULATOR_BYPASS = 'true'; + + const result = await verifyPlayIntegrity('fake-token', 'fake-challenge'); + expect(result).toEqual({ ok: true, packageName: 'com.example.simulator' }); + }); +}); + +// ── Decode endpoint resource path ───────────────────────────────────────── + +describe('verifyPlayIntegrity decode endpoint', () => { + const originalFetch = global.fetch; + const mockFetch = jest.fn(); + + beforeAll(() => { + // getAccessToken signs a real JWT, so the service account key needs a + // valid RSA private key. + const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); + mockConfig.GOOGLE_PLAY_INTEGRITY_SERVICE_ACCOUNT_KEY = JSON.stringify({ + client_email: 'play-integrity@example.iam.gserviceaccount.com', + private_key: privateKey.export({ type: 'pkcs8', format: 'pem' }), + token_uri: 'https://oauth2.googleapis.com/token', + }); + global.fetch = mockFetch as unknown as typeof fetch; + }); + + afterAll(() => { + global.fetch = originalFetch; + }); + + beforeEach(() => { + mockFetch.mockReset(); + }); + + test('calls decodeIntegrityToken with the configured package name in the resource path', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ access_token: 'test-access-token' }), + }); + const expectedNonce = createHash('sha256').update('challenge-value').digest('base64'); + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + tokenPayloadExternal: { + requestDetails: { + requestPackageName: 'com.kilocode.kiloapp', + nonce: expectedNonce, + }, + appIntegrity: { + appRecognitionVerdict: 'PLAY_RECOGNIZED', + certificateSha256Digest: ['test-cert-digest'], + packageName: 'com.kilocode.kiloapp', + }, + deviceIntegrity: { + deviceRecognitionVerdict: ['MEETS_DEVICE_INTEGRITY'], + }, + }, + }), + }); + + const result = await verifyPlayIntegrity('test-integrity-token', 'challenge-value'); + + expect(result).toEqual({ ok: true, packageName: 'com.kilocode.kiloapp' }); + // Google rejects the project-number-only path, so the exact URL must carry + // the configured package name. + expect(mockFetch).toHaveBeenNthCalledWith( + 2, + 'https://playintegrity.googleapis.com/v1/com.kilocode.kiloapp:decodeIntegrityToken', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + Authorization: 'Bearer test-access-token', + 'Content-Type': 'application/json', + }), + body: JSON.stringify({ integrityToken: 'test-integrity-token' }), + }) + ); + }); + + test('accepts a standard request bound through requestHash', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ access_token: 'test-access-token' }), + }); + const expectedHash = createHash('sha256').update('challenge-value', 'utf8').digest('base64'); + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + tokenPayloadExternal: { + // Standard requests return requestHash verbatim and carry no nonce. + requestDetails: { + requestPackageName: 'com.kilocode.kiloapp', + requestHash: expectedHash, + }, + appIntegrity: { + appRecognitionVerdict: 'PLAY_RECOGNIZED', + certificateSha256Digest: ['test-cert-digest'], + packageName: 'com.kilocode.kiloapp', + }, + deviceIntegrity: { deviceRecognitionVerdict: ['MEETS_DEVICE_INTEGRITY'] }, + }, + }), + }); + + const result = await verifyPlayIntegrity('test-integrity-token', 'challenge-value'); + + expect(result).toEqual({ ok: true, packageName: 'com.kilocode.kiloapp' }); + }); + + test('refuses a standard request whose requestHash binds another challenge', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ access_token: 'test-access-token' }), + }); + const otherHash = createHash('sha256').update('other-challenge', 'utf8').digest('base64'); + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + tokenPayloadExternal: { + requestDetails: { + requestPackageName: 'com.kilocode.kiloapp', + requestHash: otherHash, + }, + appIntegrity: { + appRecognitionVerdict: 'PLAY_RECOGNIZED', + certificateSha256Digest: ['test-cert-digest'], + packageName: 'com.kilocode.kiloapp', + }, + deviceIntegrity: { deviceRecognitionVerdict: ['MEETS_DEVICE_INTEGRITY'] }, + }, + }), + }); + + const result = await verifyPlayIntegrity('test-integrity-token', 'challenge-value'); + + expect(result).toEqual({ ok: false, error: 'NONCE_MISMATCH' }); + }); + + test('refuses a verdict that carries no request binding', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ access_token: 'test-access-token' }), + }); + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + tokenPayloadExternal: { + requestDetails: { requestPackageName: 'com.kilocode.kiloapp' }, + appIntegrity: { + appRecognitionVerdict: 'PLAY_RECOGNIZED', + certificateSha256Digest: ['test-cert-digest'], + packageName: 'com.kilocode.kiloapp', + }, + deviceIntegrity: { deviceRecognitionVerdict: ['MEETS_DEVICE_INTEGRITY'] }, + }, + }), + }); + + const result = await verifyPlayIntegrity('test-integrity-token', 'challenge-value'); + + expect(result).toEqual({ ok: false, error: 'NONCE_MISMATCH' }); + }); + + test('surfaces an API status error without exposing the configured package name', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ access_token: 'test-access-token' }), + }); + mockFetch.mockResolvedValueOnce({ ok: false, status: 500 }); + + try { + await verifyPlayIntegrity('test-integrity-token', 'challenge-value'); + throw new Error('verifyPlayIntegrity should have thrown'); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + expect(message).toBe('Play Integrity API returned 500'); + expect(message).not.toContain('com.kilocode.kiloapp'); + } + }); +}); diff --git a/apps/web/src/lib/auth/native-admission-google.ts b/apps/web/src/lib/auth/native-admission-google.ts new file mode 100644 index 0000000000..b404f716b2 --- /dev/null +++ b/apps/web/src/lib/auth/native-admission-google.ts @@ -0,0 +1,263 @@ +import 'server-only'; +import { createHash, createSign } from 'node:crypto'; +import { captureMessage } from '@sentry/nextjs'; +import { + GOOGLE_PLAY_INTEGRITY_PACKAGE_NAME, + GOOGLE_PLAY_INTEGRITY_CERT_DIGESTS, + GOOGLE_PLAY_INTEGRITY_PROJECT_NUMBER, + GOOGLE_PLAY_INTEGRITY_SERVICE_ACCOUNT_KEY, +} from '@/lib/config.server'; +import { getEnvVariable } from '@/lib/dotenvx'; + +/** + * Google Play Integrity verifier. + * + * Calls the Play Integrity API to decode and validate a verdict token. + * In production without credentials, the provider fails closed — a 5xx is + * thrown so the admission layer can surface the infrastructure fault. + * + * Non-production can use a simulator bypass behind the production guard. + */ + +const PLAY_INTEGRITY_API_BASE = 'https://playintegrity.googleapis.com/v1'; + +// Required integrity labels per the plan +const REQUIRED_INTEGRITY_LABEL = 'MEETS_DEVICE_INTEGRITY'; + +export type PlayIntegrityError = + | 'INVALID_TOKEN' + | 'INTEGRITY_API_FAILURE' + | 'DEVICE_NOT_RECOGNIZED' + | 'APP_NOT_RECOGNIZED' + | 'NONCE_MISMATCH' + | 'PACKAGE_MISMATCH' + | 'CERT_DIGEST_MISMATCH'; + +function isProduction(): boolean { + return process.env.NODE_ENV === 'production'; +} + +/** Exported for testing the production guard. */ +export const isProductionInternal = isProduction; + +/** Obtain a GCP access token from a service-account key via JWT bearer assertion. */ +async function getAccessToken( + clientEmail: string, + privateKey: string, + tokenUri: string +): Promise { + const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url'); + const now = Math.floor(Date.now() / 1000); + const claimSet = Buffer.from( + JSON.stringify({ + iss: clientEmail, + scope: 'https://www.googleapis.com/auth/playintegrity', + aud: tokenUri, + exp: now + 3600, + iat: now, + }) + ).toString('base64url'); + + const signingInput = `${header}.${claimSet}`; + const sign = createSign('RSA-SHA256'); + sign.update(signingInput); + sign.end(); + const sig = sign.sign(privateKey, 'base64url'); + const jwt = `${signingInput}.${sig}`; + + const res = await fetch(tokenUri, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: jwt, + }), + }); + if (!res.ok) throw new Error(`Google token endpoint ${res.status}`); + const data = (await res.json()) as { access_token: string }; + return data.access_token; +} + +/** + * Required server-side environment variables for Play Integrity. + */ +interface PlayIntegrityConfig { + serviceAccountKey: string; + expectedPackageName: string; + expectedCertDigests: string[]; +} + +function getPlayIntegrityConfig(): PlayIntegrityConfig | null { + const projectNumber = GOOGLE_PLAY_INTEGRITY_PROJECT_NUMBER; + const serviceAccountKey = GOOGLE_PLAY_INTEGRITY_SERVICE_ACCOUNT_KEY; + + if (!projectNumber || !serviceAccountKey || !GOOGLE_PLAY_INTEGRITY_PACKAGE_NAME) { + return null; + } + + const expectedCertDigests = GOOGLE_PLAY_INTEGRITY_CERT_DIGESTS + ? GOOGLE_PLAY_INTEGRITY_CERT_DIGESTS.split(',') + .map(s => s.trim()) + .filter(Boolean) + : []; + + return { + serviceAccountKey, + expectedPackageName: GOOGLE_PLAY_INTEGRITY_PACKAGE_NAME, + expectedCertDigests, + }; +} + +/** + * Verify a Play Integrity verdict token. + * + * Binds the token to the server-issued challenge via the nonce field. + * Requires MEETS_DEVICE_INTEGRITY in the deviceRecognitionVerdict array. + * Verifies package name and signing certificate digest. + * + * Throws on infrastructure faults (network errors, auth failures) so the + * admission layer can surface them as 5xx. + */ +export async function verifyPlayIntegrity( + integrityToken: string, + challenge: string +): Promise<{ ok: true; packageName: string } | { ok: false; error: PlayIntegrityError }> { + // Production guard: simulator bypass only in non-production. + if (!isProduction()) { + const bypass = getEnvVariable('NATIVE_ADMISSION_SIMULATOR_BYPASS'); + if (bypass === 'true') { + captureMessage('google_play_integrity_simulator_bypass'); + return { ok: true, packageName: 'com.example.simulator' }; + } + } + + const config = getPlayIntegrityConfig(); + + if (!config) { + captureMessage('google_play_integrity_missing_credentials'); + // Fail-closed: missing credentials in production means we cannot verify. + throw new Error('Google Play Integrity credentials not configured'); + } + + // Decode and verify the integrity token + let accessToken: string; + try { + const saKey = JSON.parse(config.serviceAccountKey); + accessToken = await getAccessToken( + saKey.client_email, + saKey.private_key, + saKey.token_uri || 'https://oauth2.googleapis.com/token' + ); + } catch (err) { + captureMessage('google_play_integrity_auth_failure'); + throw new Error('Failed to obtain Play Integrity access token', { cause: err }); + } + + // Google requires the app's package name as the resource path on the decode + // endpoint; the project number alone is rejected. + const response = await fetch( + `${PLAY_INTEGRITY_API_BASE}/${encodeURIComponent(config.expectedPackageName)}:decodeIntegrityToken`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ integrityToken }), + } + ); + + if (!response.ok) { + captureMessage(`play_integrity_api_status: ${response.status}`); + throw new Error(`Play Integrity API returned ${response.status}`); + } + + const result = (await response.json()) as { + tokenPayloadExternal?: { + requestDetails?: { + requestPackageName?: string; + /** Standard requests: the client-supplied hash, returned verbatim. */ + requestHash?: string; + /** Classic requests only. */ + nonce?: string; + timestampMillis?: string; + }; + appIntegrity?: { + appRecognitionVerdict?: string; + certificateSha256Digest?: string[]; + packageName?: string; + }; + deviceIntegrity?: { + deviceRecognitionVerdict?: string[]; + }; + accountDetails?: { + appLicensingVerdict?: string; + }; + }; + }; + + const payload = result.tokenPayloadExternal; + if (!payload) return { ok: false, error: 'INVALID_TOKEN' }; + + // ── Challenge binding ─────────────────────────────────────────────────── + // Standard requests bind through `requestHash`, returned verbatim; classic + // requests use `nonce`. The client sends standard requests, so read + // `requestHash` and fall back to `nonce` only so a device still running a + // classic-request build is not refused mid-rollout. Both are compared + // against the same digest, so accepting either binds the same challenge. + const binding = payload.requestDetails?.requestHash ?? payload.requestDetails?.nonce; + if (!binding) { + captureMessage('play_integrity_missing_request_hash'); + return { ok: false, error: 'NONCE_MISMATCH' }; + } + + const expectedBinding = createHash('sha256').update(challenge, 'utf8').digest('base64'); + if (binding !== expectedBinding) { + captureMessage('play_integrity_request_hash_mismatch'); + return { ok: false, error: 'NONCE_MISMATCH' }; + } + + // ── Package identity ──────────────────────────────────────────────────── + const actualPackageName = + payload.requestDetails?.requestPackageName ?? payload.appIntegrity?.packageName; + if (!actualPackageName || actualPackageName !== config.expectedPackageName) { + captureMessage( + `play_integrity_package_mismatch: expected=${config.expectedPackageName} got=${actualPackageName ?? 'null'}` + ); + return { ok: false, error: 'PACKAGE_MISMATCH' }; + } + + // ── Signing certificate digest ────────────────────────────────────────── + const certDigests = payload.appIntegrity?.certificateSha256Digest ?? []; + const hasMatch = config.expectedCertDigests.some(expected => + certDigests.some(actual => actual.toLowerCase() === expected.toLowerCase()) + ); + if (!hasMatch) { + captureMessage( + `play_integrity_cert_digest_mismatch: expected=[${config.expectedCertDigests.join(',')}] got=[${certDigests.join(',')}]` + ); + return { ok: false, error: 'CERT_DIGEST_MISMATCH' }; + } + + // ── App recognition ───────────────────────────────────────────────────── + const appVerdict = payload.appIntegrity?.appRecognitionVerdict; + if (appVerdict !== 'PLAY_RECOGNIZED') { + captureMessage(`play_integrity_app_unrecognized: ${appVerdict ?? 'null'}`); + return { ok: false, error: 'APP_NOT_RECOGNIZED' }; + } + + // ── Device integrity: must contain MEETS_DEVICE_INTEGRITY in the array ── + const deviceVerdicts = payload.deviceIntegrity?.deviceRecognitionVerdict; + if (!deviceVerdicts || !Array.isArray(deviceVerdicts) || deviceVerdicts.length === 0) { + return { ok: false, error: 'DEVICE_NOT_RECOGNIZED' }; + } + + if (!deviceVerdicts.includes(REQUIRED_INTEGRITY_LABEL)) { + captureMessage( + `play_integrity_device_verdict_insufficient: got=[${deviceVerdicts.join(',')}] needed=${REQUIRED_INTEGRITY_LABEL}` + ); + return { ok: false, error: 'DEVICE_NOT_RECOGNIZED' }; + } + + return { ok: true, packageName: actualPackageName }; +} diff --git a/apps/web/src/lib/auth/native-admission.test.ts b/apps/web/src/lib/auth/native-admission.test.ts new file mode 100644 index 0000000000..2c8276df8d --- /dev/null +++ b/apps/web/src/lib/auth/native-admission.test.ts @@ -0,0 +1,1036 @@ +/* eslint-disable drizzle/enforce-update-with-where */ +/* eslint-disable drizzle/enforce-delete-with-where */ +jest.mock('@sentry/nextjs', () => ({ + captureMessage: jest.fn(), +})); + +jest.mock('@vercel/firewall', () => ({ + checkRateLimit: jest.fn(), +})); + +jest.mock('./native-admission-apple', () => ({ + verifyAppleAttestation: jest.fn(), + verifyAppleAssertion: jest.fn(), + // The digest is handed straight to the mocked verifyAppleAssertion, so its + // value is irrelevant here. native-admission-apple.test.ts pins the actual + // clientDataHash convention. + appAttestClientDataHash: jest.fn(() => Buffer.alloc(32)), +})); + +jest.mock('./native-admission-google', () => ({ + verifyPlayIntegrity: jest.fn(), +})); + +import { describe, test, expect, beforeEach } from '@jest/globals'; +import { + checkNativeAdmission, + validateAdmissionPayload, + issueAdmissionChallenge, + verifyAdmissionAsync, + persistAttestedKey, + shouldRefuseAsyncFailure, + ChallengeRateLimitError, + KeyCollisionError, + type AdmissionPayload, +} from './native-admission'; +import { captureMessage } from '@sentry/nextjs'; +import { checkRateLimit } from '@vercel/firewall'; +import { verifyAppleAttestation, verifyAppleAssertion } from './native-admission-apple'; +import { verifyPlayIntegrity } from './native-admission-google'; +import { db } from '@/lib/drizzle'; +import { native_attested_keys } from '@kilocode/db/schema'; +import type { SQL } from 'drizzle-orm'; +import { PgDialect } from 'drizzle-orm/pg-core/dialect'; + +jest.mock('@/lib/drizzle', () => ({ + db: { + insert: jest.fn().mockReturnValue({ + values: jest.fn().mockReturnValue({ + onConflictDoNothing: jest.fn().mockResolvedValue(undefined), + }), + }), + select: jest.fn().mockReturnValue({ from: jest.fn().mockReturnValue({ where: jest.fn() }) }), + update: jest.fn().mockReturnValue({ + set: jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([]), + }), + }), + }), + delete: jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ returning: jest.fn().mockResolvedValue([]) }), + }), + query: { + native_attested_keys: { + findFirst: jest.fn().mockResolvedValue(null), + }, + }, + }, +})); + +const mockCaptureMessage = jest.mocked(captureMessage); +const mockCheckRateLimit = jest.mocked(checkRateLimit); +const mockVerifyAppleAttestation = jest.mocked(verifyAppleAttestation); +const mockVerifyAppleAssertion = jest.mocked(verifyAppleAssertion); +const mockVerifyPlayIntegrity = jest.mocked(verifyPlayIntegrity); + +const setMode = (mode: string) => { + process.env.NATIVE_ADMISSION_MODE = mode; +}; + +// Helper to create a minimal NextRequest-like object for issueAdmissionChallenge +function makeRequest(): any { + return { + headers: new Map(), + nextUrl: { pathname: '/api/auth/native/admission-challenge' }, + }; +} + +/** + * Mock `db.update` so the challenge-consume update + * (`native_admission_challenges`) resolves with `challengeRows` and the + * attested-key update (`native_attested_keys`) resolves with `keyRows`. + * Assertion tests exercise both updates, so the two must be distinguished. + */ +function mockDualUpdate( + keyRows: unknown[], + challengeRows: unknown[] = [{ challenge: 'ch123' }] +): void { + jest.mocked(db.update).mockImplementation((table: unknown) => { + if (table === native_attested_keys) { + return { + set: jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue(keyRows), + }), + }), + } as any; + } + return { + set: jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue(challengeRows), + }), + }), + } as any; + }); +} + +/** + * Mock `db.update` with a stateful atomic sign-count gate: the attested-key + * update is accepted only while `sign_count` strictly increases, mirroring the + * DB predicate. Returns the update mock and a getter for the stored count so + * tests can prove a stale assertion never regresses the counter. + */ +function mockAtomicSignCountGate(initialCount: number): { + setMock: jest.Mock; + getStored: () => number; +} { + let stored = initialCount; + const setMock = jest.fn().mockImplementation((setValues: { sign_count: number }) => { + const accepted = setValues.sign_count > stored; + if (accepted) stored = setValues.sign_count; + return { + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue(accepted ? [{ key_id: 'key1' }] : []), + }), + }; + }); + jest.mocked(db.update).mockImplementation((table: unknown) => { + if (table === native_attested_keys) { + return { set: setMock } as any; + } + return { + set: jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([{ challenge: 'ch123' }]), + }), + }), + } as any; + }); + return { setMock, getStored: () => stored }; +} + +/** + * Mock `db.update` and capture the real conditional UPDATE that production + * sends for the attested-key table: the `set` values and the `where` + * predicate. The predicate is the actual Drizzle SQL object built by + * `verifyAppleAdmission`, so tests can render it and prove the strict + * `sign_count < asserted` clause reaches the database. + */ +function mockSignCountPredicateUpdate(keyRows: unknown[]): { + getSet: () => Record | undefined; + getWhere: () => SQL | undefined; +} { + let setValues: Record | undefined; + let whereCond: SQL | undefined; + jest.mocked(db.update).mockImplementation((table: unknown) => { + if (table === native_attested_keys) { + return { + set: jest.fn().mockImplementation((values: Record) => { + setValues = values; + return { + where: jest.fn().mockImplementation((cond: SQL) => { + whereCond = cond; + return { returning: jest.fn().mockResolvedValue(keyRows) }; + }), + }; + }), + } as any; + } + return { + set: jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([{ challenge: 'ch123' }]), + }), + }), + } as any; + }); + return { getSet: () => setValues, getWhere: () => whereCond }; +} + +// ── Wire contract validation ─────────────────────────────────────────────── + +describe('validateAdmissionPayload', () => { + test('rejects null', () => { + expect(validateAdmissionPayload(null)).toBeUndefined(); + }); + + test('rejects non-object', () => { + expect(validateAdmissionPayload('hello')).toBeUndefined(); + }); + + test('rejects empty object', () => { + expect(validateAdmissionPayload({})).toBeUndefined(); + }); + + test('rejects missing challenge', () => { + expect( + validateAdmissionPayload({ + platform: 'ios', + kind: 'attestation', + payload: 'abc', + keyId: 'k1', + }) + ).toBeUndefined(); + }); + + test('rejects wrong kind', () => { + expect( + validateAdmissionPayload({ + platform: 'ios', + challenge: 'abc', + kind: 'wrong', + payload: 'abc', + keyId: 'k1', + }) + ).toBeUndefined(); + }); + + test('rejects unknown platform', () => { + expect( + validateAdmissionPayload({ + platform: 'windows', + challenge: 'abc', + kind: 'attestation', + payload: 'abc', + }) + ).toBeUndefined(); + }); + + test('rejects missing payload', () => { + expect( + validateAdmissionPayload({ + platform: 'ios', + kind: 'attestation', + challenge: 'abc', + keyId: 'k1', + }) + ).toBeUndefined(); + }); + + test('rejects empty payload string', () => { + expect( + validateAdmissionPayload({ + platform: 'ios', + kind: 'attestation', + challenge: 'abc', + payload: '', + keyId: 'k1', + }) + ).toBeUndefined(); + }); + + test('rejects ios without keyId', () => { + expect( + validateAdmissionPayload({ + platform: 'ios', + kind: 'attestation', + challenge: 'abc', + payload: 'data', + }) + ).toBeUndefined(); + }); + + test('rejects ios with empty keyId', () => { + expect( + validateAdmissionPayload({ + platform: 'ios', + kind: 'attestation', + challenge: 'abc', + payload: 'data', + keyId: '', + }) + ).toBeUndefined(); + }); + + test('accepts valid ios attestation', () => { + const result = validateAdmissionPayload({ + platform: 'ios', + kind: 'attestation', + challenge: 'abc123', + payload: 'base64data', + keyId: 'key123', + }); + expect(result).toBeDefined(); + expect(result?.platform).toBe('ios'); + expect(result?.kind).toBe('attestation'); + expect(result?.challenge).toBe('abc123'); + expect(result?.payload).toBe('base64data'); + expect(result?.keyId).toBe('key123'); + }); + + test('accepts valid ios assertion', () => { + const result = validateAdmissionPayload({ + platform: 'ios', + kind: 'assertion', + challenge: 'abc123', + payload: 'base64assertion', + keyId: 'key123', + }); + expect(result).toBeDefined(); + expect(result?.kind).toBe('assertion'); + }); + + test('accepts valid android attestation (no keyId needed)', () => { + const result = validateAdmissionPayload({ + platform: 'android', + kind: 'attestation', + challenge: 'abc123', + payload: 'integrityToken', + }); + expect(result).toBeDefined(); + expect(result?.platform).toBe('android'); + expect(result?.keyId).toBeUndefined(); + }); +}); + +// ── Mode behavior ────────────────────────────────────────────────────────── + +describe('checkNativeAdmission', () => { + beforeEach(() => { + delete process.env.NATIVE_ADMISSION_MODE; + jest.clearAllMocks(); + }); + + describe('mode: off', () => { + test('admits everything', () => { + setMode('off'); + expect(checkNativeAdmission({})).toEqual({ admission: { ok: true }, verifyAsync: false }); + }); + + test('admits with malformed admission', () => { + setMode('off'); + expect(checkNativeAdmission({ admission: 'garbage' })).toEqual({ + admission: { ok: true }, + verifyAsync: false, + }); + }); + }); + + describe('mode: report', () => { + test('admits when no admission field is present', () => { + setMode('report'); + expect(checkNativeAdmission({})).toEqual({ admission: { ok: true }, verifyAsync: false }); + }); + + test('admits with malformed admission and logs', () => { + setMode('report'); + expect(checkNativeAdmission({ admission: 'garbage' })).toEqual({ + admission: { ok: true }, + verifyAsync: false, + }); + expect(mockCaptureMessage).toHaveBeenCalledWith('native_admission_invalid_shape'); + }); + + test('sets verifyAsync true for valid admission payload', () => { + setMode('report'); + const result = checkNativeAdmission({ + provider: 'apple', + idToken: 'abc', + admission: { + platform: 'ios', + kind: 'attestation', + challenge: 'ch123', + payload: 'base64data', + keyId: 'key123', + }, + }); + expect(result).toEqual({ admission: { ok: true }, verifyAsync: true }); + }); + }); + + describe('mode: undefined (unset)', () => { + test('admits everything (off is the default)', () => { + expect(checkNativeAdmission({})).toEqual({ admission: { ok: true }, verifyAsync: false }); + }); + + test('admits with present-invalid admission (empty mode behaves as off)', () => { + expect(checkNativeAdmission({ admission: 'garbage' })).toEqual({ + admission: { ok: true }, + verifyAsync: false, + }); + }); + }); + + describe('mode: enforce', () => { + beforeEach(() => { + setMode('enforce'); + }); + + test('absent admission field admits and logs legacy counter', () => { + const result = checkNativeAdmission({ provider: 'google', idToken: 'abc' }); + expect(result).toEqual({ admission: { ok: true }, verifyAsync: false }); + expect(mockCaptureMessage).toHaveBeenCalledWith('native_admission_legacy_count: 1'); + }); + + test('well-formed ios attestation passes sync check', () => { + const result = checkNativeAdmission({ + provider: 'apple', + idToken: 'abc', + admission: { + platform: 'ios', + kind: 'attestation', + challenge: 'ch123', + payload: 'base64data', + keyId: 'key123', + }, + }); + expect(result).toEqual({ admission: { ok: true }, verifyAsync: true }); + }); + + test('well-formed ios assertion passes sync check', () => { + const result = checkNativeAdmission({ + provider: 'apple', + idToken: 'abc', + admission: { + platform: 'ios', + kind: 'assertion', + challenge: 'ch123', + payload: 'base64data', + keyId: 'key123', + }, + }); + expect(result).toEqual({ admission: { ok: true }, verifyAsync: true }); + }); + + test('well-formed android assertion passes sync check', () => { + const result = checkNativeAdmission({ + provider: 'google', + serverAuthCode: 'auth123', + googleClientId: 'client123', + admission: { + platform: 'android', + kind: 'assertion', + challenge: 'ch123', + payload: 'integrityToken', + }, + }); + expect(result).toEqual({ admission: { ok: true }, verifyAsync: true }); + }); + + test('malformed admission is refused', () => { + const result = checkNativeAdmission({ + provider: 'google', + idToken: 'abc', + admission: { some: 'data' }, + }); + expect(result).toEqual({ + admission: { ok: false, errorCode: 'ADMISSION_REQUIRED' }, + verifyAsync: false, + }); + }); + }); +}); + +// ── Challenge issuance ───────────────────────────────────────────────────── + +describe('issueAdmissionChallenge', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('returns challenge and expiresIn in seconds', async () => { + mockCheckRateLimit.mockResolvedValue({ rateLimited: false }); + const mockValues = jest.fn().mockResolvedValue(undefined); + jest.mocked(db.insert).mockReturnValue({ values: mockValues } as any); + + const result = await issueAdmissionChallenge(makeRequest(), '127.0.0.1'); + expect(result.challenge).toEqual(expect.any(String)); + expect(result.challenge.length).toBeGreaterThan(0); + expect(result.expiresIn).toBe(120); // 2 minutes in seconds + expect(mockCheckRateLimit).toHaveBeenCalledWith( + 'native-admission-challenge', + expect.objectContaining({ rateLimitKey: 'native-challenge:127.0.0.1' }) + ); + }); + + test('throws ChallengeRateLimitError when rate limited', async () => { + mockCheckRateLimit.mockResolvedValue({ rateLimited: true }); + + await expect(issueAdmissionChallenge(makeRequest(), '127.0.0.1')).rejects.toThrow( + ChallengeRateLimitError + ); + }); +}); + +// ── Async admission verification ─────────────────────────────────────────── + +describe('verifyAdmissionAsync', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('returns error when challenge is already consumed', async () => { + // Simulate consumed_at already set (no rows returned from atomic update) + const setMock = jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([]), + }), + }); + jest.mocked(db.update).mockReturnValue({ set: setMock } as any); + + const admission: AdmissionPayload = { + platform: 'ios', + kind: 'attestation', + challenge: 'ch123', + payload: 'base64data', + keyId: 'key1', + }; + const result = await verifyAdmissionAsync(admission); + expect(result).toEqual({ ok: false, errorCode: 'ADMISSION_REQUIRED' }); + }); + + test('returns error when challenge is expired', async () => { + const setMock = jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([]), + }), + }); + jest.mocked(db.update).mockReturnValue({ set: setMock } as any); + + const admission: AdmissionPayload = { + platform: 'ios', + kind: 'attestation', + challenge: 'ch123', + payload: 'base64data', + keyId: 'key1', + }; + const result = await verifyAdmissionAsync(admission); + expect(result).toEqual({ ok: false, errorCode: 'ADMISSION_REQUIRED' }); + }); + + // ── iOS attestation ──────────────────────────────────────────────────── + + test('ios attestation succeeds and returns public key', async () => { + // Simulate successful atomic consume + const setMock = jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([{ challenge: 'ch123' }]), + }), + }); + jest.mocked(db.update).mockReturnValue({ set: setMock } as any); + + mockVerifyAppleAttestation.mockResolvedValue({ + ok: true, + credentialId: Buffer.from('cred'), + publicKeySpkiBase64: 'base64pubkey', + }); + + const admission: AdmissionPayload = { + platform: 'ios', + kind: 'attestation', + challenge: 'ch123', + payload: 'base64attest', + keyId: 'key1', + }; + const result = await verifyAdmissionAsync(admission); + expect(result).toEqual({ + ok: true, + platform: 'ios', + keyId: 'key1', + publicKey: 'base64pubkey', + }); + }); + + test('ios attestation returns existingKeyUserId when keyId is already bound', async () => { + const setMock = jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([{ challenge: 'ch123' }]), + }), + }); + jest.mocked(db.update).mockReturnValue({ set: setMock } as any); + + mockVerifyAppleAttestation.mockResolvedValue({ + ok: true, + credentialId: Buffer.from('cred'), + publicKeySpkiBase64: 'base64pubkey', + }); + + // KeyId already exists for a different user + jest.mocked(db.query.native_attested_keys.findFirst).mockResolvedValue({ + key_id: 'key1', + kilo_user_id: 'existingUser', + platform: 'ios', + public_key: 'base64pubkey', + sign_count: 0, + attested_at: '2026-01-01T00:00:00.000Z', + created_at: '2026-01-01T00:00:00.000Z', + last_used_at: null, + } as any); + + const admission: AdmissionPayload = { + platform: 'ios', + kind: 'attestation', + challenge: 'ch123', + payload: 'base64attest', + keyId: 'key1', + }; + const result = await verifyAdmissionAsync(admission); + expect(result).toEqual({ + ok: true, + platform: 'ios', + keyId: 'key1', + publicKey: 'base64pubkey', + existingKeyUserId: 'existingUser', + }); + }); + + test('ios attestation fails with provider error', async () => { + const setMock = jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([{ challenge: 'ch123' }]), + }), + }); + jest.mocked(db.update).mockReturnValue({ set: setMock } as any); + + mockVerifyAppleAttestation.mockResolvedValue({ + ok: false, + error: 'CERT_CHAIN_INVALID', + }); + + const admission: AdmissionPayload = { + platform: 'ios', + kind: 'attestation', + challenge: 'ch123', + payload: 'base64attest', + keyId: 'key1', + }; + const result = await verifyAdmissionAsync(admission); + expect(result).toEqual({ ok: false, errorCode: 'ADMISSION_REQUIRED' }); + expect(mockCaptureMessage).toHaveBeenCalledWith('apple_attestation_failed: CERT_CHAIN_INVALID'); + }); + + // ── iOS assertion ────────────────────────────────────────────────────── + + test('ios assertion fails when key is unknown', async () => { + const setMock = jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([{ challenge: 'ch123' }]), + }), + }); + jest.mocked(db.update).mockReturnValue({ set: setMock } as any); + + // No existing key found + jest.mocked(db.query.native_attested_keys.findFirst).mockResolvedValue(null as any); + + const admission: AdmissionPayload = { + platform: 'ios', + kind: 'assertion', + challenge: 'ch123', + payload: 'base64assertion', + keyId: 'unknownKey', + }; + const result = await verifyAdmissionAsync(admission); + expect(result).toEqual({ ok: false, errorCode: 'ADMISSION_REQUIRED' }); + expect(mockCaptureMessage).toHaveBeenCalledWith('apple_assertion_unknown_key'); + }); + + test('ios assertion fails when sign count is not increasing', async () => { + // Atomic key-counter update matches zero rows: stored 10 >= asserted 5. + mockDualUpdate([]); + + const existingKey = { + key_id: 'key1', + kilo_user_id: 'user1', + platform: 'ios', + public_key: 'base64pubkey', + sign_count: 10, + attested_at: '2026-01-01T00:00:00.000Z', + created_at: '2026-01-01T00:00:00.000Z', + last_used_at: null, + }; + jest.mocked(db.query.native_attested_keys.findFirst).mockResolvedValue(existingKey as any); + + mockVerifyAppleAssertion.mockResolvedValue({ + ok: true, + signCount: 5, // not greater than stored 10 + }); + + const admission: AdmissionPayload = { + platform: 'ios', + kind: 'assertion', + challenge: 'ch123', + payload: 'base64assertion', + keyId: 'key1', + }; + const result = await verifyAdmissionAsync(admission); + expect(result).toEqual({ ok: false, errorCode: 'ADMISSION_REQUIRED' }); + expect(mockCaptureMessage).toHaveBeenCalledWith( + 'apple_assertion_stale_or_replayed: asserted 5 rejected' + ); + }); + + test('ios assertion succeeds and proves the database sign_count < asserted predicate', async () => { + // Capture the real conditional UPDATE production sends to the database: + // stored 10 < asserted 11 matches the row. The predicate is rendered SQL, + // not a JavaScript model, so this test fails if the strict `<` clause is + // removed or weakened (for example to `<=`). + const { getSet, getWhere } = mockSignCountPredicateUpdate([{ key_id: 'key1' }]); + + const existingKey = { + key_id: 'key1', + kilo_user_id: 'user1', + platform: 'ios', + public_key: 'base64pubkey', + sign_count: 10, + attested_at: '2026-01-01T00:00:00.000Z', + created_at: '2026-01-01T00:00:00.000Z', + last_used_at: null, + }; + jest.mocked(db.query.native_attested_keys.findFirst).mockResolvedValue(existingKey as any); + + mockVerifyAppleAssertion.mockResolvedValue({ + ok: true, + signCount: 11, + }); + + const admission: AdmissionPayload = { + platform: 'ios', + kind: 'assertion', + challenge: 'ch123', + payload: 'base64assertion', + keyId: 'key1', + }; + const result = await verifyAdmissionAsync(admission); + expect(result).toEqual({ + ok: true, + platform: 'ios', + keyId: 'key1', + signCount: 11, + existingKeyUserId: 'user1', + }); + + // Verification advances the counter in the same conditional update. + expect(getSet()).toEqual({ sign_count: 11 }); + + // The strict `<` clause must reach the database: the rendered predicate + // contains `"sign_count" < $N` bound to the asserted count. + const { sql, params } = new PgDialect().sqlToQuery(getWhere()!); + const signCountPredicate = /"sign_count"\s*<\s*\$(\d+)/.exec(sql); + expect(signCountPredicate).not.toBeNull(); + expect(params[Number(signCountPredicate![1]) - 1]).toBe(11); + }); + + // ── iOS assertion: concurrency and monotonicity ───────────────────────── + + test('two concurrent assertions for the same sign count cannot both be accepted', async () => { + const { setMock, getStored } = mockAtomicSignCountGate(10); + + const existingKey = { + key_id: 'key1', + kilo_user_id: 'user1', + platform: 'ios', + public_key: 'base64pubkey', + sign_count: 10, + attested_at: '2026-01-01T00:00:00.000Z', + created_at: '2026-01-01T00:00:00.000Z', + last_used_at: null, + }; + jest.mocked(db.query.native_attested_keys.findFirst).mockResolvedValue(existingKey as any); + mockVerifyAppleAssertion.mockResolvedValue({ ok: true, signCount: 11 }); + + const admission: AdmissionPayload = { + platform: 'ios', + kind: 'assertion', + challenge: 'ch123', + payload: 'base64assertion', + keyId: 'key1', + }; + + // Both assertions verify the same authenticator and race for the same + // counter. The DB gate admits exactly one; the loser sees zero rows. + const [first, second] = await Promise.all([ + verifyAdmissionAsync(admission), + verifyAdmissionAsync(admission), + ]); + + expect([first, second].filter(r => r.ok)).toHaveLength(1); + expect(setMock).toHaveBeenCalledTimes(2); + expect(getStored()).toBe(11); + }); + + test('a lower or equal sign count cannot overwrite a higher stored count', async () => { + // A concurrent assertion already advanced the counter to 12. + const { setMock, getStored } = mockAtomicSignCountGate(12); + + const existingKey = { + key_id: 'key1', + kilo_user_id: 'user1', + platform: 'ios', + public_key: 'base64pubkey', + sign_count: 12, + attested_at: '2026-01-01T00:00:00.000Z', + created_at: '2026-01-01T00:00:00.000Z', + last_used_at: null, + }; + jest.mocked(db.query.native_attested_keys.findFirst).mockResolvedValue(existingKey as any); + + for (const staleCount of [11, 12]) { + mockVerifyAppleAssertion.mockResolvedValue({ ok: true, signCount: staleCount }); + + const admission: AdmissionPayload = { + platform: 'ios', + kind: 'assertion', + challenge: 'ch123', + payload: 'base64assertion', + keyId: 'key1', + }; + const result = await verifyAdmissionAsync(admission); + expect(result).toEqual({ ok: false, errorCode: 'ADMISSION_REQUIRED' }); + expect(mockCaptureMessage).toHaveBeenCalledWith( + `apple_assertion_stale_or_replayed: asserted ${staleCount} rejected` + ); + // The higher stored count is never overwritten. + expect(getStored()).toBe(12); + } + expect(setMock).toHaveBeenCalledTimes(2); + }); + + // ── Android ───────────────────────────────────────────────────────────── + + test('android assertion succeeds', async () => { + const setMock = jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([{ challenge: 'ch123' }]), + }), + }); + jest.mocked(db.update).mockReturnValue({ set: setMock } as any); + + mockVerifyPlayIntegrity.mockResolvedValue({ + ok: true, + packageName: 'com.kilocode.app', + }); + + const admission: AdmissionPayload = { + platform: 'android', + kind: 'assertion', + challenge: 'ch123', + payload: 'integrityToken', + }; + const result = await verifyAdmissionAsync(admission); + expect(result).toEqual({ + ok: true, + platform: 'android', + keyId: '', + }); + }); + + test('android fails with provider error', async () => { + const setMock = jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([{ challenge: 'ch123' }]), + }), + }); + jest.mocked(db.update).mockReturnValue({ set: setMock } as any); + + mockVerifyPlayIntegrity.mockResolvedValue({ + ok: false, + error: 'DEVICE_NOT_RECOGNIZED', + }); + + const admission: AdmissionPayload = { + platform: 'android', + kind: 'assertion', + challenge: 'ch123', + payload: 'integrityToken', + }; + const result = await verifyAdmissionAsync(admission); + expect(result).toEqual({ ok: false, errorCode: 'ADMISSION_REQUIRED' }); + }); + + test('android attestation kind is refused', async () => { + const setMock = jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([{ challenge: 'ch123' }]), + }), + }); + jest.mocked(db.update).mockReturnValue({ set: setMock } as any); + + const admission: AdmissionPayload = { + platform: 'android', + kind: 'attestation', + challenge: 'ch123', + payload: 'integrityToken', + }; + const result = await verifyAdmissionAsync(admission); + expect(result).toEqual({ ok: false, errorCode: 'ADMISSION_REQUIRED' }); + }); +}); + +// ── Key persistence ──────────────────────────────────────────────────────── + +describe('persistAttestedKey', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('inserts new key on attestation', async () => { + const mockOnConflict = jest.fn().mockResolvedValue(undefined); + const mockValues = jest.fn().mockReturnValue({ onConflictDoNothing: mockOnConflict }); + jest.mocked(db.insert).mockReturnValue({ values: mockValues } as any); + + // Simulate findFirst returning the inserted key + jest.mocked(db.query.native_attested_keys.findFirst).mockResolvedValue({ + key_id: 'key1', + kilo_user_id: 'user1', + platform: 'ios', + public_key: 'base64pubkey', + sign_count: 0, + attested_at: '2026-01-01T00:00:00.000Z', + created_at: '2026-01-01T00:00:00.000Z', + last_used_at: null, + } as any); + + await persistAttestedKey('user1', { + ok: true, + platform: 'ios', + keyId: 'key1', + publicKey: 'base64pubkey', + }); + }); + + test('throws on cross-user key collision', async () => { + const mockOnConflict = jest.fn().mockResolvedValue(undefined); + const mockValues = jest.fn().mockReturnValue({ onConflictDoNothing: mockOnConflict }); + jest.mocked(db.insert).mockReturnValue({ values: mockValues } as any); + + // Key exists but for a different user + jest.mocked(db.query.native_attested_keys.findFirst).mockResolvedValue({ + key_id: 'key1', + kilo_user_id: 'otherUser', + platform: 'ios', + public_key: 'base64pubkey', + sign_count: 0, + attested_at: '2026-01-01T00:00:00.000Z', + created_at: '2026-01-01T00:00:00.000Z', + last_used_at: null, + } as any); + + await expect( + persistAttestedKey('user1', { + ok: true, + platform: 'ios', + keyId: 'key1', + publicKey: 'base64pubkey', + }) + ).rejects.toThrow(KeyCollisionError); + }); + + test('refreshes last_used_at on assertion without rewriting the sign count', async () => { + const setMock = jest.fn().mockReturnValue({ + where: jest.fn().mockResolvedValue(undefined), + }); + jest.mocked(db.update).mockReturnValue({ set: setMock } as any); + + await persistAttestedKey('user1', { + ok: true, + platform: 'ios', + keyId: 'key1', + signCount: 15, + }); + + // The counter was bumped atomically during verification; persistence must + // only touch last_used_at so a stale assertion cannot regress it. + expect(setMock).toHaveBeenCalledWith({ last_used_at: expect.any(String) }); + expect(setMock.mock.calls[0]?.[0]).not.toHaveProperty('sign_count'); + }); + + test('skips persistence for Android (no key tracking)', async () => { + const mockOnConflict = jest.fn(); + const mockValues = jest.fn().mockReturnValue({ onConflictDoNothing: mockOnConflict }); + jest.mocked(db.insert).mockReturnValue({ values: mockValues } as any); + + await persistAttestedKey('user1', { + ok: true, + platform: 'android', + keyId: '', + }); + + // Should not attempt insert or update + expect(mockValues).not.toHaveBeenCalled(); + }); +}); + +// ── Async refusal mode ───────────────────────────────────────────────────── + +describe('shouldRefuseAsyncFailure', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('returns false in off mode', () => { + setMode('off'); + expect(shouldRefuseAsyncFailure()).toBe(false); + }); + + test('returns false in report mode', () => { + setMode('report'); + expect(shouldRefuseAsyncFailure()).toBe(false); + }); + + test('returns true in enforce mode', () => { + setMode('enforce'); + expect(shouldRefuseAsyncFailure()).toBe(true); + }); + + test('returns false when mode is unset', () => { + delete process.env.NATIVE_ADMISSION_MODE; + expect(shouldRefuseAsyncFailure()).toBe(false); + }); +}); + +// ── Cleanup ──────────────────────────────────────────────────────────────── + +describe('cleanupExpiredAdmissionChallenges', () => { + test('deletes expired challenges', async () => { + const { cleanupExpiredAdmissionChallenges } = await import('./native-admission'); + const whereMock = jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([{ challenge: 'ch1' }, { challenge: 'ch2' }]), + }); + jest.mocked(db.delete).mockReturnValue({ where: whereMock } as any); + + const count = await cleanupExpiredAdmissionChallenges(); + expect(count).toBe(2); + }); +}); diff --git a/apps/web/src/lib/auth/native-admission.ts b/apps/web/src/lib/auth/native-admission.ts new file mode 100644 index 0000000000..560672b2f6 --- /dev/null +++ b/apps/web/src/lib/auth/native-admission.ts @@ -0,0 +1,525 @@ +import 'server-only'; +import { randomBytes } from 'node:crypto'; +import { getEnvVariable } from '@/lib/dotenvx'; +import { captureMessage } from '@sentry/nextjs'; +import { db } from '@/lib/drizzle'; +import type { DrizzleTransaction } from '@/lib/drizzle'; +import { native_admission_challenges, native_attested_keys } from '@kilocode/db/schema'; +import { eq, and, lt, isNull, gt } from 'drizzle-orm'; +import { checkRateLimit } from '@vercel/firewall'; +import type { NextRequest } from 'next/server'; +import { + appAttestClientDataHash, + verifyAppleAttestation, + verifyAppleAssertion, +} from './native-admission-apple'; +import { verifyPlayIntegrity } from './native-admission-google'; + +// ── Types ────────────────────────────────────────────────────────────────── + +export type NativeAdmissionMode = 'off' | 'report' | 'enforce'; + +export type AdmissionPlatform = 'ios' | 'android'; + +export type AdmissionKind = 'attestation' | 'assertion'; + +export type NativeAdmissionResult = { ok: true } | { ok: false; errorCode: 'ADMISSION_REQUIRED' }; + +// ── Wire contract ────────────────────────────────────────────────────────── + +/** + * Admission payload as received from the mobile client. + * + * Plan contract: + * admission: { platform: 'ios'|'android', kind: 'attestation'|'assertion', + * challenge: string, payload: string, keyId?: string } + */ +export type AdmissionPayload = { + platform: AdmissionPlatform; + kind: AdmissionKind; + challenge: string; + /** Base64-encoded platform-specific data (attestation, assertion, or integrity token) */ + payload: string; + /** Required for iOS attestation and assertion */ + keyId?: string; +}; + +// ── Challenge lifecycle ──────────────────────────────────────────────────── + +export const CHALLENGE_EXPIRY_MS = 2 * 60 * 1000; // 2 minutes +const CHALLENGE_RATE_LIMIT_ID = 'native-admission-challenge'; + +/** + * Issue a server-side challenge for native admission. + * + * Uses @vercel/firewall checkRateLimit per client IP, matching the email + * sign-in rate-limit pattern. + */ +export async function issueAdmissionChallenge( + request: NextRequest, + ipAddress: string +): Promise<{ challenge: string; expiresIn: number }> { + const { rateLimited } = await checkRateLimit(CHALLENGE_RATE_LIMIT_ID, { + request, + rateLimitKey: `native-challenge:${ipAddress}`, + }); + + if (rateLimited) { + throw new ChallengeRateLimitError(); + } + + const challenge = randomBytes(32).toString('base64url'); + const expiresAt = new Date(Date.now() + CHALLENGE_EXPIRY_MS); + + await db.insert(native_admission_challenges).values({ + challenge, + expires_at: expiresAt.toISOString(), + }); + + return { challenge, expiresIn: Math.floor(CHALLENGE_EXPIRY_MS / 1000) }; +} + +export class ChallengeRateLimitError extends Error { + constructor() { + super('Too many challenges'); + this.name = 'ChallengeRateLimitError'; + } +} + +// ── Admission payload validation ─────────────────────────────────────────── + +/** + * Validate the shape of the admission payload received from the client. + * Returns a sanitized AdmissionPayload on success, undefined on failure. + */ +export function validateAdmissionPayload(raw: unknown): AdmissionPayload | undefined { + if (typeof raw !== 'object' || raw === null) return undefined; + const obj = raw as Record; + + // platform: 'ios' | 'android' + if (obj['platform'] !== 'ios' && obj['platform'] !== 'android') return undefined; + + // kind: 'attestation' | 'assertion' + if (obj['kind'] !== 'attestation' && obj['kind'] !== 'assertion') return undefined; + + // challenge: non-empty string + if (typeof obj['challenge'] !== 'string' || !obj['challenge']) return undefined; + + // payload: non-empty string (base64) + if (typeof obj['payload'] !== 'string' || !obj['payload']) return undefined; + + // keyId: optional string (required for iOS assertion) + const kind = obj['kind'] as AdmissionKind; + const platform = obj['platform'] as AdmissionPlatform; + const keyId = obj['keyId']; + + if (platform === 'ios') { + // iOS requires keyId for both attestation and assertion + if (typeof keyId !== 'string' || !keyId) return undefined; + } + + return { + platform, + kind, + challenge: obj['challenge'], + payload: obj['payload'], + keyId: typeof keyId === 'string' ? keyId : undefined, + }; +} + +// ── Admission gate (sync) ────────────────────────────────────────────────── + +/** + * Evaluate admission for a native auth request body. + * + * Mode behaviour: + * - 'off': admit everything. + * - 'report': evaluate and log, always admit. + * - 'enforce': validate admission. Absent field = legacy admit + count. + * Present invalid = refuse. Provider fault = 5xx via throw. + * + * Returns whether async verification is needed ('report' and 'enforce' modes + * with a valid admission payload). + */ +export function checkNativeAdmission(body: Record): { + admission: NativeAdmissionResult; + /** True when async crypto verification is needed (enforce or report). */ + verifyAsync: boolean; +} { + const mode = getEnvVariable('NATIVE_ADMISSION_MODE') as NativeAdmissionMode; + + // off mode (or unset) always admits with no async verification + if (!mode || mode === 'off') { + return { admission: { ok: true }, verifyAsync: false }; + } + + const hasAdmission = 'admission' in body && body['admission'] !== undefined; + + if (!hasAdmission) { + if (mode === 'enforce') { + captureMessage('native_admission_legacy_count: 1'); + } + return { admission: { ok: true }, verifyAsync: false }; + } + + // Validate the admission payload shape synchronously. + const admission = validateAdmissionPayload(body['admission']); + if (!admission) { + if (mode === 'report') { + captureMessage('native_admission_invalid_shape'); + return { admission: { ok: true }, verifyAsync: false }; + } + return { admission: { ok: false, errorCode: 'ADMISSION_REQUIRED' }, verifyAsync: false }; + } + + // Valid payload — async verification needed for enforce and report modes. + return { admission: { ok: true }, verifyAsync: mode === 'enforce' || mode === 'report' }; +} + +/** + * Whether to refuse admission when the async verify fails. + * Only true under enforce mode; report mode evaluates but always admits. + */ +export function shouldRefuseAsyncFailure(): boolean { + const mode = getEnvVariable('NATIVE_ADMISSION_MODE') as NativeAdmissionMode; + return mode === 'enforce'; +} + +// ── Async attestation / assertion verification ───────────────────────────── + +/** + * Perform full async admission verification BEFORE user settlement. + * + * Must be called before createOrUpdateUser in the token route so that: + * - Failed admission under enforce never leaves a settled user. + * - Keys are written after the user id exists (via the token route after + * successful settlement). + * + * Atomically consumes the challenge. On provider infrastructure errors, + * throws so the caller can surface a 5xx. + * + * Returns the key data to persist after settlement, or an error. + */ +export type VerifyAdmissionOk = { + ok: true; + /** Platform that verified */ + platform: AdmissionPlatform; + /** The keyId for the attested/asserted key */ + keyId: string; + /** DER-encoded SPKI public key (base64) — set on attestation only */ + publicKey?: string; + /** Sign count from the assertion authenticator data — set on assertion only */ + signCount?: number; + /** The userId that owns this key (from DB row) — set on assertion only */ + existingKeyUserId?: string; +}; + +export async function verifyAdmissionAsync( + admission: AdmissionPayload +): Promise { + // Atomically consume the challenge + const now = new Date().toISOString(); + const [consumed] = await db + .update(native_admission_challenges) + .set({ consumed_at: now }) + .where( + and( + eq(native_admission_challenges.challenge, admission.challenge), + gt(native_admission_challenges.expires_at, now), + isNull(native_admission_challenges.consumed_at) + ) + ) + .returning(); + + if (!consumed) { + return { ok: false, errorCode: 'ADMISSION_REQUIRED' }; + } + + // Route to platform-specific verifier + if (admission.platform === 'ios') { + return verifyAppleAdmission(admission); + } + + if (admission.platform === 'android') { + return verifyAndroidAdmission(admission); + } + + return { ok: false, errorCode: 'ADMISSION_REQUIRED' }; +} + +// ── Apple (iOS) admission ────────────────────────────────────────────────── + +async function verifyAppleAdmission( + admission: AdmissionPayload +): Promise { + const { kind, challenge, payload, keyId } = admission; + + if (!keyId) { + return { ok: false, errorCode: 'ADMISSION_REQUIRED' }; + } + + if (kind === 'attestation') { + // First-time attestation: verify and extract public key + const result = await verifyAppleAttestation(payload, challenge, keyId); + + if (!result.ok) { + captureMessage(`apple_attestation_failed: ${result.error}`); + return { ok: false, errorCode: 'ADMISSION_REQUIRED' }; + } + + // Check whether this keyId already exists — the caller uses this for + // preflight ownership checks before committing a sign-in code. + const existingKey = await db.query.native_attested_keys.findFirst({ + where: and(eq(native_attested_keys.key_id, keyId), eq(native_attested_keys.platform, 'ios')), + }); + + return { + ok: true, + platform: 'ios', + keyId, + publicKey: result.publicKeySpkiBase64, + existingKeyUserId: existingKey?.kilo_user_id, + }; + } + + // kind === 'assertion': verify against existing key + // Must have an existing attested key + const existingKey = await db.query.native_attested_keys.findFirst({ + where: and(eq(native_attested_keys.key_id, keyId), eq(native_attested_keys.platform, 'ios')), + }); + + if (!existingKey) { + captureMessage('apple_assertion_unknown_key'); + return { ok: false, errorCode: 'ADMISSION_REQUIRED' }; + } + + // Cross-user key collision: the caller must check ownership after settlement + // (we return the existing user id for that check) + + const assertionResult = await verifyAppleAssertion( + keyId, + appAttestClientDataHash(challenge), + payload, + existingKey.public_key + ); + + if (!assertionResult.ok) { + captureMessage(`apple_assertion_failed: ${assertionResult.error}`); + return { ok: false, errorCode: 'ADMISSION_REQUIRED' }; + } + + // Atomic monotonic sign-count gate. The check and the update are a single + // conditional UPDATE requiring sign_count < assertion count, so two + // concurrent assertions cannot both accept the same count and a lower or + // equal count can never overwrite a higher stored count. Zero updated rows + // means the assertion is stale or replayed — refuse admission. + const [updatedKey] = await db + .update(native_attested_keys) + .set({ sign_count: assertionResult.signCount }) + .where( + and( + eq(native_attested_keys.key_id, keyId), + eq(native_attested_keys.platform, 'ios'), + eq(native_attested_keys.kilo_user_id, existingKey.kilo_user_id), + lt(native_attested_keys.sign_count, assertionResult.signCount) + ) + ) + .returning({ key_id: native_attested_keys.key_id }); + + if (!updatedKey) { + // The update matched zero rows, so the assertion is stale or replayed. + // Report only the asserted count: `existingKey.sign_count` is the pre-read + // value and may have been advanced by a concurrent assertion, so it must + // not be presented as the current database count. + captureMessage( + `apple_assertion_stale_or_replayed: asserted ${assertionResult.signCount} rejected` + ); + return { ok: false, errorCode: 'ADMISSION_REQUIRED' }; + } + + return { + ok: true, + platform: 'ios', + keyId, + signCount: assertionResult.signCount, + existingKeyUserId: existingKey.kilo_user_id, + }; +} + +// ── Android (Play Integrity) admission ───────────────────────────────────── + +async function verifyAndroidAdmission( + admission: AdmissionPayload +): Promise { + const { kind, challenge, payload } = admission; + + // Android sends Play Integrity tokens with kind 'assertion' per the plan. + if (kind !== 'assertion') { + return { ok: false, errorCode: 'ADMISSION_REQUIRED' }; + } + + // verifyPlayIntegrity throws on infrastructure faults — caller must + // catch and surface as 5xx. + const result = await verifyPlayIntegrity(payload, challenge); + + if (!result.ok) { + captureMessage(`play_integrity_failed: ${result.error}`); + return { ok: false, errorCode: 'ADMISSION_REQUIRED' }; + } + + // Android Play Integrity tokens are per-request — no persistent key + // tracking. We return an empty keyId so the caller skips persistence. + return { + ok: true, + platform: 'android', + keyId: '', + }; +} + +// ── Key persistence (after settlement) ───────────────────────────────────── + +/** + * Persist an attested key after user settlement. + * + * For attestation: inserts a new key row. + * For assertion: verification already advanced `sign_count` atomically in + * `verifyAppleAdmission`; persistence only refreshes `last_used_at`. + * + * Cross-user collision: if the keyId already exists for a different user, + * refuses the insert/update. + */ +export async function persistAttestedKey( + userId: string, + verification: VerifyAdmissionOk +): Promise { + // Android has no persistent key tracking — skip. + if (verification.platform === 'android') return; + + if (verification.publicKey !== undefined) { + // Attestation: insert new key + await db + .insert(native_attested_keys) + .values({ + key_id: verification.keyId, + kilo_user_id: userId, + platform: verification.platform, + public_key: verification.publicKey, + sign_count: 0, + attested_at: new Date().toISOString(), + }) + .onConflictDoNothing(); + + // Verify the inserted key belongs to this user (cross-user collision check) + const inserted = await db.query.native_attested_keys.findFirst({ + where: and( + eq(native_attested_keys.key_id, verification.keyId), + eq(native_attested_keys.platform, verification.platform) + ), + }); + + if (!inserted || inserted.kilo_user_id !== userId) { + captureMessage('native_attested_key_cross_user_collision'); + throw new KeyCollisionError(); + } + } else if (verification.signCount !== undefined) { + // Assertion: refresh last_used_at only. The sign count was already bumped + // atomically during verification; never rewrite it here so a stale + // assertion cannot regress a newer count. The ownership predicate is + // preserved. + const now = new Date().toISOString(); + await db + .update(native_attested_keys) + .set({ + last_used_at: now, + }) + .where( + and( + eq(native_attested_keys.key_id, verification.keyId), + eq(native_attested_keys.platform, verification.platform), + eq(native_attested_keys.kilo_user_id, userId) + ) + ); + } +} + +export class KeyCollisionError extends Error { + constructor() { + super('Attested key already belongs to a different user'); + this.name = 'KeyCollisionError'; + } +} + +/** + * Persist an attested key within an existing Drizzle transaction. + * + * Same semantics as `persistAttestedKey` but uses the supplied transaction + * instead of the default `db` instance. Used when key persistence must be + * atomic with device session creation. + */ +export async function persistAttestedKeyTx( + tx: DrizzleTransaction, + userId: string, + verification: VerifyAdmissionOk +): Promise { + if (verification.platform === 'android') return; + + if (verification.publicKey !== undefined) { + // Attestation: insert new key + await tx + .insert(native_attested_keys) + .values({ + key_id: verification.keyId, + kilo_user_id: userId, + platform: verification.platform, + public_key: verification.publicKey, + sign_count: 0, + attested_at: new Date().toISOString(), + }) + .onConflictDoNothing(); + + // Verify the inserted key belongs to this user (cross-user collision check) + const inserted = await tx.query.native_attested_keys.findFirst({ + where: and( + eq(native_attested_keys.key_id, verification.keyId), + eq(native_attested_keys.platform, verification.platform) + ), + }); + + if (!inserted || inserted.kilo_user_id !== userId) { + captureMessage('native_attested_key_cross_user_collision'); + throw new KeyCollisionError(); + } + } else if (verification.signCount !== undefined) { + // Assertion: refresh last_used_at only. The sign count was already bumped + // atomically during verification; never rewrite it here so a stale + // assertion cannot regress a newer count. The ownership predicate is + // preserved. + const now = new Date().toISOString(); + await tx + .update(native_attested_keys) + .set({ + last_used_at: now, + }) + .where( + and( + eq(native_attested_keys.key_id, verification.keyId), + eq(native_attested_keys.platform, verification.platform), + eq(native_attested_keys.kilo_user_id, userId) + ) + ); + } +} + +// ── Cleanup ──────────────────────────────────────────────────────────────── + +/** + * Delete expired admission challenges. Called by the cron job. + */ +export async function cleanupExpiredAdmissionChallenges(): Promise { + const result = await db + .delete(native_admission_challenges) + .where(lt(native_admission_challenges.expires_at, new Date().toISOString())) + .returning({ challenge: native_admission_challenges.challenge }); + + return result.length; +} diff --git a/apps/web/src/lib/auth/native-id-tokens.test.ts b/apps/web/src/lib/auth/native-id-tokens.test.ts index fcf9a06d7e..2471fc6b55 100644 --- a/apps/web/src/lib/auth/native-id-tokens.test.ts +++ b/apps/web/src/lib/auth/native-id-tokens.test.ts @@ -1,6 +1,7 @@ import { verifyAppleJwtWithJwks, AppleJwtClientError } from '@/lib/auth/apple-jwks'; import { OAuth2Client } from 'google-auth-library'; import type jwt from 'jsonwebtoken'; +import { createHash } from 'node:crypto'; // The app-wide `jsonwebtoken` module augmentation (see types/next-auth.d.ts) adds // kiloUserId/version to JwtPayload for internal service tokens; irrelevant to Apple's @@ -13,35 +14,70 @@ jest.mock('@/lib/auth/apple-jwks', () => ({ })); jest.mock('google-auth-library'); // GOOGLE_IOS_CLIENT_ID is mutable (via the getter) so a test can simulate it being unset. -const mockConfig = { GOOGLE_IOS_CLIENT_ID: 'ios-client-id' }; +const mockConfig = { + GOOGLE_IOS_CLIENT_ID: 'ios-client-id', + GOOGLE_CLIENT_SECRET: 'web-client-secret', + APPLE_APP_BUNDLE_ID: 'com.kilocode.kiloapp', +}; jest.mock('@/lib/config.server', () => ({ GOOGLE_CLIENT_ID: 'web-client-id', + get GOOGLE_CLIENT_SECRET() { + return mockConfig.GOOGLE_CLIENT_SECRET; + }, get GOOGLE_IOS_CLIENT_ID() { return mockConfig.GOOGLE_IOS_CLIENT_ID; }, + get APPLE_APP_BUNDLE_ID() { + return mockConfig.APPLE_APP_BUNDLE_ID; + }, +})); +jest.mock('@sentry/nextjs', () => ({ + captureMessage: jest.fn(), })); import { verifyNativeAppleIdToken, verifyNativeGoogleIdToken, + exchangeNativeGoogleAuthCode, NativeIdTokenError, } from './native-id-tokens'; +import { GOOGLE_CLIENT_ID, GOOGLE_IOS_CLIENT_ID } from '@/lib/config.server'; +import { captureMessage } from '@sentry/nextjs'; const mockVerifyAppleJwtWithJwks = jest.mocked(verifyAppleJwtWithJwks); const mockGetFederatedSignonCertsAsync = jest.fn(); const mockVerifySignedJwtWithCertsAsync = jest.fn(); +const mockGetToken = jest.fn(); +const mockVerifyIdToken = jest.fn(); +const mockCaptureMessage = jest.mocked(captureMessage); (OAuth2Client as unknown as jest.Mock).mockImplementation(() => ({ getFederatedSignonCertsAsync: mockGetFederatedSignonCertsAsync, verifySignedJwtWithCertsAsync: mockVerifySignedJwtWithCertsAsync, + getToken: mockGetToken, + verifyIdToken: mockVerifyIdToken, })); describe('verifyNativeAppleIdToken', () => { beforeEach(() => { jest.clearAllMocks(); + mockConfig.APPLE_APP_BUNDLE_ID = 'com.kilocode.kiloapp'; + }); + + it('verifies against the configured Apple bundle ID and returns sub/email', async () => { + mockConfig.APPLE_APP_BUNDLE_ID = 'com.kilocode.kiloapp.dev'; + mockVerifyAppleJwtWithJwks.mockResolvedValue( + applePayload({ sub: 'apple-sub-1', email: 'user@example.com', email_verified: true }) + ); + + const result = await verifyNativeAppleIdToken('a-token'); + + expect(mockVerifyAppleJwtWithJwks).toHaveBeenCalledWith('a-token', 'com.kilocode.kiloapp.dev'); + expect(result).toEqual({ sub: 'apple-sub-1', email: 'user@example.com' }); }); - it('verifies against the Kilo app bundle ID and returns sub/email', async () => { + it('falls back to the default bundle ID when APPLE_APP_BUNDLE_ID is not configured', async () => { + mockConfig.APPLE_APP_BUNDLE_ID = ''; mockVerifyAppleJwtWithJwks.mockResolvedValue( applePayload({ sub: 'apple-sub-1', email: 'user@example.com', email_verified: true }) ); @@ -82,6 +118,53 @@ describe('verifyNativeAppleIdToken', () => { await expect(verifyNativeAppleIdToken('bad-token')).rejects.toThrow(AppleJwtClientError); }); + + // C12: Apple nonce binding + it('accepts a token when the nonce digest matches the sent raw nonce', async () => { + const rawNonce = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const expectedDigest = createHash('sha256').update(rawNonce).digest('hex'); + mockVerifyAppleJwtWithJwks.mockResolvedValue( + applePayload({ + sub: 'apple-sub-1', + email: 'user@example.com', + email_verified: true, + nonce: expectedDigest, + }) + ); + + const result = await verifyNativeAppleIdToken('a-token', rawNonce); + expect(result).toEqual({ sub: 'apple-sub-1', email: 'user@example.com' }); + expect(mockCaptureMessage).not.toHaveBeenCalled(); + }); + + it('throws NativeIdTokenError when the nonce does not match', async () => { + const rawNonce = 'correct-raw-nonce-that-client-sent'; + // Apple embeds the digest of whatever nonce Apple received. If the token + // contains a different nonce, the server must reject it. + mockVerifyAppleJwtWithJwks.mockResolvedValue( + applePayload({ + sub: 'apple-sub-1', + email: 'user@example.com', + email_verified: true, + nonce: 'wrong-nonce-digest', + }) + ); + + await expect(verifyNativeAppleIdToken('a-token', rawNonce)).rejects.toThrow(NativeIdTokenError); + await expect(verifyNativeAppleIdToken('a-token', rawNonce)).rejects.toThrow( + 'Apple nonce mismatch' + ); + }); + + it('accepts a token without a nonce and records legacy use', async () => { + mockVerifyAppleJwtWithJwks.mockResolvedValue( + applePayload({ sub: 'apple-sub-1', email: 'user@example.com', email_verified: true }) + ); + + const result = await verifyNativeAppleIdToken('a-token'); + expect(result).toEqual({ sub: 'apple-sub-1', email: 'user@example.com' }); + expect(mockCaptureMessage).toHaveBeenCalledWith('native_apple_nonce_legacy_count: 1'); + }); }); describe('verifyNativeGoogleIdToken', () => { @@ -144,7 +227,7 @@ describe('verifyNativeGoogleIdToken', () => { await expect(verifyNativeGoogleIdToken('g-token')).rejects.toThrow(NativeIdTokenError); }); - it('throws when verifyIdToken rejects (invalid token)', async () => { + it('throws when verifySignedJwtWithCertsAsync rejects (invalid token)', async () => { mockVerifySignedJwtWithCertsAsync.mockRejectedValue(new Error('Wrong number of segments')); await expect(verifyNativeGoogleIdToken('bad-token')).rejects.toThrow(NativeIdTokenError); @@ -177,3 +260,133 @@ describe('verifyNativeGoogleIdToken', () => { ); }); }); + +describe('exchangeNativeGoogleAuthCode', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockConfig.GOOGLE_CLIENT_SECRET = 'web-client-secret'; + }); + + it('exchanges a serverAuthCode and returns verified Google user data', async () => { + mockGetToken.mockResolvedValue({ + tokens: { id_token: 'exchanged-id-token' }, + }); + mockGetFederatedSignonCertsAsync.mockResolvedValue({ certs: { key: 'certificate' } }); + mockVerifySignedJwtWithCertsAsync.mockResolvedValue({ + getPayload: () => ({ + sub: 'google-sub-1', + email: 'user@example.com', + email_verified: true, + name: 'Google User', + picture: 'https://example.com/pic.png', + hd: 'example.com', + }), + }); + + const result = await exchangeNativeGoogleAuthCode('auth-code'); + expect(result).toEqual({ + sub: 'google-sub-1', + email: 'user@example.com', + name: 'Google User', + picture: 'https://example.com/pic.png', + hd: 'example.com', + }); + expect(mockGetToken).toHaveBeenCalledWith('auth-code'); + expect(mockGetFederatedSignonCertsAsync).toHaveBeenCalled(); + expect(mockVerifySignedJwtWithCertsAsync).toHaveBeenCalledWith( + 'exchanged-id-token', + { key: 'certificate' }, + ['web-client-id'], + ['accounts.google.com', 'https://accounts.google.com'] + ); + }); + + it('throws NativeIdTokenError when getToken fails with a server response (replayed code)', async () => { + const oauthError = Object.assign(new Error('invalid_grant'), { + response: { data: { error: 'invalid_grant' } }, + }); + mockGetToken.mockRejectedValue(oauthError); + + await expect(exchangeNativeGoogleAuthCode('replayed-code')).rejects.toThrow(NativeIdTokenError); + await expect(exchangeNativeGoogleAuthCode('replayed-code')).rejects.toThrow( + 'Google authorization code exchange failed' + ); + }); + + it('propagates network errors from getToken without wrapping (5xx path)', async () => { + const networkError = new Error('connect ECONNREFUSED'); + // No response property → network/infra failure. + mockGetToken.mockRejectedValue(networkError); + + await expect(exchangeNativeGoogleAuthCode('auth-code')).rejects.toBe(networkError); + }); + + it('throws NativeIdTokenError when token response has no id_token', async () => { + mockGetToken.mockResolvedValue({ tokens: {} }); + + await expect(exchangeNativeGoogleAuthCode('auth-code')).rejects.toThrow(NativeIdTokenError); + await expect(exchangeNativeGoogleAuthCode('auth-code')).rejects.toThrow( + 'Google token response missing id_token' + ); + }); + + it('throws NativeIdTokenError when email_verified is false', async () => { + mockGetToken.mockResolvedValue({ tokens: { id_token: 'exchanged-id-token' } }); + mockGetFederatedSignonCertsAsync.mockResolvedValue({ certs: { key: 'certificate' } }); + mockVerifySignedJwtWithCertsAsync.mockResolvedValue({ + getPayload: () => ({ + sub: 'google-sub-1', + email: 'user@example.com', + email_verified: false, + }), + }); + + await expect(exchangeNativeGoogleAuthCode('auth-code')).rejects.toThrow(NativeIdTokenError); + }); + + it('throws NativeIdTokenError when verifySignedJwtWithCertsAsync rejects (invalid token)', async () => { + mockGetToken.mockResolvedValue({ tokens: { id_token: 'exchanged-id-token' } }); + mockGetFederatedSignonCertsAsync.mockResolvedValue({ certs: { key: 'certificate' } }); + // google-auth-library throws a plain Error (no .response) for JWT verify failures. + mockVerifySignedJwtWithCertsAsync.mockRejectedValue(new Error('Wrong number of segments')); + + await expect(exchangeNativeGoogleAuthCode('auth-code')).rejects.toThrow(NativeIdTokenError); + await expect(exchangeNativeGoogleAuthCode('auth-code')).rejects.toThrow( + 'Google ID token verification failed after code exchange' + ); + }); + + it('propagates cert-fetch failures as 5xx (outside the token-verify try block)', async () => { + mockGetToken.mockResolvedValue({ tokens: { id_token: 'exchanged-id-token' } }); + const certError = new Error('Failed to retrieve verification certificates: network'); + mockGetFederatedSignonCertsAsync.mockRejectedValue(certError); + + await expect(exchangeNativeGoogleAuthCode('auth-code')).rejects.toBe(certError); + }); + + it('throws a plain Error when credentials are not configured', async () => { + mockConfig.GOOGLE_CLIENT_SECRET = ''; + // Reset the module-level mock to match (jest mocks persist across tests) + // Use a separate require approach — actually, the mockConfig is the source + // of truth for the getter. The function reads GOOGLE_CLIENT_SECRET at + // call time, so this should work. + await expect(exchangeNativeGoogleAuthCode('auth-code')).rejects.toThrow( + 'GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET is not configured' + ); + }); +}); + +// C12: Configuration invariant — the server GOOGLE_CLIENT_ID must equal the mobile +// GOOGLE_WEB_CLIENT_ID for the serverAuthCode exchange to succeed. The mobile test +// compares against the same expected value. +const EXPECTED_GOOGLE_WEB_CLIENT_ID = 'web-client-id'; + +describe('configuration invariants', () => { + it('GOOGLE_CLIENT_ID equals the expected web client ID (must match mobile GOOGLE_WEB_CLIENT_ID)', () => { + expect(GOOGLE_CLIENT_ID).toBe(EXPECTED_GOOGLE_WEB_CLIENT_ID); + }); + + it('GOOGLE_IOS_CLIENT_ID is distinct from GOOGLE_CLIENT_ID (they serve different audiences)', () => { + expect(GOOGLE_CLIENT_ID).not.toBe(GOOGLE_IOS_CLIENT_ID); + }); +}); diff --git a/apps/web/src/lib/auth/native-id-tokens.ts b/apps/web/src/lib/auth/native-id-tokens.ts index d0d21b5d34..b5af49d83f 100644 --- a/apps/web/src/lib/auth/native-id-tokens.ts +++ b/apps/web/src/lib/auth/native-id-tokens.ts @@ -1,11 +1,29 @@ import 'server-only'; +import { createHash } from 'node:crypto'; import { OAuth2Client } from 'google-auth-library'; import { verifyAppleJwtWithJwks } from '@/lib/auth/apple-jwks'; -import { GOOGLE_CLIENT_ID, GOOGLE_IOS_CLIENT_ID } from '@/lib/config.server'; +import { + APPLE_APP_BUNDLE_ID, + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_IOS_CLIENT_ID, +} from '@/lib/config.server'; +import { captureMessage } from '@sentry/nextjs'; /** Thrown when a native (mobile) ID token fails verification — maps to 401 INVALID_TOKEN. */ export class NativeIdTokenError extends Error {} +/** Returns true when the error carries an HTTP response from Google's servers, + * as opposed to a network or infrastructure failure. */ +function hasResponse(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'response' in error && + (error as { response: unknown }).response !== undefined + ); +} + export type VerifiedAppleIdToken = { sub: string; email: string }; export type VerifiedGoogleIdToken = { @@ -16,8 +34,31 @@ export type VerifiedGoogleIdToken = { hd?: string; }; -export async function verifyNativeAppleIdToken(idToken: string): Promise { - const payload = await verifyAppleJwtWithJwks(idToken, 'com.kilocode.kiloapp'); +export async function verifyNativeAppleIdToken( + idToken: string, + nonce?: string +): Promise { + // The Apple identity token's `aud` claim is the native app's bundle ID. Use + // the configured bundle ID and fall back to the shipped default so + // deployments that have not set APPLE_APP_BUNDLE_ID keep accepting tokens + // from the current release. + const audience = APPLE_APP_BUNDLE_ID || 'com.kilocode.kiloapp'; + const payload = await verifyAppleJwtWithJwks(idToken, audience); + + // The mobile client pre-computes the SHA-256 digest of the raw nonce and passes the + // digest to AppleAuthentication.signInAsync. Apple embeds the digest in the identity + // token payload as-is (no second hash). The server must compute SHA-256 of the raw + // nonce the mobile client sent and compare against payload.nonce. + if (nonce !== undefined) { + const expectedNonce = createHash('sha256').update(nonce).digest('hex'); + if (payload.nonce !== expectedNonce) { + throw new NativeIdTokenError('Apple nonce mismatch'); + } + } else { + // ponytail: remove legacy no-nonce path after all shipped builds send a nonce + // and the legacy counter has drained. + captureMessage('native_apple_nonce_legacy_count: 1'); + } if (typeof payload.email !== 'string' || !payload.email) { throw new NativeIdTokenError('Apple ID token missing email'); @@ -72,3 +113,75 @@ export async function verifyNativeGoogleIdToken(idToken: string): Promise { + if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) { + throw new Error('GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET is not configured'); + } + + const client = new OAuth2Client(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET); + + let tokenResponse; + try { + tokenResponse = await client.getToken(serverAuthCode); + } catch (error) { + // A replayed or expired authorization code surfaces as an OAuth error from + // Google with a response body. Network or infrastructure failures have no + // response — propagate those as server errors so the route returns 5xx. + if (hasResponse(error)) { + throw new NativeIdTokenError('Google authorization code exchange failed', { cause: error }); + } + throw error; + } + + const idToken = tokenResponse.tokens.id_token; + if (!idToken) { + throw new NativeIdTokenError('Google token response missing id_token'); + } + + // Verify the returned ID token against the web client audience and apply + // the same payload checks as the direct idToken path. Follow the same + // cert-fetch-outside / JWT-verify-inside pattern as verifyNativeGoogleIdToken: + // cert/network failures surface as 5xx; invalid tokens surface as 401. + const { certs } = await client.getFederatedSignonCertsAsync(); + let ticket; + try { + ticket = await client.verifySignedJwtWithCertsAsync( + idToken, + certs, + [GOOGLE_CLIENT_ID], + ['accounts.google.com', 'https://accounts.google.com'] + ); + } catch (error) { + throw new NativeIdTokenError('Google ID token verification failed after code exchange', { + cause: error, + }); + } + const payload = ticket.getPayload(); + + if (!payload) { + throw new NativeIdTokenError('Invalid Google ID token payload from code exchange'); + } + if (!payload.email_verified) { + throw new NativeIdTokenError('Google email not verified'); + } + if (!payload.email || !payload.sub) { + throw new NativeIdTokenError('Google ID token missing email or sub'); + } + + return { + sub: payload.sub, + email: payload.email, + name: payload.name, + picture: payload.picture, + hd: payload.hd, + }; +} diff --git a/apps/web/src/lib/config.server.ts b/apps/web/src/lib/config.server.ts index 2c24ee8aa4..5ad3ea8f7c 100644 --- a/apps/web/src/lib/config.server.ts +++ b/apps/web/src/lib/config.server.ts @@ -214,10 +214,25 @@ export const APPLE_CLIENT_ID = getEnvVariable('APPLE_CLIENT_ID'); export const APPLE_TEAM_ID = getEnvVariable('APPLE_TEAM_ID'); export const APPLE_KEY_ID = getEnvVariable('APPLE_KEY_ID'); export const APPLE_PRIVATE_KEY = getEnvVariable('APPLE_PRIVATE_KEY'); +export const APPLE_APP_BUNDLE_ID = getEnvVariable('APPLE_APP_BUNDLE_ID'); // Native Google client is a separate OAuth client from the web GOOGLE_CLIENT_ID. export const GOOGLE_IOS_CLIENT_ID = getEnvVariable('GOOGLE_IOS_CLIENT_ID'); +// Play Integrity verification config +export const GOOGLE_PLAY_INTEGRITY_PACKAGE_NAME = getEnvVariable( + 'GOOGLE_PLAY_INTEGRITY_PACKAGE_NAME' +); +export const GOOGLE_PLAY_INTEGRITY_CERT_DIGESTS = getEnvVariable( + 'GOOGLE_PLAY_INTEGRITY_CERT_DIGESTS' +); +export const GOOGLE_PLAY_INTEGRITY_PROJECT_NUMBER = getEnvVariable( + 'GOOGLE_PLAY_INTEGRITY_PROJECT_NUMBER' +); +export const GOOGLE_PLAY_INTEGRITY_SERVICE_ACCOUNT_KEY = getEnvVariable( + 'GOOGLE_PLAY_INTEGRITY_SERVICE_ACCOUNT_KEY' +); + // Posts user feedback into a fixed Slack channel in the Kilo workspace. // Expected to be a Slack Incoming Webhook URL. export const SLACK_USER_FEEDBACK_WEBHOOK_URL = getEnvVariable('SLACK_USER_FEEDBACK_WEBHOOK_URL'); diff --git a/apps/web/src/lib/device-auth/device-auth-viewer-token.test.ts b/apps/web/src/lib/device-auth/device-auth-viewer-token.test.ts new file mode 100644 index 0000000000..a9d356006c --- /dev/null +++ b/apps/web/src/lib/device-auth/device-auth-viewer-token.test.ts @@ -0,0 +1,71 @@ +import { describe, test, expect } from '@jest/globals'; +import { + createDeviceAuthViewerToken, + verifyDeviceAuthViewerToken, +} from './device-auth-viewer-token'; + +describe('device-auth-viewer-token', () => { + test('creates and verifies a valid token', () => { + const token = createDeviceAuthViewerToken('ABCD-EFGH', 'user-123'); + const verified = verifyDeviceAuthViewerToken(token); + + expect(verified).not.toBeNull(); + expect(verified!.code).toBe('ABCD-EFGH'); + expect(verified!.userId).toBe('user-123'); + }); + + test('returns null for null input', () => { + expect(verifyDeviceAuthViewerToken(null)).toBeNull(); + }); + + test('returns null for empty string', () => { + expect(verifyDeviceAuthViewerToken('')).toBeNull(); + }); + + test('returns null for token without separator', () => { + expect(verifyDeviceAuthViewerToken('justSomeString')).toBeNull(); + }); + + test('returns null for tampered payload', () => { + const token = createDeviceAuthViewerToken('ABCD-EFGH', 'user-123'); + // Replace the last char of the signature. + const tampered = token.slice(0, -1) + (token.at(-1) === 'a' ? 'b' : 'a'); + expect(verifyDeviceAuthViewerToken(tampered)).toBeNull(); + }); + + test('returns null for token with different code', () => { + const token = createDeviceAuthViewerToken('ABCD-EFGH', 'user-123'); + // Manually decode, change code, re-encode — verify must fail because the + // signature only covers the original payload. + const dotIdx = token.indexOf('.'); + const payload = Buffer.from(token.slice(0, dotIdx), 'base64url').toString('utf8'); + const obj = JSON.parse(payload) as Record; + obj.code = 'DIFFERENT'; + const newPayload = Buffer.from(JSON.stringify(obj)).toString('base64url'); + const sig = token.slice(dotIdx + 1); + expect(verifyDeviceAuthViewerToken(`${newPayload}.${sig}`)).toBeNull(); + }); + + test('returns null for token with different userId', () => { + const token = createDeviceAuthViewerToken('ABCD-EFGH', 'user-123'); + const dotIdx = token.indexOf('.'); + const payload = Buffer.from(token.slice(0, dotIdx), 'base64url').toString('utf8'); + const obj = JSON.parse(payload) as Record; + obj.userId = 'attacker'; + const newPayload = Buffer.from(JSON.stringify(obj)).toString('base64url'); + const sig = token.slice(dotIdx + 1); + expect(verifyDeviceAuthViewerToken(`${newPayload}.${sig}`)).toBeNull(); + }); + + test('tokens for different codes are distinct', () => { + const t1 = createDeviceAuthViewerToken('CODE-1111', 'user-1'); + const t2 = createDeviceAuthViewerToken('CODE-2222', 'user-1'); + expect(t1).not.toBe(t2); + }); + + test('tokens for different users are distinct', () => { + const t1 = createDeviceAuthViewerToken('CODE-1111', 'user-a'); + const t2 = createDeviceAuthViewerToken('CODE-1111', 'user-b'); + expect(t1).not.toBe(t2); + }); +}); diff --git a/apps/web/src/lib/device-auth/device-auth-viewer-token.ts b/apps/web/src/lib/device-auth/device-auth-viewer-token.ts new file mode 100644 index 0000000000..1707de6412 --- /dev/null +++ b/apps/web/src/lib/device-auth/device-auth-viewer-token.ts @@ -0,0 +1,75 @@ +import 'server-only'; +import crypto from 'node:crypto'; +import { NEXTAUTH_SECRET } from '@/lib/config.server'; + +const HMAC_ALGORITHM = 'sha256'; +const VIEWER_TOKEN_TTL_SECONDS = 10 * 60; +const NONCE_BYTES = 16; + +type DeviceAuthViewerTokenPayload = { + code: string; + userId: string; + iat: number; + nonce: string; +}; + +export type VerifiedDeviceAuthViewerToken = { + code: string; + userId: string; +}; + +function sign(data: string): string { + return crypto.createHmac(HMAC_ALGORITHM, NEXTAUTH_SECRET).update(data).digest('base64url'); +} + +export function createDeviceAuthViewerToken(code: string, userId: string): string { + const payload: DeviceAuthViewerTokenPayload = { + code, + userId, + iat: Math.floor(Date.now() / 1000), + nonce: crypto.randomBytes(NONCE_BYTES).toString('base64url'), + }; + const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url'); + return `${encodedPayload}.${sign(encodedPayload)}`; +} + +export function verifyDeviceAuthViewerToken( + token: string | null +): VerifiedDeviceAuthViewerToken | null { + if (!token) return null; + + const dotIndex = token.indexOf('.'); + if (dotIndex === -1) return null; + + const payload = token.slice(0, dotIndex); + const providedSig = token.slice(dotIndex + 1); + const expectedSig = sign(payload); + + if ( + providedSig.length !== expectedSig.length || + !crypto.timingSafeEqual(Buffer.from(providedSig), Buffer.from(expectedSig)) + ) { + return null; + } + + try { + const data = JSON.parse( + Buffer.from(payload, 'base64url').toString('utf8') + ) as Partial; + + if (typeof data.code !== 'string' || data.code.length === 0) return null; + if (typeof data.userId !== 'string' || data.userId.length === 0) return null; + if (typeof data.iat !== 'number') return null; + if (typeof data.nonce !== 'string' || data.nonce.length === 0) return null; + + const ageSeconds = Math.floor(Date.now() / 1000) - data.iat; + if (ageSeconds < 0 || ageSeconds > VIEWER_TOKEN_TTL_SECONDS) return null; + + return { + code: data.code, + userId: data.userId, + }; + } catch { + return null; + } +} diff --git a/apps/web/src/lib/device-auth/device-auth.test.ts b/apps/web/src/lib/device-auth/device-auth.test.ts index 1b5ca6905d..1c896060eb 100644 --- a/apps/web/src/lib/device-auth/device-auth.test.ts +++ b/apps/web/src/lib/device-auth/device-auth.test.ts @@ -1,17 +1,57 @@ import { describe, test, expect, beforeEach, afterEach } from '@jest/globals'; import { db } from '@/lib/drizzle'; -import { device_auth_requests, kilocode_users } from '@kilocode/db/schema'; +import { device_auth_requests, device_sessions, kilocode_users } from '@kilocode/db/schema'; import { eq } from 'drizzle-orm'; import { + generateUserCode, generateDeviceCode, + generateDeviceSecret, + hashDeviceSecret, createDeviceAuthRequest, getDeviceAuthRequest, approveDeviceAuthRequest, denyDeviceAuthRequest, pollDeviceAuthRequest, + consumeDeviceAuthByDeviceCode, isDeviceAuthRequestExpired, cleanupExpiredDeviceAuthRequests, + DeviceAuthPendingLimitError, + DEVICE_AUTH_PENDING_LIMIT_MESSAGE, } from './device-auth'; +import { issueSessionCredentials } from '@/lib/auth/device-sessions'; +import { generateApiToken } from '@/lib/tokens'; + +// Capture real implementations for pass-through default behaviour. +// Must use var declarations — jest.mock factories are hoisted and run before +// const/let initializers, but var is hoisted with an undefined initial value +// that is safe to assign to. +// eslint-disable-next-line no-var +var _realIssueSessionCredentials: ((...args: any[]) => any) | undefined; +// eslint-disable-next-line no-var +var _realGenerateApiToken: ((...args: any[]) => any) | undefined; + +jest.mock('@/lib/auth/device-sessions', () => { + const actual = jest.requireActual('@/lib/auth/device-sessions') as any; + _realIssueSessionCredentials = actual.issueSessionCredentials; + return { + ...actual, + issueSessionCredentials: jest.fn((...args: any[]) => + (_realIssueSessionCredentials as any)(...args) + ), + }; +}); + +jest.mock('@/lib/tokens', () => { + const actual = jest.requireActual('@/lib/tokens') as any; + _realGenerateApiToken = actual.generateApiToken; + return { + ...actual, + generateApiToken: jest.fn((...args: any[]) => (_realGenerateApiToken as any)(...args)), + }; +}); + +const mockedIssueSessionCredentials = jest.mocked(issueSessionCredentials); +const mockedGenerateApiToken = jest.mocked(generateApiToken); describe('Device Auth', () => { const testUserId = 'test-user-' + Date.now(); @@ -29,55 +69,140 @@ describe('Device Auth', () => { }); afterEach(async () => { + // Reset mocks to pass-through defaults. + mockedIssueSessionCredentials.mockImplementation((...args: any[]) => + (_realIssueSessionCredentials as any)(...args) + ); + mockedGenerateApiToken.mockImplementation((...args: any[]) => + (_realGenerateApiToken as any)(...args) + ); + jest.clearAllMocks(); + // Clean up test data await db.delete(device_auth_requests).where(eq(device_auth_requests.kilo_user_id, testUserId)); await db.delete(kilocode_users).where(eq(kilocode_users.id, testUserId)); }); - describe('generateDeviceCode', () => { + describe('generateUserCode', () => { test('generates a 9-character code with hyphen (XXXX-XXXX format)', () => { - const code = generateDeviceCode(); + const code = generateUserCode(); expect(code).toMatch(/^[A-Z2-9]{4}-[A-Z2-9]{4}$/); }); test('generates unique codes', () => { - const codes = new Set(); + const codes = new Set(); for (let i = 0; i < 100; i++) { - codes.add(generateDeviceCode()); + codes.add(generateUserCode()); } expect(codes.size).toBe(100); }); }); + describe('generateDeviceCode (deprecated alias)', () => { + test('is an alias for generateUserCode', () => { + // They are the same function reference. + expect(generateDeviceCode).toBe(generateUserCode); + }); + }); + + describe('generateDeviceSecret', () => { + test('generates a base64url-encoded 256-bit secret', () => { + const secret = generateDeviceSecret(); + expect(secret).toMatch(/^[A-Za-z0-9_-]+$/); + // 32 random bytes in base64url → at least 43 characters without padding. + expect(secret.length).toBeGreaterThanOrEqual(43); + }); + + test('generates unique secrets', () => { + const secrets = new Set(); + for (let i = 0; i < 100; i++) { + secrets.add(generateDeviceSecret()); + } + expect(secrets.size).toBe(100); + }); + }); + + describe('hashDeviceSecret', () => { + test('produces a deterministic SHA-256 hex digest', () => { + const secret = 'test-secret'; + const hash1 = hashDeviceSecret(secret); + const hash2 = hashDeviceSecret(secret); + expect(hash1).toBe(hash2); + // SHA-256 hex digest is 64 hex characters. + expect(hash1).toMatch(/^[a-f0-9]{64}$/); + }); + + test('produces different hashes for different secrets', () => { + const hash1 = hashDeviceSecret('secret-a'); + const hash2 = hashDeviceSecret('secret-b'); + expect(hash1).not.toBe(hash2); + }); + }); + describe('createDeviceAuthRequest', () => { - test('creates a new device auth request', async () => { + test('creates a new device auth request with both code and device code', async () => { const result = await createDeviceAuthRequest({ userAgent: 'test-agent', ipAddress: '127.0.0.1', }); expect(result.code).toMatch(/^[A-Z2-9]{4}-[A-Z2-9]{4}$/); + expect(result.userCode).toBe(result.code); + expect(result.deviceCode).toMatch(/^[A-Za-z0-9_-]+$/); expect(result.expiresAt).toBeInstanceOf(Date); expect(result.expiresAt.getTime()).toBeGreaterThan(Date.now()); const request = await getDeviceAuthRequest(result.code); expect(request).toBeDefined(); expect(request?.status).toBe('pending'); + expect(request?.user_code).toBe(result.userCode); + expect(request?.device_code_hash).toBe(hashDeviceSecret(result.deviceCode)); expect(request?.user_agent).toBe('test-agent'); expect(request?.ip_address).toBe('127.0.0.1'); }); - test('enforces rate limiting per IP', async () => { - const ipAddress = '192.168.1.1'; + test('enforces rate limiting per IP — live rows block, expired rows do not', async () => { + const ipAddress = `192.168.1.${(Date.now() * 7) % 254}`; + + // Clean up any leftover rows from previous runs with this IP. + await db.delete(device_auth_requests).where(eq(device_auth_requests.ip_address, ipAddress)); - // Create 5 pending requests (the limit) + // Create 5 expired pending requests with the same IP — these must not block a new request. + for (let i = 0; i < 5; i++) { + const { code } = await createDeviceAuthRequest({ ipAddress }); + // Manually expire each row so the pending count excludes them. + await db + .update(device_auth_requests) + .set({ + status: 'pending', + expires_at: new Date(Date.now() - 60_000).toISOString(), + }) + .where(eq(device_auth_requests.code, code)); + } + + // A new request from the same IP should succeed because all pending rows + // are expired. + const result = await createDeviceAuthRequest({ ipAddress }); + expect(result.code).toBeDefined(); + expect(result.userCode).toMatch(/^[A-Z2-9]{4}-[A-Z2-9]{4}$/); + }); + test('rejects the sixth live pending request from the same IP', async () => { + const ipAddress = `192.168.1.${(Date.now() * 11) % 254}`; + + // Clean up any leftover rows. + await db.delete(device_auth_requests).where(eq(device_auth_requests.ip_address, ipAddress)); + + // Create 5 live pending requests. for (let i = 0; i < 5; i++) { await createDeviceAuthRequest({ ipAddress }); } - // 6th request should fail + // The 6th request must be rejected. + await expect(createDeviceAuthRequest({ ipAddress })).rejects.toThrow( + DeviceAuthPendingLimitError + ); await expect(createDeviceAuthRequest({ ipAddress })).rejects.toThrow( - 'Too many pending authorization requests from this IP' + DEVICE_AUTH_PENDING_LIMIT_MESSAGE ); }); }); @@ -135,7 +260,254 @@ describe('Device Auth', () => { }); }); - describe('pollDeviceAuthRequest', () => { + describe('consumeDeviceAuthByDeviceCode', () => { + test('returns pending status for unapproved request', async () => { + const { deviceCode } = await createDeviceAuthRequest({}); + + const result = await consumeDeviceAuthByDeviceCode(deviceCode); + + expect(result.status).toBe('pending'); + expect(result.token).toBeUndefined(); + }); + + test('returns approved status with token for approved request', async () => { + const { code, deviceCode } = await createDeviceAuthRequest({}); + await approveDeviceAuthRequest(code, testUserId); + + const result = await consumeDeviceAuthByDeviceCode(deviceCode); + + expect(result.status).toBe('approved'); + expect(result.token).toBeDefined(); + expect(result.userId).toBe(testUserId); + expect(result.userEmail).toBe(testUserEmail); + }); + + test('returns denied status for denied request', async () => { + const { code, deviceCode } = await createDeviceAuthRequest({}); + await denyDeviceAuthRequest(code); + + const result = await consumeDeviceAuthByDeviceCode(deviceCode); + + expect(result.status).toBe('denied'); + expect(result.token).toBeUndefined(); + }); + + test('returns expired status for expired request', async () => { + const { code, deviceCode } = await createDeviceAuthRequest({}); + + // Manually expire the request + await db + .update(device_auth_requests) + .set({ expires_at: new Date(Date.now() - 1000).toISOString() }) + .where(eq(device_auth_requests.code, code)); + + const result = await consumeDeviceAuthByDeviceCode(deviceCode); + + expect(result.status).toBe('expired'); + expect(result.token).toBeUndefined(); + }); + + test('returns expired for non-existent device code', async () => { + const result = await consumeDeviceAuthByDeviceCode(generateDeviceSecret()); + expect(result.status).toBe('expired'); + }); + + test('returns consumed on second call (single-use)', async () => { + const { code, deviceCode } = await createDeviceAuthRequest({}); + await approveDeviceAuthRequest(code, testUserId); + + const first = await consumeDeviceAuthByDeviceCode(deviceCode); + expect(first.status).toBe('approved'); + expect(first.token).toBeDefined(); + + const second = await consumeDeviceAuthByDeviceCode(deviceCode); + // The second call returns the raw 'consumed' status; callers (route handlers) + // map it to 410 (expired) for client-facing responses. + expect(second.status).toBe('consumed'); + expect(second.token).toBeUndefined(); + }); + + test('rejects the user code (display code) — device secret is not the user code', async () => { + const { code, deviceCode } = await createDeviceAuthRequest({}); + await approveDeviceAuthRequest(code, testUserId); + + // Trying to consume with the displayed user code must fail. + const result = await consumeDeviceAuthByDeviceCode(code); + expect(result.status).toBe('expired'); + expect(result.token).toBeUndefined(); + + // The actual device secret still works. + const real = await consumeDeviceAuthByDeviceCode(deviceCode); + expect(real.status).toBe('approved'); + expect(real.token).toBeDefined(); + }); + + test('concurrent consumes mint at most one token', async () => { + // Run 20 iterations to ensure the race is reliably closed. + for (let i = 0; i < 20; i++) { + const { code, deviceCode } = await createDeviceAuthRequest({}); + await approveDeviceAuthRequest(code, testUserId); + + const [a, b] = await Promise.all([ + consumeDeviceAuthByDeviceCode(deviceCode), + consumeDeviceAuthByDeviceCode(deviceCode), + ]); + + const approved = [a, b].filter(r => r.status === 'approved'); + expect(approved.length).toBe(1); + expect(approved[0]!.token).toBeDefined(); + } + }); + + test('supportsRefresh: true creates a device session and issues short-lived token pair', async () => { + const { code, deviceCode } = await createDeviceAuthRequest({}); + await approveDeviceAuthRequest(code, testUserId); + + const result = await consumeDeviceAuthByDeviceCode(deviceCode, { + supportsRefresh: true, + }); + + expect(result.status).toBe('approved'); + expect(result.token).toBeDefined(); + expect(result.refreshToken).toBeDefined(); + expect(result.expiresIn).toBe(60 * 60); + expect(result.userId).toBe(testUserId); + }); + + test('supportsRefresh: false returns long-lived token only', async () => { + const { code, deviceCode } = await createDeviceAuthRequest({}); + await approveDeviceAuthRequest(code, testUserId); + + const result = await consumeDeviceAuthByDeviceCode(deviceCode, { + supportsRefresh: false, + }); + + expect(result.status).toBe('approved'); + expect(result.token).toBeDefined(); + expect(result.refreshToken).toBeUndefined(); + expect(result.expiresIn).toBeUndefined(); + }); + + test('returns denied for a blocked user (new path)', async () => { + const { code, deviceCode } = await createDeviceAuthRequest({}); + await approveDeviceAuthRequest(code, testUserId); + + // Block the user + await db + .update(kilocode_users) + .set({ blocked_reason: 'test block', blocked_at: new Date().toISOString() }) + .where(eq(kilocode_users.id, testUserId)); + + const result = await consumeDeviceAuthByDeviceCode(deviceCode); + + expect(result.status).toBe('denied'); + expect(result.token).toBeUndefined(); + + // Verify the request is durably denied, not consumed. + const request = await getDeviceAuthRequest(code); + expect(request?.status).toBe('denied'); + }); + + test('failed issuance after consume restores row and removes orphan session (supportsRefresh)', async () => { + mockedIssueSessionCredentials.mockRejectedValue(new Error('issuance failed')); + + const { code, deviceCode } = await createDeviceAuthRequest({}); + await approveDeviceAuthRequest(code, testUserId); + + // First consume fails because issueSessionCredentials throws. + await expect( + consumeDeviceAuthByDeviceCode(deviceCode, { supportsRefresh: true }) + ).rejects.toThrow('issuance failed'); + + // Row was restored to approved. + const request = await getDeviceAuthRequest(code); + expect(request?.status).toBe('approved'); + expect(request?.consumed_at).toBeNull(); + + // No device session was left behind. + const sessions = await db + .select() + .from(device_sessions) + .where(eq(device_sessions.device_auth_request_id, request!.id)); + expect(sessions).toHaveLength(0); + + // Reset mock to pass-through for the re-consume. + mockedIssueSessionCredentials.mockImplementation((...args: any[]) => + (_realIssueSessionCredentials as any)(...args) + ); + + // A later consume succeeds. + const retry = await consumeDeviceAuthByDeviceCode(deviceCode); + expect(retry.status).toBe('approved'); + expect(retry.token).toBeDefined(); + }); + + test('manually restored approved row is redeemable again', async () => { + // Manually restore a consumed row to approved status, simulating what the + // catch block does after a real issuance failure. A subsequent consume must + // succeed, proving the restore enables re-redemption. + const { code, deviceCode } = await createDeviceAuthRequest({}); + await approveDeviceAuthRequest(code, testUserId); + + // First consume succeeds. + const first = await consumeDeviceAuthByDeviceCode(deviceCode); + expect(first.status).toBe('approved'); + expect(first.token).toBeDefined(); + + // Manually restore the row (simulating what the catch block does on failure). + await db + .update(device_auth_requests) + .set({ status: 'approved', consumed_at: null }) + .where(eq(device_auth_requests.code, code)); + + // Second consume succeeds — the restored row is redeemable again. + const second = await consumeDeviceAuthByDeviceCode(deviceCode); + expect(second.status).toBe('approved'); + expect(second.token).toBeDefined(); + }); + + test('does not delete another request session during cleanup', async () => { + const { code: code1, deviceCode: deviceCode1 } = await createDeviceAuthRequest({}); + const { code: code2, deviceCode: deviceCode2 } = await createDeviceAuthRequest({}); + await approveDeviceAuthRequest(code1, testUserId); + await approveDeviceAuthRequest(code2, testUserId); + + // Consume code2 successfully with a session first. + const result2 = await consumeDeviceAuthByDeviceCode(deviceCode2, { supportsRefresh: true }); + expect(result2.status).toBe('approved'); + expect(result2.token).toBeDefined(); + + // Now make code1's issuance fail. + mockedIssueSessionCredentials.mockRejectedValue(new Error('issuance failed')); + + await expect( + consumeDeviceAuthByDeviceCode(deviceCode1, { supportsRefresh: true }) + ).rejects.toThrow('issuance failed'); + + // Code1's session was cleaned up. + const request1 = await getDeviceAuthRequest(code1); + const sessions1 = await db + .select() + .from(device_sessions) + .where(eq(device_sessions.device_auth_request_id, request1!.id)); + expect(sessions1).toHaveLength(0); + + // Code2's session must still exist. + const request2 = await getDeviceAuthRequest(code2); + const sessions2 = await db + .select() + .from(device_sessions) + .where(eq(device_sessions.device_auth_request_id, request2!.id)); + expect(sessions2).toHaveLength(1); + + // Reset mock. + mockedIssueSessionCredentials.mockImplementation((...args: any[]) => + (_realIssueSessionCredentials as any)(...args) + ); + }); + }); + + describe('pollDeviceAuthRequest (legacy)', () => { test('returns pending status for unapproved request', async () => { const { code } = await createDeviceAuthRequest({}); @@ -186,6 +558,83 @@ describe('Device Auth', () => { const result = await pollDeviceAuthRequest('XXX-XXX'); expect(result.status).toBe('expired'); }); + + test('concurrent legacy polls mint at most one token', async () => { + for (let i = 0; i < 20; i++) { + const { code } = await createDeviceAuthRequest({}); + await approveDeviceAuthRequest(code, testUserId); + + const [a, b] = await Promise.all([ + pollDeviceAuthRequest(code), + pollDeviceAuthRequest(code), + ]); + + const approved = [a, b].filter(r => r.status === 'approved'); + expect(approved.length).toBe(1); + expect(approved[0]!.token).toBeDefined(); + } + }); + + test('sequential legacy polls consume only once (single-use)', async () => { + const { code } = await createDeviceAuthRequest({}); + await approveDeviceAuthRequest(code, testUserId); + + const first = await pollDeviceAuthRequest(code); + expect(first.status).toBe('approved'); + expect(first.token).toBeDefined(); + + const second = await pollDeviceAuthRequest(code); + // After consume, the second sequential poll returns expired. + expect(second.status).toBe('expired'); + expect(second.token).toBeUndefined(); + }); + + test('returns denied for a blocked user (legacy path)', async () => { + const { code } = await createDeviceAuthRequest({}); + await approveDeviceAuthRequest(code, testUserId); + + // Block the user + await db + .update(kilocode_users) + .set({ blocked_reason: 'test block', blocked_at: new Date().toISOString() }) + .where(eq(kilocode_users.id, testUserId)); + + const result = await pollDeviceAuthRequest(code); + + expect(result.status).toBe('denied'); + expect(result.token).toBeUndefined(); + + // Verify the request is durably denied, not consumed. + const request = await getDeviceAuthRequest(code); + expect(request?.status).toBe('denied'); + }); + + test('failed token generation after consume restores row for re-redemption', async () => { + mockedGenerateApiToken.mockImplementation(() => { + throw new Error('token generation failed'); + }); + + const { code } = await createDeviceAuthRequest({}); + await approveDeviceAuthRequest(code, testUserId); + + // First poll fails because generateApiToken throws. + await expect(pollDeviceAuthRequest(code)).rejects.toThrow('token generation failed'); + + // Row was restored to approved. + const request = await getDeviceAuthRequest(code); + expect(request?.status).toBe('approved'); + expect(request?.consumed_at).toBeNull(); + + // Reset mock to pass-through for the re-consume. + mockedGenerateApiToken.mockImplementation((...args: any[]) => + (_realGenerateApiToken as any)(...args) + ); + + // A later poll succeeds. + const retry = await pollDeviceAuthRequest(code); + expect(retry.status).toBe('approved'); + expect(retry.token).toBeDefined(); + }); }); describe('isDeviceAuthRequestExpired', () => { @@ -236,46 +685,16 @@ describe('Device Auth', () => { expect(expiredRequest).toBeUndefined(); expect(validRequest).toBeDefined(); }); + }); - describe('Security Features', () => { - test('enforces single-use token - second poll returns expired', async () => { - const { code } = await createDeviceAuthRequest({}); - await approveDeviceAuthRequest(code, testUserId); - - // First poll should succeed - const firstResult = await pollDeviceAuthRequest(code); - expect(firstResult.status).toBe('approved'); - expect(firstResult.token).toBeDefined(); - - // Second poll should return expired (consumed) - const secondResult = await pollDeviceAuthRequest(code); - expect(secondResult.status).toBe('expired'); - expect(secondResult.token).toBeUndefined(); - }); - - test('normalizes responses - non-existent code returns expired', async () => { - const result = await pollDeviceAuthRequest('FAKE-CODE'); - expect(result.status).toBe('expired'); - }); - - test('normalizes responses - consumed code returns expired', async () => { - const { code } = await createDeviceAuthRequest({}); - await approveDeviceAuthRequest(code, testUserId); - - // Consume the code - await pollDeviceAuthRequest(code); - - // Polling again should return expired, not consumed - const result = await pollDeviceAuthRequest(code); - expect(result.status).toBe('expired'); - }); - - test('code entropy increased - generates 8 character codes', () => { - const code = generateDeviceCode(); - // Remove hyphen and check length - const codeWithoutHyphen = code.replace('-', ''); - expect(codeWithoutHyphen.length).toBe(8); - }); + describe('verificationUrl', () => { + test('contains the user code, never the device secret', () => { + const secret = generateDeviceSecret(); + // The verification URL is built from the user code only, so no code path + // ever puts the 256-bit device secret into a URL. We verify that the + // generateDeviceSecret output is clearly distinguishable from a user code. + expect(secret).toMatch(/^[A-Za-z0-9_-]+$/); + expect(secret.length).toBeGreaterThan(9); // user codes are 9 chars }); }); }); diff --git a/apps/web/src/lib/device-auth/device-auth.ts b/apps/web/src/lib/device-auth/device-auth.ts index 88578bad0f..4dd70f9327 100644 --- a/apps/web/src/lib/device-auth/device-auth.ts +++ b/apps/web/src/lib/device-auth/device-auth.ts @@ -1,19 +1,31 @@ import 'server-only'; import { db } from '@/lib/drizzle'; -import { device_auth_requests, kilocode_users } from '@kilocode/db/schema'; -import { eq, and, lt, sql } from 'drizzle-orm'; +import { device_auth_requests, device_sessions, kilocode_users } from '@kilocode/db/schema'; +import { eq, and, lt, gt, isNull, isNotNull, sql } from 'drizzle-orm'; import { generateApiToken } from '@/lib/tokens'; -import { randomInt } from 'node:crypto'; +import { randomInt, createHash, randomBytes } from 'node:crypto'; +import { createDeviceSession, issueSessionCredentials } from '@/lib/auth/device-sessions'; const CODE_LENGTH = 8; const CODE_EXPIRATION_MINUTES = 10; const MAX_PENDING_REQUESTS_PER_IP = 5; +export const DEVICE_AUTH_PENDING_LIMIT_MESSAGE = + 'Too many sign-in attempts from this network. Wait a few minutes and try again.'; + +/** Rate-limit error thrown when an IP has too many live pending requests. */ +export class DeviceAuthPendingLimitError extends Error { + constructor() { + super(DEVICE_AUTH_PENDING_LIMIT_MESSAGE); + this.name = 'DeviceAuthPendingLimitError'; + } +} + /** - * Generate a random device authorization code - * Uses only unambiguous characters for better UX + * Generate a random human-readable device authorization code. + * Uses only unambiguous characters for better UX. */ -export function generateDeviceCode(): string { +export function generateUserCode(): string { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; let code = ''; for (let i = 0; i < CODE_LENGTH; i++) { @@ -23,13 +35,33 @@ export function generateDeviceCode(): string { return `${code.slice(0, 4)}-${code.slice(4)}`; } +/** @deprecated — ponytail: remove after all shipped clients migrate to userCode/deviceCode split */ +export const generateDeviceCode = generateUserCode; + +/** + * Generate a high-entropy device secret for polling. + * 256-bit random value encoded as base64url. + */ +export function generateDeviceSecret(): string { + return randomBytes(32).toString('base64url'); +} + /** - * Create a new device authorization request + * Hash a device secret with SHA-256. + * The secret is already high-entropy, so a plain digest is sufficient. + */ +export function hashDeviceSecret(secret: string): string { + return createHash('sha256').update(secret).digest('hex'); +} + +/** + * Create a new device authorization request. + * Writes both the legacy code and new user_code/device_code_hash. */ export async function createDeviceAuthRequest(params: { userAgent?: string; ipAddress?: string; -}): Promise<{ code: string; expiresAt: Date }> { +}): Promise<{ code: string; userCode: string; deviceCode: string; expiresAt: Date }> { const { userAgent, ipAddress } = params; // Validate IP address on Production @@ -45,31 +77,36 @@ export async function createDeviceAuthRequest(params: { .where( and( eq(device_auth_requests.ip_address, ipAddress), - eq(device_auth_requests.status, 'pending') + eq(device_auth_requests.status, 'pending'), + gt(device_auth_requests.expires_at, new Date().toISOString()) ) ); if (result && result.count >= MAX_PENDING_REQUESTS_PER_IP) { - throw new Error('Too many pending authorization requests from this IP'); + throw new DeviceAuthPendingLimitError(); } } - const code = generateDeviceCode(); + const userCode = generateUserCode(); + const deviceSecret = generateDeviceSecret(); + const deviceCodeHash = hashDeviceSecret(deviceSecret); const expiresAt = new Date(Date.now() + CODE_EXPIRATION_MINUTES * 60 * 1000); await db.insert(device_auth_requests).values({ - code, + code: userCode, + user_code: userCode, + device_code_hash: deviceCodeHash, status: 'pending', expires_at: expiresAt.toISOString(), user_agent: userAgent, ip_address: ipAddress, }); - return { code, expiresAt }; + return { code: userCode, userCode, deviceCode: deviceSecret, expiresAt }; } /** - * Get device auth request by code + * Get device auth request by code (legacy path). */ export async function getDeviceAuthRequest(code: string) { const [request] = await db @@ -82,7 +119,7 @@ export async function getDeviceAuthRequest(code: string) { } /** - * Check if a device auth request has expired + * Check if a device auth request has expired. */ export function isDeviceAuthRequestExpired(request: { expires_at: string; @@ -92,7 +129,7 @@ export function isDeviceAuthRequestExpired(request: { } /** - * Approve a device authorization request + * Approve a device authorization request. */ export async function approveDeviceAuthRequest(code: string, userId: string): Promise { const request = await getDeviceAuthRequest(code); @@ -124,7 +161,7 @@ export async function approveDeviceAuthRequest(code: string, userId: string): Pr } /** - * Deny a device authorization request + * Deny a device authorization request. */ export async function denyDeviceAuthRequest(code: string): Promise { const request = await getDeviceAuthRequest(code); @@ -144,8 +181,134 @@ export async function denyDeviceAuthRequest(code: string): Promise { } /** - * Poll for device authorization status and return token if approved - * Implements single-use token enforcement + * Atomically consume an approved request by device code. + * Returns the appropriate status and mints a token only once per row. + * + * If credential issuance fails after the atomic consume succeeds, the row is + * restored to `approved` so the request remains redeemable. + */ +export async function consumeDeviceAuthByDeviceCode( + deviceCode: string, + options?: { supportsRefresh?: boolean } +): Promise<{ + status: 'pending' | 'approved' | 'denied' | 'expired' | 'consumed'; + token?: string; + refreshToken?: string; + expiresIn?: number; + userId?: string; + userEmail?: string; +}> { + const deviceCodeHash = hashDeviceSecret(deviceCode); + const now = new Date().toISOString(); + + // Atomic consume — UPDATE … RETURNING guarantees at most one caller succeeds. + const [consumed] = await db + .update(device_auth_requests) + .set({ status: 'consumed', consumed_at: now }) + .where( + and( + eq(device_auth_requests.device_code_hash, deviceCodeHash), + eq(device_auth_requests.status, 'approved'), + gt(device_auth_requests.expires_at, now), + isNull(device_auth_requests.consumed_at) + ) + ) + .returning(); + + if (!consumed) { + // Look up current status for the same hash. + const [row] = await db + .select({ + status: device_auth_requests.status, + expires_at: device_auth_requests.expires_at, + }) + .from(device_auth_requests) + .where(eq(device_auth_requests.device_code_hash, deviceCodeHash)) + .limit(1); + + if (!row) return { status: 'expired' }; + + if (row.status === 'expired' || new Date(row.expires_at) < new Date()) { + return { status: 'expired' }; + } + + return { status: row.status as 'pending' | 'denied' | 'consumed' }; + } + + // Only reachable after a successful atomic consume. + let sessionId: string | undefined; + try { + if (!consumed.kilo_user_id) { + throw new Error('Approved request has no user'); + } + + const [user] = await db + .select() + .from(kilocode_users) + .where(eq(kilocode_users.id, consumed.kilo_user_id)) + .limit(1); + + if (!user) { + throw new Error('User not found'); + } + + if (user.blocked_reason) { + await db + .update(device_auth_requests) + .set({ status: 'denied' }) + .where(eq(device_auth_requests.code, consumed.code)); + return { status: 'denied' }; + } + + const token = options?.supportsRefresh + ? undefined + : generateApiToken(user, { deviceAuthRequestCode: consumed.code }); + + if (options?.supportsRefresh) { + sessionId = await createDeviceSession({ + userId: user.id, + userAgent: consumed.user_agent ?? undefined, + deviceAuthRequestId: consumed.id, + }); + const pair = await issueSessionCredentials(user, sessionId); + return { + status: 'approved', + token: pair.token, + refreshToken: pair.refreshToken, + expiresIn: pair.expiresIn, + userId: user.id, + userEmail: user.google_user_email, + }; + } + + return { + status: 'approved', + token, + userId: user.id, + userEmail: user.google_user_email, + }; + } catch (err) { + // If a device session was created, delete it to avoid orphans. + if (sessionId) { + await db.delete(device_sessions).where(eq(device_sessions.id, sessionId)); + } + // Restore the row so the request remains redeemable. + await db + .update(device_auth_requests) + .set({ status: 'approved', consumed_at: null }) + .where( + and(eq(device_auth_requests.id, consumed.id), eq(device_auth_requests.status, 'consumed')) + ); + throw err; + } +} + +/** + * Poll for device authorization status and return token if approved. + * Legacy path — uses atomic consume to prevent double-spend. + * + * If credential issuance fails after the atomic consume succeeds, the row is + * restored to `approved` so the request remains redeemable. */ export async function pollDeviceAuthRequest(code: string): Promise<{ status: 'pending' | 'approved' | 'denied' | 'expired'; @@ -153,7 +316,77 @@ export async function pollDeviceAuthRequest(code: string): Promise<{ userId?: string; userEmail?: string; }> { - const request = await getDeviceAuthRequest(code); + const now = new Date().toISOString(); + + // Atomic consume — UPDATE … RETURNING guarantees at most one caller succeeds. + const [consumed] = await db + .update(device_auth_requests) + .set({ status: 'consumed', consumed_at: now }) + .where( + and( + eq(device_auth_requests.code, code), + eq(device_auth_requests.status, 'approved'), + isNotNull(device_auth_requests.kilo_user_id), + gt(device_auth_requests.expires_at, now), + isNull(device_auth_requests.consumed_at) + ) + ) + .returning(); + + if (consumed) { + // Only reachable after a successful atomic consume. + try { + const kiloUserId = consumed.kilo_user_id; + if (!kiloUserId) { + throw new Error('Approved request has no user'); + } + + const [user] = await db + .select() + .from(kilocode_users) + .where(eq(kilocode_users.id, kiloUserId)) + .limit(1); + + if (!user) { + throw new Error('User not found'); + } + + if (user.blocked_reason) { + await db + .update(device_auth_requests) + .set({ status: 'denied' }) + .where(eq(device_auth_requests.code, code)); + return { status: 'denied' }; + } + + const token = generateApiToken(user, { + deviceAuthRequestCode: consumed.code, + }); + + return { + status: 'approved', + token, + userId: user.id, + userEmail: user.google_user_email, + }; + } catch (err) { + // Restore the row so the request remains redeemable. + await db + .update(device_auth_requests) + .set({ status: 'approved', consumed_at: null }) + .where( + and(eq(device_auth_requests.id, consumed.id), eq(device_auth_requests.status, 'consumed')) + ); + throw err; + } + } + + // Fall through: not consumed, look up current status. + const [request] = await db + .select() + .from(device_auth_requests) + .where(eq(device_auth_requests.code, code)) + .limit(1); // Normalize response: return 'expired' for non-existent codes to prevent enumeration if (!request) { @@ -171,43 +404,17 @@ export async function pollDeviceAuthRequest(code: string): Promise<{ return { status: 'expired' }; } - // Return status for non-approved requests - if (request.status !== 'approved' || !request.kilo_user_id) { - return { status: request.status as 'pending' | 'denied' }; - } - - // For approved requests, fetch user and generate token - const [user] = await db - .select() - .from(kilocode_users) - .where(eq(kilocode_users.id, request.kilo_user_id)) - .limit(1); - - if (!user) { - throw new Error('User not found'); + // Return status for non-approved requests (or already-consumed) + if (request.status === 'consumed') { + return { status: 'expired' }; } - const token = generateApiToken(user, { deviceAuthRequestCode: code }); - - // Mark as consumed to enforce single-use - await db - .update(device_auth_requests) - .set({ - status: 'expired', - }) - .where(eq(device_auth_requests.code, code)); - - return { - status: 'approved', - token, - userId: user.id, - userEmail: user.google_user_email, - }; + return { status: request.status as 'pending' | 'denied' }; } /** - * Clean up expired device auth requests - * Should be called periodically (e.g., via cron job) + * Clean up expired device auth requests. + * Should be called periodically (e.g., via cron job). */ export async function cleanupExpiredDeviceAuthRequests(): Promise { const result = await db diff --git a/apps/web/src/lib/integrations/db/platform-integrations.test.ts b/apps/web/src/lib/integrations/db/platform-integrations.test.ts new file mode 100644 index 0000000000..327abe84bb --- /dev/null +++ b/apps/web/src/lib/integrations/db/platform-integrations.test.ts @@ -0,0 +1,743 @@ +import { afterEach, beforeEach, describe, expect, test } from '@jest/globals'; +import { db } from '@/lib/drizzle'; +import { platform_integrations, kilocode_users, organizations } from '@kilocode/db/schema'; +import { and, eq } from 'drizzle-orm'; +import { + deleteIntegration, + deleteIntegrationForOwner, + findIntegrationByInstallationId, + suspendIntegration, + suspendIntegrationForOwner, + unsuspendIntegration, + unsuspendIntegrationForOwner, + updateIntegrationRepositories, + upsertPlatformIntegrationForOwner, +} from './platform-integrations'; +import type { Owner } from '../core/types'; + +const INSTALLATION_ID = `test-github-install-${Date.now()}`; + +describe('upsertPlatformIntegrationForOwner', () => { + const userId = `test-upsert-user-${Date.now()}`; + const otherUserId = `test-upsert-other-user-${Date.now()}`; + const orgId = crypto.randomUUID(); + const otherOrgId = crypto.randomUUID(); + + beforeEach(async () => { + await db.insert(kilocode_users).values([ + { + id: userId, + google_user_email: `upsert-${Date.now()}-a@example.com`, + google_user_name: 'Upsert Test User A', + google_user_image_url: 'https://example.com/avatar.jpg', + stripe_customer_id: `cus_upsert_a_${Date.now()}`, + }, + { + id: otherUserId, + google_user_email: `upsert-${Date.now()}-b@example.com`, + google_user_name: 'Upsert Test User B', + google_user_image_url: 'https://example.com/avatar.jpg', + stripe_customer_id: `cus_upsert_b_${Date.now()}`, + }, + ]); + + await db.insert(organizations).values([ + { id: orgId, name: `Upsert Test Org A ${Date.now()}` }, + { id: otherOrgId, name: `Upsert Test Org B ${Date.now()}` }, + ]); + }); + + afterEach(async () => { + await db + .delete(platform_integrations) + .where(eq(platform_integrations.platform_installation_id, INSTALLATION_ID)); + await db + .delete(platform_integrations) + .where(eq(platform_integrations.owned_by_user_id, userId)); + await db + .delete(platform_integrations) + .where(eq(platform_integrations.owned_by_user_id, otherUserId)); + await db + .delete(platform_integrations) + .where(eq(platform_integrations.owned_by_organization_id, orgId)); + await db + .delete(platform_integrations) + .where(eq(platform_integrations.owned_by_organization_id, otherOrgId)); + await db.delete(organizations).where(eq(organizations.id, orgId)); + await db.delete(organizations).where(eq(organizations.id, otherOrgId)); + await db.delete(kilocode_users).where(eq(kilocode_users.id, userId)); + await db.delete(kilocode_users).where(eq(kilocode_users.id, otherUserId)); + }); + + const baseInstallData = (installationId: string) => ({ + platform: 'github', + integrationType: 'app', + platformInstallationId: installationId, + platformAccountId: '12345', + platformAccountLogin: 'test-owner', + permissions: null, + scopes: [], + repositoryAccess: 'all' as const, + repositories: null, + installedAt: new Date().toISOString(), + githubAppType: 'standard' as const, + }); + + test('inserts a new GitHub installation for a user owner', async () => { + const owner: Owner = { type: 'user', id: userId }; + const result = await upsertPlatformIntegrationForOwner(owner, baseInstallData(INSTALLATION_ID)); + + expect(result).toEqual({ ok: true }); + + const [row] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.platform_installation_id, INSTALLATION_ID)); + + expect(row).toBeDefined(); + expect(row.owned_by_user_id).toBe(userId); + expect(row.owned_by_organization_id).toBeNull(); + expect(row.platform).toBe('github'); + }); + + test('inserts a new GitHub installation for an org owner', async () => { + const owner: Owner = { type: 'org', id: orgId }; + const result = await upsertPlatformIntegrationForOwner(owner, baseInstallData(INSTALLATION_ID)); + + expect(result).toEqual({ ok: true }); + + const [row] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.platform_installation_id, INSTALLATION_ID)); + + expect(row.owned_by_user_id).toBeNull(); + expect(row.owned_by_organization_id).toBe(orgId); + }); + + test('same-owner refresh updates the existing row (by primary key)', async () => { + const owner: Owner = { type: 'user', id: userId }; + + // First insert. + await upsertPlatformIntegrationForOwner(owner, baseInstallData(INSTALLATION_ID)); + + // Second call with different account login — same-owner refresh. + const result = await upsertPlatformIntegrationForOwner(owner, { + ...baseInstallData(INSTALLATION_ID), + platformAccountLogin: 'new-login', + }); + + expect(result).toEqual({ ok: true }); + + const [row] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.platform_installation_id, INSTALLATION_ID)); + + expect(row.platform_account_login).toBe('new-login'); + // Ownership must not have changed. + expect(row.owned_by_user_id).toBe(userId); + expect(row.owned_by_organization_id).toBeNull(); + }); + + test('cross-owner collision returns claimed_by_other_owner without updating', async () => { + const ownerA: Owner = { type: 'user', id: userId }; + const ownerB: Owner = { type: 'user', id: otherUserId }; + + // Owner A claims the installation. + await upsertPlatformIntegrationForOwner(ownerA, baseInstallData(INSTALLATION_ID)); + + // Owner B tries to claim the same installation. + const result = await upsertPlatformIntegrationForOwner( + ownerB, + baseInstallData(INSTALLATION_ID) + ); + + expect(result).toEqual({ ok: false, reason: 'claimed_by_other_owner' }); + + // Ownership must still be owner A. + const [row] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.platform_installation_id, INSTALLATION_ID)); + + expect(row.owned_by_user_id).toBe(userId); + }); + + test('cross-owner type mismatch returns claimed_by_other_owner (user vs org)', async () => { + // Insert as a user owner. + const userOwner: Owner = { type: 'user', id: userId }; + await upsertPlatformIntegrationForOwner(userOwner, baseInstallData(INSTALLATION_ID)); + + // Try to upsert as an org owner. The owner type differs from the existing + // integration's user ownership, so the comparison must reject it. + const orgOwner: Owner = { type: 'org', id: orgId }; + const result = await upsertPlatformIntegrationForOwner( + orgOwner, + baseInstallData(INSTALLATION_ID) + ); + + expect(result).toEqual({ ok: false, reason: 'claimed_by_other_owner' }); + + // Ownership must still be the user owner. + const [row] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.platform_installation_id, INSTALLATION_ID)); + + expect(row.owned_by_user_id).toBe(userId); + expect(row.owned_by_organization_id).toBeNull(); + }); + + test('second insert after unique index hit same-owner refresh updates (edge case)', async () => { + const owner: Owner = { type: 'user', id: userId }; + + // Insert a row with onConflictDoNothing on global index. + const result1 = await upsertPlatformIntegrationForOwner( + owner, + baseInstallData(INSTALLATION_ID) + ); + expect(result1).toEqual({ ok: true }); + + // Same owner inserts again, skips onDoNothing, re-reads, finds same owner. + const result2 = await upsertPlatformIntegrationForOwner(owner, { + ...baseInstallData(INSTALLATION_ID), + platformAccountLogin: 'refreshed-login', + }); + expect(result2).toEqual({ ok: true }); + + // Verify the update happened. + const [row] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.platform_installation_id, INSTALLATION_ID)); + + expect(row.platform_account_login).toBe('refreshed-login'); + }); + + const crossTypeInstallId = `test-github-cross-type-${Date.now()}`; + + afterEach(async () => { + await db + .delete(platform_integrations) + .where(eq(platform_integrations.platform_installation_id, crossTypeInstallId)); + }); + + test('selects the correct row for the same installation id across app types', async () => { + const ownerA: Owner = { type: 'user', id: userId }; + const ownerB: Owner = { type: 'user', id: otherUserId }; + + await upsertPlatformIntegrationForOwner(ownerA, { + ...baseInstallData(crossTypeInstallId), + githubAppType: 'standard', + }); + await upsertPlatformIntegrationForOwner(ownerB, { + ...baseInstallData(crossTypeInstallId), + githubAppType: 'lite', + }); + + const standard = await findIntegrationByInstallationId( + 'github', + crossTypeInstallId, + 'standard' + ); + const lite = await findIntegrationByInstallationId('github', crossTypeInstallId, 'lite'); + + expect(standard?.owned_by_user_id).toBe(userId); + expect(standard?.github_app_type).toBe('standard'); + expect(lite?.owned_by_user_id).toBe(otherUserId); + expect(lite?.github_app_type).toBe('lite'); + }); + + test('cross-owner conflict is scoped to the app type', async () => { + const ownerA: Owner = { type: 'user', id: userId }; + const ownerB: Owner = { type: 'user', id: otherUserId }; + + // Owner A claims the installation with the standard app. + await upsertPlatformIntegrationForOwner(ownerA, { + ...baseInstallData(crossTypeInstallId), + githubAppType: 'standard', + }); + + // Owner B cannot claim the same standard row. + const blocked = await upsertPlatformIntegrationForOwner(ownerB, { + ...baseInstallData(crossTypeInstallId), + githubAppType: 'standard', + }); + expect(blocked).toEqual({ ok: false, reason: 'claimed_by_other_owner' }); + + // Owner B can claim the same installation id under the lite app. + const liteClaim = await upsertPlatformIntegrationForOwner(ownerB, { + ...baseInstallData(crossTypeInstallId), + githubAppType: 'lite', + }); + expect(liteClaim).toEqual({ ok: true }); + + const rows = await db + .select() + .from(platform_integrations) + .where( + and( + eq(platform_integrations.platform, 'github'), + eq(platform_integrations.platform_installation_id, crossTypeInstallId) + ) + ); + expect(rows).toHaveLength(2); + }); + + test('same-owner refresh with app type is not confused by another owner other-app-type row', async () => { + const ownerA: Owner = { type: 'user', id: userId }; + const ownerB: Owner = { type: 'user', id: otherUserId }; + + // Owner B claims the lite row first so an unscoped lookup would find it. + await upsertPlatformIntegrationForOwner(ownerB, { + ...baseInstallData(crossTypeInstallId), + githubAppType: 'lite', + }); + + // Owner A claims the standard row. + const claimed = await upsertPlatformIntegrationForOwner(ownerA, { + ...baseInstallData(crossTypeInstallId), + githubAppType: 'standard', + }); + expect(claimed).toEqual({ ok: true }); + + // Owner A refreshes the standard row — must not hit owner B's lite row. + const refreshed = await upsertPlatformIntegrationForOwner(ownerA, { + ...baseInstallData(crossTypeInstallId), + githubAppType: 'standard', + platformAccountLogin: 'refreshed-login', + }); + expect(refreshed).toEqual({ ok: true }); + + const standard = await findIntegrationByInstallationId( + 'github', + crossTypeInstallId, + 'standard' + ); + const lite = await findIntegrationByInstallationId('github', crossTypeInstallId, 'lite'); + + expect(standard?.owned_by_user_id).toBe(userId); + expect(standard?.platform_account_login).toBe('refreshed-login'); + expect(standard?.github_app_type).toBe('standard'); + expect(lite?.owned_by_user_id).toBe(otherUserId); + expect(lite?.github_app_type).toBe('lite'); + }); + + describe('app-type-scoped destructive mutations', () => { + const destructiveInstallId = `test-github-destructive-${Date.now()}`; + const siblingInstallId = `test-github-destructive-sibling-${Date.now()}`; + + async function getRowsByInstallId() { + return db + .select() + .from(platform_integrations) + .where( + and( + eq(platform_integrations.platform, 'github'), + eq(platform_integrations.platform_installation_id, destructiveInstallId) + ) + ); + } + + async function getOrgRows() { + return db + .select() + .from(platform_integrations) + .where( + and( + eq(platform_integrations.platform, 'github'), + eq(platform_integrations.owned_by_organization_id, orgId) + ) + ); + } + + async function getUserRows() { + return db + .select() + .from(platform_integrations) + .where( + and( + eq(platform_integrations.platform, 'github'), + eq(platform_integrations.owned_by_user_id, userId) + ) + ); + } + + // The owner unique index `(owner, platform, installation_id)` forbids two + // rows for the same owner and installation id, so an owner's standard and + // lite rows live on separate installations. An unscoped owner mutation + // (app-type predicate removed) would touch both rows; the app-type scoped + // one must leave the lite sibling untouched. + async function seedOrgSiblings() { + await db.insert(platform_integrations).values([ + { + owned_by_organization_id: orgId, + platform: 'github', + integration_type: 'app', + platform_installation_id: destructiveInstallId, + platform_account_id: '1000', + platform_account_login: 'org-a', + repository_access: 'all', + integration_status: 'active', + github_app_type: 'standard', + }, + { + owned_by_organization_id: orgId, + platform: 'github', + integration_type: 'app', + platform_installation_id: siblingInstallId, + platform_account_id: '2000', + platform_account_login: 'org-b', + repository_access: 'all', + integration_status: 'active', + github_app_type: 'lite', + }, + ]); + } + + async function seedUserSiblings() { + await db.insert(platform_integrations).values([ + { + owned_by_user_id: userId, + platform: 'github', + integration_type: 'app', + platform_installation_id: destructiveInstallId, + platform_account_id: '3000', + platform_account_login: 'user-a', + repository_access: 'all', + integration_status: 'active', + github_app_type: 'standard', + }, + { + owned_by_user_id: userId, + platform: 'github', + integration_type: 'app', + platform_installation_id: siblingInstallId, + platform_account_id: '4000', + platform_account_login: 'user-b', + repository_access: 'all', + integration_status: 'active', + github_app_type: 'lite', + }, + ]); + } + + // The installation-scoped update matches by installation id, so its + // app-type predicate only matters when two rows share one installation id. + // That arrangement is legal only across owners (one row per app type). + async function seedOrgInstallationSiblings() { + await db.insert(platform_integrations).values([ + { + owned_by_organization_id: orgId, + platform: 'github', + integration_type: 'app', + platform_installation_id: destructiveInstallId, + platform_account_id: '1000', + platform_account_login: 'org-a', + repository_access: 'all', + integration_status: 'active', + github_app_type: 'standard', + }, + { + owned_by_organization_id: otherOrgId, + platform: 'github', + integration_type: 'app', + platform_installation_id: destructiveInstallId, + platform_account_id: '2000', + platform_account_login: 'org-b', + repository_access: 'all', + integration_status: 'active', + github_app_type: 'lite', + }, + ]); + } + + test('deleteIntegration with app type leaves the owner sibling app-type row', async () => { + await seedOrgSiblings(); + + await deleteIntegration(orgId, 'github', 'standard'); + + const rows = await getOrgRows(); + expect(rows).toHaveLength(1); + expect(rows[0].platform_installation_id).toBe(siblingInstallId); + expect(rows[0].github_app_type).toBe('lite'); + }); + + test('suspendIntegration with app type leaves the owner sibling app-type row active', async () => { + await seedOrgSiblings(); + + await suspendIntegration(orgId, 'github', 'webhook-sender', 'standard'); + + const rows = await getOrgRows(); + expect(rows).toHaveLength(2); + const standard = rows.find(row => row.github_app_type === 'standard'); + const lite = rows.find(row => row.github_app_type === 'lite'); + expect(standard?.integration_status).toBe('suspended'); + expect(standard?.suspended_by).toBe('webhook-sender'); + expect(lite?.integration_status).toBe('active'); + expect(lite?.suspended_at).toBeNull(); + }); + + test('unsuspendIntegration with app type leaves the owner sibling app-type row suspended', async () => { + await seedOrgSiblings(); + await db + .update(platform_integrations) + .set({ integration_status: 'suspended' }) + .where( + and( + eq(platform_integrations.platform, 'github'), + eq(platform_integrations.owned_by_organization_id, orgId) + ) + ); + + await unsuspendIntegration(orgId, 'github', 'standard'); + + const rows = await getOrgRows(); + expect(rows).toHaveLength(2); + const standard = rows.find(row => row.github_app_type === 'standard'); + const lite = rows.find(row => row.github_app_type === 'lite'); + expect(standard?.integration_status).toBe('active'); + expect(lite?.integration_status).toBe('suspended'); + }); + + test('deleteIntegrationForOwner with app type leaves the owner sibling app-type row', async () => { + await seedUserSiblings(); + + await deleteIntegrationForOwner({ type: 'user', id: userId }, 'github', 'standard'); + + const rows = await getUserRows(); + expect(rows).toHaveLength(1); + expect(rows[0].platform_installation_id).toBe(siblingInstallId); + expect(rows[0].github_app_type).toBe('lite'); + }); + + test('suspendIntegrationForOwner with app type leaves the owner sibling app-type row active', async () => { + await seedUserSiblings(); + + await suspendIntegrationForOwner( + { type: 'user', id: userId }, + 'github', + 'webhook-sender', + 'standard' + ); + + const rows = await getUserRows(); + expect(rows).toHaveLength(2); + const standard = rows.find(row => row.github_app_type === 'standard'); + const lite = rows.find(row => row.github_app_type === 'lite'); + expect(standard?.integration_status).toBe('suspended'); + expect(lite?.integration_status).toBe('active'); + }); + + test('unsuspendIntegrationForOwner with app type leaves the owner sibling app-type row suspended', async () => { + await seedUserSiblings(); + await db + .update(platform_integrations) + .set({ integration_status: 'suspended' }) + .where( + and( + eq(platform_integrations.platform, 'github'), + eq(platform_integrations.owned_by_user_id, userId) + ) + ); + + await unsuspendIntegrationForOwner({ type: 'user', id: userId }, 'github', 'standard'); + + const rows = await getUserRows(); + expect(rows).toHaveLength(2); + const standard = rows.find(row => row.github_app_type === 'standard'); + const lite = rows.find(row => row.github_app_type === 'lite'); + expect(standard?.integration_status).toBe('active'); + expect(lite?.integration_status).toBe('suspended'); + }); + + test('updateIntegrationRepositories with app type updates only the matched row', async () => { + await seedOrgInstallationSiblings(); + + await updateIntegrationRepositories( + 'github', + destructiveInstallId, + [{ id: 999, name: 'new-repo', full_name: 'acme/new-repo', private: false }], + 'standard' + ); + + const rows = await getRowsByInstallId(); + expect(rows).toHaveLength(2); + const standard = rows.find(row => row.github_app_type === 'standard'); + const lite = rows.find(row => row.github_app_type === 'lite'); + expect(standard?.repositories).toEqual([ + { id: 999, name: 'new-repo', full_name: 'acme/new-repo', private: false }, + ]); + expect(lite?.repositories).toBeNull(); + }); + + // Two rows for the same owner, same app type, but different installation + // ids are legal (the GitHub unique index is keyed per installation id). + // A mutation scoped only to owner and app type would touch both rows; the + // installation-scoped one must leave the sibling untouched. + async function seedOrgSameAppTypeSiblings() { + await db.insert(platform_integrations).values([ + { + owned_by_organization_id: orgId, + platform: 'github', + integration_type: 'app', + platform_installation_id: destructiveInstallId, + platform_account_id: '1000', + platform_account_login: 'org-a', + repository_access: 'all', + integration_status: 'active', + github_app_type: 'standard', + }, + { + owned_by_organization_id: orgId, + platform: 'github', + integration_type: 'app', + platform_installation_id: siblingInstallId, + platform_account_id: '2000', + platform_account_login: 'org-b', + repository_access: 'all', + integration_status: 'active', + github_app_type: 'standard', + }, + ]); + } + + async function seedUserSameAppTypeSiblings() { + await db.insert(platform_integrations).values([ + { + owned_by_user_id: userId, + platform: 'github', + integration_type: 'app', + platform_installation_id: destructiveInstallId, + platform_account_id: '3000', + platform_account_login: 'user-a', + repository_access: 'all', + integration_status: 'active', + github_app_type: 'standard', + }, + { + owned_by_user_id: userId, + platform: 'github', + integration_type: 'app', + platform_installation_id: siblingInstallId, + platform_account_id: '4000', + platform_account_login: 'user-b', + repository_access: 'all', + integration_status: 'active', + github_app_type: 'standard', + }, + ]); + } + + test('deleteIntegration with installation id leaves the same-owner same-app-type sibling', async () => { + await seedOrgSameAppTypeSiblings(); + + await deleteIntegration(orgId, 'github', 'standard', destructiveInstallId); + + const rows = await getOrgRows(); + expect(rows).toHaveLength(1); + expect(rows[0].platform_installation_id).toBe(siblingInstallId); + expect(rows[0].github_app_type).toBe('standard'); + }); + + test('suspendIntegration with installation id leaves the same-owner same-app-type sibling active', async () => { + await seedOrgSameAppTypeSiblings(); + + await suspendIntegration(orgId, 'github', 'webhook-sender', 'standard', destructiveInstallId); + + const rows = await getOrgRows(); + expect(rows).toHaveLength(2); + const matched = rows.find(row => row.platform_installation_id === destructiveInstallId); + const sibling = rows.find(row => row.platform_installation_id === siblingInstallId); + expect(matched?.integration_status).toBe('suspended'); + expect(matched?.suspended_by).toBe('webhook-sender'); + expect(sibling?.integration_status).toBe('active'); + expect(sibling?.suspended_at).toBeNull(); + }); + + test('unsuspendIntegration with installation id leaves the same-owner same-app-type sibling suspended', async () => { + await seedOrgSameAppTypeSiblings(); + await db + .update(platform_integrations) + .set({ integration_status: 'suspended' }) + .where( + and( + eq(platform_integrations.platform, 'github'), + eq(platform_integrations.owned_by_organization_id, orgId) + ) + ); + + await unsuspendIntegration(orgId, 'github', 'standard', destructiveInstallId); + + const rows = await getOrgRows(); + expect(rows).toHaveLength(2); + const matched = rows.find(row => row.platform_installation_id === destructiveInstallId); + const sibling = rows.find(row => row.platform_installation_id === siblingInstallId); + expect(matched?.integration_status).toBe('active'); + expect(sibling?.integration_status).toBe('suspended'); + }); + + test('deleteIntegrationForOwner with installation id leaves the same-owner same-app-type sibling', async () => { + await seedUserSameAppTypeSiblings(); + + await deleteIntegrationForOwner( + { type: 'user', id: userId }, + 'github', + 'standard', + destructiveInstallId + ); + + const rows = await getUserRows(); + expect(rows).toHaveLength(1); + expect(rows[0].platform_installation_id).toBe(siblingInstallId); + expect(rows[0].github_app_type).toBe('standard'); + }); + + test('suspendIntegrationForOwner with installation id leaves the same-owner same-app-type sibling active', async () => { + await seedUserSameAppTypeSiblings(); + + await suspendIntegrationForOwner( + { type: 'user', id: userId }, + 'github', + 'webhook-sender', + 'standard', + destructiveInstallId + ); + + const rows = await getUserRows(); + expect(rows).toHaveLength(2); + const matched = rows.find(row => row.platform_installation_id === destructiveInstallId); + const sibling = rows.find(row => row.platform_installation_id === siblingInstallId); + expect(matched?.integration_status).toBe('suspended'); + expect(sibling?.integration_status).toBe('active'); + }); + + test('unsuspendIntegrationForOwner with installation id leaves the same-owner same-app-type sibling suspended', async () => { + await seedUserSameAppTypeSiblings(); + await db + .update(platform_integrations) + .set({ integration_status: 'suspended' }) + .where( + and( + eq(platform_integrations.platform, 'github'), + eq(platform_integrations.owned_by_user_id, userId) + ) + ); + + await unsuspendIntegrationForOwner( + { type: 'user', id: userId }, + 'github', + 'standard', + destructiveInstallId + ); + + const rows = await getUserRows(); + expect(rows).toHaveLength(2); + const matched = rows.find(row => row.platform_installation_id === destructiveInstallId); + const sibling = rows.find(row => row.platform_installation_id === siblingInstallId); + expect(matched?.integration_status).toBe('active'); + expect(sibling?.integration_status).toBe('suspended'); + }); + }); +}); diff --git a/apps/web/src/lib/integrations/db/platform-integrations.ts b/apps/web/src/lib/integrations/db/platform-integrations.ts index 648c6cf945..c4fcc9ce7a 100644 --- a/apps/web/src/lib/integrations/db/platform-integrations.ts +++ b/apps/web/src/lib/integrations/db/platform-integrations.ts @@ -15,23 +15,33 @@ import { PendingInstallationMetadataWrapperSchema } from '../core/schemas'; import type { GitHubAppType } from '../platforms/github/app-selector'; /** - * Finds a platform integration by installation ID + * Finds a platform integration by installation ID. + * + * GitHub uniqueness is keyed on `(platform, github_app_type, installation_id)`, + * so the same installation ID can exist once per app type (owned by different + * owners). Callers that know the app type must pass `githubAppType` to select + * the correct row. Callers without an app type (e.g. legacy bot-link states) + * keep the unscoped lookup, which preserves legacy behavior. */ export async function findIntegrationByInstallationId( platform: string, - installationId: string | undefined + installationId: string | undefined, + githubAppType?: GitHubAppType ) { if (!installationId) return null; + const conditions = [ + eq(platform_integrations.platform, platform), + eq(platform_integrations.platform_installation_id, installationId), + ]; + if (githubAppType) { + conditions.push(eq(platform_integrations.github_app_type, githubAppType)); + } + const [integration] = await db .select() .from(platform_integrations) - .where( - and( - eq(platform_integrations.platform, platform), - eq(platform_integrations.platform_installation_id, installationId) - ) - ) + .where(and(...conditions)) .limit(1); return integration || null; @@ -137,13 +147,25 @@ export async function upsertPlatformIntegration(data: { } /** - * Updates repository list for an integration by installation ID + * Updates repository list for an integration by installation ID. + * GitHub rows are unique per `(platform, github_app_type, installation_id)`; + * pass `githubAppType` when the caller knows it so the update targets only the + * matching standard or lite row. */ export async function updateIntegrationRepositories( platform: string, installationId: string, - repositories: PlatformRepository[] + repositories: PlatformRepository[], + githubAppType?: GitHubAppType ) { + const conditions = [ + eq(platform_integrations.platform, platform), + eq(platform_integrations.platform_installation_id, installationId), + ]; + if (githubAppType) { + conditions.push(eq(platform_integrations.github_app_type, githubAppType)); + } + await db .update(platform_integrations) .set({ @@ -153,12 +175,7 @@ export async function updateIntegrationRepositories( auth_invalid_reason: null, updated_at: new Date().toISOString(), }) - .where( - and( - eq(platform_integrations.platform, platform), - eq(platform_integrations.platform_installation_id, installationId) - ) - ); + .where(and(...conditions)); } /** @@ -196,13 +213,32 @@ export async function updateIntegrationAccountIdentity( } /** - * Suspends a platform integration + * Suspends a platform integration. + * + * GitHub rows are unique per `(platform, github_app_type, installation_id)`, + * so an organization can hold both a standard and a lite row. Pass + * `githubAppType` when the caller knows it so a suspend touches only the + * matching app-type row. Pass `installationId` when the caller already + * matched a specific installation so the suspend touches only that row. */ export async function suspendIntegration( organizationId: string, platform: string, - suspendedBy: string + suspendedBy: string, + githubAppType?: GitHubAppType, + installationId?: string ) { + const conditions = [ + eq(platform_integrations.owned_by_organization_id, organizationId), + eq(platform_integrations.platform, platform), + ]; + if (githubAppType) { + conditions.push(eq(platform_integrations.github_app_type, githubAppType)); + } + if (installationId) { + conditions.push(eq(platform_integrations.platform_installation_id, installationId)); + } + await db .update(platform_integrations) .set({ @@ -211,18 +247,34 @@ export async function suspendIntegration( suspended_by: suspendedBy, updated_at: new Date().toISOString(), }) - .where( - and( - eq(platform_integrations.owned_by_organization_id, organizationId), - eq(platform_integrations.platform, platform) - ) - ); + .where(and(...conditions)); } /** - * Unsuspends a platform integration + * Unsuspends a platform integration. + * + * Pass `githubAppType` when the caller knows it so an unsuspend touches only + * the matching standard or lite row. Pass `installationId` when the caller + * already matched a specific installation so the unsuspend touches only that + * row. */ -export async function unsuspendIntegration(organizationId: string, platform: string) { +export async function unsuspendIntegration( + organizationId: string, + platform: string, + githubAppType?: GitHubAppType, + installationId?: string +) { + const conditions = [ + eq(platform_integrations.owned_by_organization_id, organizationId), + eq(platform_integrations.platform, platform), + ]; + if (githubAppType) { + conditions.push(eq(platform_integrations.github_app_type, githubAppType)); + } + if (installationId) { + conditions.push(eq(platform_integrations.platform_installation_id, installationId)); + } + await db .update(platform_integrations) .set({ @@ -231,26 +283,34 @@ export async function unsuspendIntegration(organizationId: string, platform: str suspended_by: null, updated_at: new Date().toISOString(), }) - .where( - and( - eq(platform_integrations.owned_by_organization_id, organizationId), - eq(platform_integrations.platform, platform) - ) - ); + .where(and(...conditions)); } /** - * Deletes a platform integration + * Deletes a platform integration. + * + * Pass `githubAppType` when the caller knows it so a delete removes only the + * matching standard or lite row. Pass `installationId` when the caller + * already matched a specific installation so the delete removes only that row. */ -export async function deleteIntegration(organizationId: string, platform: string) { - await db - .delete(platform_integrations) - .where( - and( - eq(platform_integrations.owned_by_organization_id, organizationId), - eq(platform_integrations.platform, platform) - ) - ); +export async function deleteIntegration( + organizationId: string, + platform: string, + githubAppType?: GitHubAppType, + installationId?: string +) { + const conditions = [ + eq(platform_integrations.owned_by_organization_id, organizationId), + eq(platform_integrations.platform, platform), + ]; + if (githubAppType) { + conditions.push(eq(platform_integrations.github_app_type, githubAppType)); + } + if (installationId) { + conditions.push(eq(platform_integrations.platform_installation_id, installationId)); + } + + await db.delete(platform_integrations).where(and(...conditions)); } /** @@ -493,32 +553,61 @@ export async function getAllIntegrationsForOwner(owner: Owner) { } /** - * Deletes a platform integration for an owner (user or organization) + * Deletes a platform integration for an owner (user or organization). + * + * Pass `githubAppType` when the caller knows it so a delete removes only the + * matching standard or lite row. Pass `installationId` when the caller + * already matched a specific installation so the delete removes only that row. */ -export async function deleteIntegrationForOwner(owner: Owner, platform: string) { +export async function deleteIntegrationForOwner( + owner: Owner, + platform: string, + githubAppType?: GitHubAppType, + installationId?: string +) { const ownershipCondition = owner.type === 'user' ? eq(platform_integrations.owned_by_user_id, owner.id) : eq(platform_integrations.owned_by_organization_id, owner.id); - await db - .delete(platform_integrations) - .where(and(ownershipCondition, eq(platform_integrations.platform, platform))); + const conditions = [ownershipCondition, eq(platform_integrations.platform, platform)]; + if (githubAppType) { + conditions.push(eq(platform_integrations.github_app_type, githubAppType)); + } + if (installationId) { + conditions.push(eq(platform_integrations.platform_installation_id, installationId)); + } + + await db.delete(platform_integrations).where(and(...conditions)); } /** - * Suspends a platform integration for an owner (user or organization) + * Suspends a platform integration for an owner (user or organization). + * + * Pass `githubAppType` when the caller knows it so a suspend touches only the + * matching standard or lite row. Pass `installationId` when the caller + * already matched a specific installation so the suspend touches only that row. */ export async function suspendIntegrationForOwner( owner: Owner, platform: string, - suspendedBy: string + suspendedBy: string, + githubAppType?: GitHubAppType, + installationId?: string ) { const ownershipCondition = owner.type === 'user' ? eq(platform_integrations.owned_by_user_id, owner.id) : eq(platform_integrations.owned_by_organization_id, owner.id); + const conditions = [ownershipCondition, eq(platform_integrations.platform, platform)]; + if (githubAppType) { + conditions.push(eq(platform_integrations.github_app_type, githubAppType)); + } + if (installationId) { + conditions.push(eq(platform_integrations.platform_installation_id, installationId)); + } + await db .update(platform_integrations) .set({ @@ -527,18 +616,36 @@ export async function suspendIntegrationForOwner( suspended_by: suspendedBy, updated_at: new Date().toISOString(), }) - .where(and(ownershipCondition, eq(platform_integrations.platform, platform))); + .where(and(...conditions)); } /** - * Unsuspends a platform integration for an owner (user or organization) + * Unsuspends a platform integration for an owner (user or organization). + * + * Pass `githubAppType` when the caller knows it so an unsuspend touches only + * the matching standard or lite row. Pass `installationId` when the caller + * already matched a specific installation so the unsuspend touches only that + * row. */ -export async function unsuspendIntegrationForOwner(owner: Owner, platform: string) { +export async function unsuspendIntegrationForOwner( + owner: Owner, + platform: string, + githubAppType?: GitHubAppType, + installationId?: string +) { const ownershipCondition = owner.type === 'user' ? eq(platform_integrations.owned_by_user_id, owner.id) : eq(platform_integrations.owned_by_organization_id, owner.id); + const conditions = [ownershipCondition, eq(platform_integrations.platform, platform)]; + if (githubAppType) { + conditions.push(eq(platform_integrations.github_app_type, githubAppType)); + } + if (installationId) { + conditions.push(eq(platform_integrations.platform_installation_id, installationId)); + } + await db .update(platform_integrations) .set({ @@ -547,13 +654,21 @@ export async function unsuspendIntegrationForOwner(owner: Owner, platform: strin suspended_by: null, updated_at: new Date().toISOString(), }) - .where(and(ownershipCondition, eq(platform_integrations.platform, platform))); + .where(and(...conditions)); } +export type UpsertPlatformIntegrationResult = + | { ok: true } + | { ok: false; reason: 'claimed_by_other_owner' }; + /** - * Owner-aware upsert for platform integrations - * Supports both user and organization ownership - * Uses atomic INSERT ... ON CONFLICT DO UPDATE to prevent race conditions + * Owner-aware upsert for platform integrations. + * Supports both user and organization ownership. + * + * For GitHub installations, the function prevents cross-owner theft: + * an insert targeting the global unique index uses `onConflictDoNothing`, + * and a blocked insert re-reads the owner before any update. Ownership + * columns are never set in conflict-update targets or SET clauses. */ export async function upsertPlatformIntegrationForOwner( owner: Owner, @@ -570,14 +685,102 @@ export async function upsertPlatformIntegrationForOwner( installedAt?: string; githubAppType?: GitHubAppType; } -) { - // Build ownership condition based on owner type +): Promise { + const appType = data.githubAppType ?? 'standard'; + + // Build values object used for both insert paths. + const values = { + owned_by_user_id: owner.type === 'user' ? owner.id : null, + owned_by_organization_id: owner.type === 'org' ? owner.id : null, + platform: data.platform, + integration_type: data.integrationType, + platform_installation_id: data.platformInstallationId, + platform_account_id: data.platformAccountId || null, + platform_account_login: data.platformAccountLogin || null, + permissions: (data.permissions as IntegrationPermissions) ?? null, + scopes: data.scopes || null, + repository_access: data.repositoryAccess, + integration_status: INTEGRATION_STATUS.ACTIVE, + repositories: data.repositories || null, + installed_at: data.installedAt || new Date().toISOString(), + github_app_type: appType, + }; + + // GitHub installations use a conflict-safe two-step pattern. + // Step 1: try insert with onConflictDoNothing on the global unique index. + // Step 2: if the insert was blocked, re-read the row and determine + // whether this is a same-owner refresh or a cross-owner claim. + if (data.platform === 'github') { + const inserted = await db + .insert(platform_integrations) + .values(values) + .onConflictDoNothing() + .returning({ id: platform_integrations.id }); + + if (inserted.length > 0) { + return { ok: true }; + } + + // Insert was blocked — another row claims this installation. Look up the + // row with the same app type as the unique index + // `(platform, github_app_type, installation_id)` so a standard refresh is + // never matched against a lite row (or vice versa). + const existing = await findIntegrationByInstallationId( + 'github', + data.platformInstallationId, + appType + ); + + if (!existing) { + // The blocked insert produced no row and we cannot find the + // existing row. This is an edge case from a concurrent delete. + // Retry the insert: this time without onConflictDoNothing so + // the DB enforces uniqueness (or the call throws). + await db.insert(platform_integrations).values(values); + return { ok: true }; + } + + // Compare owners by type and id — a matching id with a different + // owner type must not refresh another owner's integration. + const sameOwner = + (owner.type === 'user' && + existing.owned_by_user_id === owner.id && + existing.owned_by_organization_id === null) || + (owner.type === 'org' && + existing.owned_by_organization_id === owner.id && + existing.owned_by_user_id === null); + + if (sameOwner) { + // Same-owner refresh: update by primary key. + await db + .update(platform_integrations) + .set({ + platform_account_id: values.platform_account_id, + platform_account_login: values.platform_account_login, + permissions: values.permissions, + scopes: values.scopes, + repository_access: values.repository_access, + integration_status: INTEGRATION_STATUS.ACTIVE, + repositories: values.repositories, + github_app_type: appType, + auth_invalid_at: null, + auth_invalid_reason: null, + updated_at: new Date().toISOString(), + }) + .where(eq(platform_integrations.id, existing.id)); + return { ok: true }; + } + + // Cross-owner claim: refused. + return { ok: false, reason: 'claimed_by_other_owner' }; + } + + // Non-GitHub platforms use the existing per-owner pattern. const ownershipCondition = owner.type === 'user' ? eq(platform_integrations.owned_by_user_id, owner.id) : eq(platform_integrations.owned_by_organization_id, owner.id); - // Check if integration exists const [existing] = await db .select() .from(platform_integrations) @@ -591,42 +794,27 @@ export async function upsertPlatformIntegrationForOwner( .limit(1); if (existing) { - // Update existing integration await db .update(platform_integrations) .set({ - platform_account_id: data.platformAccountId || null, - platform_account_login: data.platformAccountLogin || null, - permissions: (data.permissions as IntegrationPermissions) ?? null, - scopes: data.scopes || null, - repository_access: data.repositoryAccess, + platform_account_id: values.platform_account_id, + platform_account_login: values.platform_account_login, + permissions: values.permissions, + scopes: values.scopes, + repository_access: values.repository_access, integration_status: INTEGRATION_STATUS.ACTIVE, - repositories: data.repositories || null, - github_app_type: data.githubAppType || existing.github_app_type, + repositories: values.repositories, + github_app_type: appType, auth_invalid_at: null, auth_invalid_reason: null, updated_at: new Date().toISOString(), }) .where(eq(platform_integrations.id, existing.id)); } else { - // Insert new integration - await db.insert(platform_integrations).values({ - owned_by_user_id: owner.type === 'user' ? owner.id : null, - owned_by_organization_id: owner.type === 'org' ? owner.id : null, - platform: data.platform, - integration_type: data.integrationType, - platform_installation_id: data.platformInstallationId, - platform_account_id: data.platformAccountId || null, - platform_account_login: data.platformAccountLogin || null, - permissions: (data.permissions as IntegrationPermissions) ?? null, - scopes: data.scopes || null, - repository_access: data.repositoryAccess, - integration_status: INTEGRATION_STATUS.ACTIVE, - repositories: data.repositories || null, - installed_at: data.installedAt || new Date().toISOString(), - github_app_type: data.githubAppType || 'standard', - }); + await db.insert(platform_integrations).values(values); } + + return { ok: true }; } /** diff --git a/apps/web/src/lib/integrations/github/install-state.test.ts b/apps/web/src/lib/integrations/github/install-state.test.ts new file mode 100644 index 0000000000..a91d2bd1e9 --- /dev/null +++ b/apps/web/src/lib/integrations/github/install-state.test.ts @@ -0,0 +1,191 @@ +import { describe, test, expect, beforeEach, afterEach } from '@jest/globals'; +import { db } from '@/lib/drizzle'; +import { github_install_states, kilocode_users } from '@kilocode/db/schema'; +import { eq, sql } from 'drizzle-orm'; +import { + createInstallState, + consumeInstallState, + cleanupExpiredInstallStates, +} from './install-state'; + +describe('install-state', () => { + const testUserId = 'test-install-user-' + Date.now(); + const testUserEmail = `test-install-${Date.now()}@example.com`; + + beforeEach(async () => { + await db.insert(kilocode_users).values({ + id: testUserId, + google_user_email: testUserEmail, + google_user_name: 'Test Install User', + google_user_image_url: 'https://example.com/avatar.jpg', + stripe_customer_id: 'cus_test_install', + }); + }); + + afterEach(async () => { + await db + .delete(github_install_states) + .where(eq(github_install_states.kilo_user_id, testUserId)); + await db.delete(kilocode_users).where(eq(kilocode_users.id, testUserId)); + }); + + describe('createInstallState', () => { + test('creates a row and returns a base64url token', async () => { + const token = await createInstallState({ + kiloUserId: testUserId, + ownerType: 'org', + ownerId: 'org-1', + githubAppType: 'standard', + returnTo: '/github-app', + }); + + expect(token).toBeTruthy(); + expect(typeof token).toBe('string'); + // 32 random bytes base64url-encoded = 43 characters (no padding) + expect(token).toMatch(/^[A-Za-z0-9_-]+$/); + }); + + test('stores the row in the database', async () => { + const token = await createInstallState({ + kiloUserId: testUserId, + ownerType: 'user', + ownerId: testUserId, + githubAppType: 'lite', + returnTo: null, + }); + + const rows = await db + .select() + .from(github_install_states) + .where(eq(github_install_states.token, token)); + + expect(rows).toHaveLength(1); + expect(rows[0].kilo_user_id).toBe(testUserId); + expect(rows[0].owner_type).toBe('user'); + expect(rows[0].owner_id).toBe(testUserId); + expect(rows[0].github_app_type).toBe('lite'); + expect(rows[0].return_to).toBeNull(); + expect(rows[0].consumed_at).toBeNull(); + expect(rows[0].expires_at).toBeTruthy(); + }); + }); + + describe('consumeInstallState', () => { + test('consumes a fresh token exactly once', async () => { + const token = await createInstallState({ + kiloUserId: testUserId, + ownerType: 'org', + ownerId: 'org-2', + githubAppType: 'standard', + }); + + const first = await consumeInstallState(token); + expect(first).not.toBeNull(); + expect(first!.token).toBe(token); + expect(first!.consumed_at).toBeTruthy(); + + const second = await consumeInstallState(token); + expect(second).toBeNull(); + }); + + test('returns null for an unknown token', async () => { + const result = await consumeInstallState('nonexistent-token'); + expect(result).toBeNull(); + }); + + test('returns null for an expired token', async () => { + // Insert a token with an already-past expires_at + const expiredToken = 'expired-test-token-' + Date.now(); + await db.insert(github_install_states).values({ + token: expiredToken, + kilo_user_id: testUserId, + owner_type: 'user', + owner_id: testUserId, + github_app_type: 'standard', + expires_at: new Date(Date.now() - 1000).toISOString(), + }); + + const result = await consumeInstallState(expiredToken); + expect(result).toBeNull(); + }); + + test('returns null for a consumed token', async () => { + const token = await createInstallState({ + kiloUserId: testUserId, + ownerType: 'org', + ownerId: 'org-3', + githubAppType: 'standard', + }); + + // Pre-consume by setting consumed_at directly + await db + .update(github_install_states) + .set({ consumed_at: sql`NOW()` }) + .where(eq(github_install_states.token, token)); + + const result = await consumeInstallState(token); + expect(result).toBeNull(); + }); + + test('returns the full row with all fields', async () => { + const token = await createInstallState({ + kiloUserId: testUserId, + ownerType: 'org', + ownerId: 'org-4', + githubAppType: 'lite', + returnTo: '/some/path', + }); + + const row = await consumeInstallState(token); + expect(row).not.toBeNull(); + expect(row!.kilo_user_id).toBe(testUserId); + expect(row!.owner_type).toBe('org'); + expect(row!.owner_id).toBe('org-4'); + expect(row!.github_app_type).toBe('lite'); + expect(row!.return_to).toBe('/some/path'); + }); + }); + + describe('cleanupExpiredInstallStates', () => { + test('deletes expired rows', async () => { + // Insert an already-expired row + await db.insert(github_install_states).values({ + token: 'cleanup-expired-' + Date.now(), + kilo_user_id: testUserId, + owner_type: 'user', + owner_id: testUserId, + github_app_type: 'standard', + expires_at: new Date(Date.now() - 60_000).toISOString(), + }); + + const deleted = await cleanupExpiredInstallStates(); + expect(deleted).toBeGreaterThanOrEqual(1); + + // Verify the expired row is gone + const remaining = await db + .select() + .from(github_install_states) + .where(eq(github_install_states.kilo_user_id, testUserId)); + + expect(remaining.filter(r => r.expires_at <= new Date().toISOString())).toHaveLength(0); + }); + + test('does not delete non-expired rows', async () => { + const token = await createInstallState({ + kiloUserId: testUserId, + ownerType: 'org', + ownerId: 'org-5', + githubAppType: 'standard', + }); + + await cleanupExpiredInstallStates(); + + const rows = await db + .select() + .from(github_install_states) + .where(eq(github_install_states.token, token)); + + expect(rows).toHaveLength(1); + }); + }); +}); diff --git a/apps/web/src/lib/integrations/github/install-state.ts b/apps/web/src/lib/integrations/github/install-state.ts new file mode 100644 index 0000000000..e9f3c1c7e9 --- /dev/null +++ b/apps/web/src/lib/integrations/github/install-state.ts @@ -0,0 +1,73 @@ +import 'server-only'; +import crypto from 'node:crypto'; +import { db } from '@/lib/drizzle'; +import { sql, eq, and, isNull } from 'drizzle-orm'; +import { github_install_states, type GitHubInstallState } from '@kilocode/db/schema'; +import { validateReturnPath } from '@/lib/integrations/validate-return-path'; + +const STATE_TTL_MINUTES = 10; + +export type CreateInstallStateParams = { + kiloUserId: string; + ownerType: 'org' | 'user'; + ownerId: string; + githubAppType: string; + returnTo?: string | null; +}; + +/** + * Creates a one-time GitHub install state token. + * The token is 32 random bytes, base64url-encoded. + * The row expires after 10 minutes. + */ +export async function createInstallState(params: CreateInstallStateParams): Promise { + const token = crypto.randomBytes(32).toString('base64url'); + const expiresAt = new Date(Date.now() + STATE_TTL_MINUTES * 60 * 1000).toISOString(); + + const validatedReturnTo = params.returnTo ? validateReturnPath(params.returnTo) : null; + + await db.insert(github_install_states).values({ + token, + kilo_user_id: params.kiloUserId, + owner_type: params.ownerType, + owner_id: params.ownerId, + github_app_type: params.githubAppType, + return_to: validatedReturnTo, + expires_at: expiresAt, + }); + + return token; +} + +/** + * Atomically consumes a GitHub install state token. + * A single UPDATE ... WHERE consumed_at IS NULL AND expires_at > NOW() RETURNING *. + * Returns null if the token is already consumed, expired, or unknown. + */ +export async function consumeInstallState(token: string): Promise { + const result = await db + .update(github_install_states) + .set({ consumed_at: sql`NOW()` }) + .where( + and( + eq(github_install_states.token, token), + isNull(github_install_states.consumed_at), + sql`${github_install_states.expires_at} > NOW()` + ) + ) + .returning(); + + return result[0] ?? null; +} + +/** + * Deletes install state rows whose expires_at has passed. + * Returns the count of deleted rows. + */ +export async function cleanupExpiredInstallStates(): Promise { + const result = await db + .delete(github_install_states) + .where(sql`${github_install_states.expires_at} <= NOW()`); + + return result.rowCount ?? 0; +} diff --git a/apps/web/src/lib/integrations/platforms/github/adapter.ts b/apps/web/src/lib/integrations/platforms/github/adapter.ts index a9a14c2235..5679edd199 100644 --- a/apps/web/src/lib/integrations/platforms/github/adapter.ts +++ b/apps/web/src/lib/integrations/platforms/github/adapter.ts @@ -412,6 +412,7 @@ export async function exchangeGitHubOAuthCode( ): Promise<{ id: string; login: string; + accessToken: string; }> { const credentials = getGitHubAppCredentials(appType); @@ -441,6 +442,7 @@ export async function exchangeGitHubOAuthCode( return { id: githubUser.id.toString(), login: githubUser.login, + accessToken, }; } diff --git a/apps/web/src/lib/integrations/platforms/github/app-selector.test.ts b/apps/web/src/lib/integrations/platforms/github/app-selector.test.ts new file mode 100644 index 0000000000..3ce4873f9f --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/github/app-selector.test.ts @@ -0,0 +1,125 @@ +import { beforeEach, describe, expect, jest, test } from '@jest/globals'; + +const mockListInstallationsForAuthenticatedUser = + jest.fn< + (_params: { + per_page: number; + page: number; + }) => Promise<{ data: { total_count: number; installations: Array<{ id: number }> } }> + >(); + +jest.mock( + '@octokit/rest', + () => + ({ + Octokit: jest.fn().mockImplementation(() => ({ + rest: { + apps: { + listInstallationsForAuthenticatedUser: mockListInstallationsForAuthenticatedUser, + }, + }, + })), + }) as never +); + +let assertUserAdministersInstallation: (params: { + accessToken: string; + installationId: number | string; +}) => Promise; + +beforeAll(async () => { + const mod = await import('./app-selector'); + assertUserAdministersInstallation = mod.assertUserAdministersInstallation; +}); + +const INSTALLATION_ID = 98765; + +function mockPage(installations: Array<{ id: number }>, totalCount: number) { + mockListInstallationsForAuthenticatedUser.mockResolvedValueOnce({ + data: { + total_count: totalCount, + installations, + }, + }); +} + +describe('assertUserAdministersInstallation', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('returns true when the installation is on the first page', async () => { + mockPage([{ id: INSTALLATION_ID }, { id: 11111 }], 2); + + const result = await assertUserAdministersInstallation({ + accessToken: 'test-token', + installationId: INSTALLATION_ID, + }); + + expect(result).toBe(true); + expect(mockListInstallationsForAuthenticatedUser).toHaveBeenCalledTimes(1); + expect(mockListInstallationsForAuthenticatedUser).toHaveBeenCalledWith({ + per_page: 100, + page: 1, + }); + }); + + test('returns true when the installation is on page two of a paginated result', async () => { + // Page 1: 100 installations, none matching. + const page1 = Array.from({ length: 100 }, (_, i) => ({ id: 10000 + i })); + mockPage(page1, 101); + + // Page 2: 1 installation, matching. + mockPage([{ id: INSTALLATION_ID }], 101); + + const result = await assertUserAdministersInstallation({ + accessToken: 'test-token', + installationId: INSTALLATION_ID, + }); + + expect(result).toBe(true); + expect(mockListInstallationsForAuthenticatedUser).toHaveBeenCalledTimes(2); + expect(mockListInstallationsForAuthenticatedUser).toHaveBeenNthCalledWith(1, { + per_page: 100, + page: 1, + }); + expect(mockListInstallationsForAuthenticatedUser).toHaveBeenNthCalledWith(2, { + per_page: 100, + page: 2, + }); + }); + + test('returns false when the installation is absent from all pages', async () => { + mockPage([{ id: 11111 }, { id: 22222 }], 2); + + const result = await assertUserAdministersInstallation({ + accessToken: 'test-token', + installationId: INSTALLATION_ID, + }); + + expect(result).toBe(false); + expect(mockListInstallationsForAuthenticatedUser).toHaveBeenCalledTimes(1); + }); + + test('accepts installationId as a string', async () => { + mockPage([{ id: INSTALLATION_ID }], 1); + + const result = await assertUserAdministersInstallation({ + accessToken: 'test-token', + installationId: '98765', + }); + + expect(result).toBe(true); + }); + + test('throws on API error rather than returning false', async () => { + mockListInstallationsForAuthenticatedUser.mockRejectedValueOnce(new Error('Bad credentials')); + + await expect( + assertUserAdministersInstallation({ + accessToken: 'test-token', + installationId: INSTALLATION_ID, + }) + ).rejects.toThrow('Bad credentials'); + }); +}); diff --git a/apps/web/src/lib/integrations/platforms/github/app-selector.ts b/apps/web/src/lib/integrations/platforms/github/app-selector.ts index 5e93ce113c..56b21994ab 100644 --- a/apps/web/src/lib/integrations/platforms/github/app-selector.ts +++ b/apps/web/src/lib/integrations/platforms/github/app-selector.ts @@ -1,5 +1,6 @@ import { getOrganizationById } from '@/lib/organizations/organizations'; import { getEnvVariable } from '@/lib/dotenvx'; +import { Octokit } from '@octokit/rest'; /** * Type of GitHub App to use @@ -83,3 +84,46 @@ export function getGitHubAppName(appType: GitHubAppType): string { } return process.env.NEXT_PUBLIC_GITHUB_APP_NAME || 'KiloConnect'; } + +/** + * Asserts that the GitHub user administers the given installation. + * + * Calls GET /user/installations with the user access token through Octokit. + * Paginates through all results. Returns true when installationId appears. + * + * @param params.accessToken - A user-scoped OAuth access token. + * @param params.installationId - The GitHub App installation ID to check. + * @returns true when the user administers the installation. + * @throws Error for network or API failures — never returns false for those. + */ +export async function assertUserAdministersInstallation(params: { + accessToken: string; + installationId: number | string; +}): Promise { + const { accessToken, installationId } = params; + const targetId = + typeof installationId === 'string' ? parseInt(installationId, 10) : installationId; + + const octokit = new Octokit({ auth: accessToken }); + + let page = 1; + const perPage = 100; + + while (true) { + const { data } = await octokit.rest.apps.listInstallationsForAuthenticatedUser({ + per_page: perPage, + page, + }); + + for (const installation of data.installations) { + if (installation.id === targetId) { + return true; + } + } + + if (data.installations.length < perPage) break; + page++; + } + + return false; +} diff --git a/apps/web/src/lib/integrations/platforms/github/webhook-handler.test.ts b/apps/web/src/lib/integrations/platforms/github/webhook-handler.test.ts index 977f403ba3..2cd9383bd8 100644 --- a/apps/web/src/lib/integrations/platforms/github/webhook-handler.test.ts +++ b/apps/web/src/lib/integrations/platforms/github/webhook-handler.test.ts @@ -11,6 +11,10 @@ const mockHandlePRReviewComment = jest.fn(); const mockHandleGitHubReviewCommentReply = jest.fn(); const mockHandleInstallationTargetRenamed = jest.fn(); const mockRevokeStoredGitHubUserAuthorization = jest.fn(); +const mockHandleInstallationDeleted = jest.fn(); +const mockHandleInstallationSuspend = jest.fn(); +const mockHandleInstallationUnsuspend = jest.fn(); +const mockHandleInstallationRepositories = jest.fn(); jest.mock('@/lib/integrations/platforms/github/adapter', () => ({ verifyGitHubWebhookSignature: (payload: string, signature: string, appType: string) => @@ -18,8 +22,11 @@ jest.mock('@/lib/integrations/platforms/github/adapter', () => ({ })); jest.mock('@/lib/integrations/db/platform-integrations', () => ({ - findIntegrationByInstallationId: (platform: string, installationId: string | undefined) => - mockFindIntegrationByInstallationId(platform, installationId), + findIntegrationByInstallationId: ( + platform: string, + installationId: string | undefined, + githubAppType?: string + ) => mockFindIntegrationByInstallationId(platform, installationId, githubAppType), })); jest.mock('@/lib/integrations/db/webhook-events', () => ({ @@ -35,10 +42,14 @@ jest.mock('@/lib/integrations/platforms/github/user-authorization', () => ({ jest.mock('@/lib/integrations/platforms/github/webhook-handlers', () => ({ handleInstallationCreated: jest.fn(), - handleInstallationDeleted: jest.fn(), - handleInstallationRepositories: jest.fn(), - handleInstallationSuspend: jest.fn(), - handleInstallationUnsuspend: jest.fn(), + handleInstallationDeleted: (payload: unknown, appType: string) => + mockHandleInstallationDeleted(payload, appType), + handleInstallationRepositories: (payload: unknown, appType: string) => + mockHandleInstallationRepositories(payload, appType), + handleInstallationSuspend: (payload: unknown, appType: string) => + mockHandleInstallationSuspend(payload, appType), + handleInstallationUnsuspend: (payload: unknown, appType: string) => + mockHandleInstallationUnsuspend(payload, appType), handleInstallationTargetRenamed: (payload: unknown, integrationId: string, appType: string) => mockHandleInstallationTargetRenamed(payload, integrationId, appType), handleIssue: jest.fn(), @@ -191,6 +202,18 @@ describe('handleGitHubWebhook', () => { Response.json({ message: 'Installation target updated' }) ); mockRevokeStoredGitHubUserAuthorization.mockResolvedValue({ kiloUserId: 'user_1' }); + mockHandleInstallationDeleted.mockResolvedValue( + Response.json({ message: 'Installation removed' }) + ); + mockHandleInstallationSuspend.mockResolvedValue( + Response.json({ message: 'Installation suspended' }) + ); + mockHandleInstallationUnsuspend.mockResolvedValue( + Response.json({ message: 'Installation unsuspended' }) + ); + mockHandleInstallationRepositories.mockResolvedValue( + Response.json({ message: 'Repositories updated' }) + ); }); it('routes installation_target renamed events through authoritative login synchronization', async () => { @@ -282,6 +305,16 @@ describe('handleGitHubWebhook', () => { expect(mockFindIntegrationByInstallationId).not.toHaveBeenCalled(); }); + it('scopes the integration lookup to the webhook app type', async () => { + const response = await handleGitHubWebhook( + signedGitHubRequest('pull_request', pullRequestPayload()), + 'lite' + ); + + expect(response.status).toBe(200); + expect(mockFindIntegrationByInstallationId).toHaveBeenCalledWith('github', '98765', 'lite'); + }); + it('keeps pull_request webhooks on the code review path', async () => { const payload = pullRequestPayload(); const response = await handleGitHubWebhook( @@ -361,4 +394,72 @@ describe('handleGitHubWebhook', () => { expect(mockHandlePullRequest).not.toHaveBeenCalled(); expect(mockHandlePRReviewComment).not.toHaveBeenCalled(); }); + + it('routes installation.deleted to the handler with the webhook app type', async () => { + const payload = { action: 'deleted', installation: { id: 98765 } }; + + const response = await handleGitHubWebhook( + signedGitHubRequest('installation', payload), + 'lite' + ); + + expect(response.status).toBe(200); + expect(mockFindIntegrationByInstallationId).toHaveBeenCalledWith('github', '98765', 'lite'); + expect(mockHandleInstallationDeleted).toHaveBeenCalledWith( + expect.objectContaining(payload), + 'lite' + ); + }); + + it('routes installation.suspend to the handler with the webhook app type', async () => { + const payload = { action: 'suspend', installation: { id: 98765 } }; + + const response = await handleGitHubWebhook( + signedGitHubRequest('installation', payload), + 'standard' + ); + + expect(response.status).toBe(200); + expect(mockFindIntegrationByInstallationId).toHaveBeenCalledWith('github', '98765', 'standard'); + expect(mockHandleInstallationSuspend).toHaveBeenCalledWith( + expect.objectContaining(payload), + 'standard' + ); + }); + + it('routes installation.unsuspend to the handler with the webhook app type', async () => { + const payload = { action: 'unsuspend', installation: { id: 98765 } }; + + const response = await handleGitHubWebhook( + signedGitHubRequest('installation', payload), + 'lite' + ); + + expect(response.status).toBe(200); + expect(mockFindIntegrationByInstallationId).toHaveBeenCalledWith('github', '98765', 'lite'); + expect(mockHandleInstallationUnsuspend).toHaveBeenCalledWith( + expect.objectContaining(payload), + 'lite' + ); + }); + + it('routes installation_repositories to the handler with the webhook app type', async () => { + const payload = { + action: 'added', + installation: { id: 98765 }, + repositories_added: [{ id: 1, name: 'widgets', full_name: 'acme/widgets', private: false }], + }; + + const response = await handleGitHubWebhook( + signedGitHubRequest('installation_repositories', payload), + 'standard' + ); + + expect(response.status).toBe(200); + expect(mockFindIntegrationByInstallationId).toHaveBeenCalledWith('github', '98765', 'standard'); + expect(mockHandleInstallationRepositories).toHaveBeenCalledWith( + expect.objectContaining(payload), + 'standard' + ); + }); }); diff --git a/apps/web/src/lib/integrations/platforms/github/webhook-handler.ts b/apps/web/src/lib/integrations/platforms/github/webhook-handler.ts index af0bc6dde8..dc46f401ae 100644 --- a/apps/web/src/lib/integrations/platforms/github/webhook-handler.ts +++ b/apps/web/src/lib/integrations/platforms/github/webhook-handler.ts @@ -187,7 +187,11 @@ export async function handleGitHubWebhook( // Get integration before deletion to log the event const installationId = parseResult.data.installation.id.toString(); - const integration = await findIntegrationByInstallationId(PLATFORM.GITHUB, installationId); + const integration = await findIntegrationByInstallationId( + PLATFORM.GITHUB, + installationId, + appType + ); if (integration) { const logResult = await logWebhook(integration, action); @@ -195,7 +199,7 @@ export async function handleGitHubWebhook( return NextResponse.json({ message: 'Duplicate event' }, { status: 200 }); } - const result = await handleInstallationDeleted(parseResult.data); + const result = await handleInstallationDeleted(parseResult.data, appType); // Mark webhook event as processed if (logResult.webhookEventId) { @@ -214,7 +218,7 @@ export async function handleGitHubWebhook( return result; } - return await handleInstallationDeleted(parseResult.data); + return await handleInstallationDeleted(parseResult.data, appType); } if (action === GITHUB_ACTION.SUSPEND) { @@ -230,7 +234,11 @@ export async function handleGitHubWebhook( } const installationId = parseResult.data.installation.id.toString(); - const integration = await findIntegrationByInstallationId(PLATFORM.GITHUB, installationId); + const integration = await findIntegrationByInstallationId( + PLATFORM.GITHUB, + installationId, + appType + ); if (integration) { const logResult = await logWebhook(integration, action); @@ -238,7 +246,7 @@ export async function handleGitHubWebhook( return NextResponse.json({ message: 'Duplicate event' }, { status: 200 }); } - const result = await handleInstallationSuspend(parseResult.data); + const result = await handleInstallationSuspend(parseResult.data, appType); // Mark webhook event as processed if (logResult.webhookEventId) { @@ -257,7 +265,7 @@ export async function handleGitHubWebhook( return result; } - return await handleInstallationSuspend(parseResult.data); + return await handleInstallationSuspend(parseResult.data, appType); } if (action === GITHUB_ACTION.UNSUSPEND) { @@ -273,7 +281,11 @@ export async function handleGitHubWebhook( } const installationId = parseResult.data.installation.id.toString(); - const integration = await findIntegrationByInstallationId(PLATFORM.GITHUB, installationId); + const integration = await findIntegrationByInstallationId( + PLATFORM.GITHUB, + installationId, + appType + ); if (integration) { const logResult = await logWebhook(integration, action); @@ -281,7 +293,7 @@ export async function handleGitHubWebhook( return NextResponse.json({ message: 'Duplicate event' }, { status: 200 }); } - const result = await handleInstallationUnsuspend(parseResult.data); + const result = await handleInstallationUnsuspend(parseResult.data, appType); // Mark webhook event as processed if (logResult.webhookEventId) { @@ -300,7 +312,7 @@ export async function handleGitHubWebhook( return result; } - return await handleInstallationUnsuspend(parseResult.data); + return await handleInstallationUnsuspend(parseResult.data, appType); } return NextResponse.json({ message: 'Event received' }, { status: 200 }); @@ -330,7 +342,11 @@ export async function handleGitHubWebhook( } const installationId = parseResult.data.installation.id.toString(); - const integration = await findIntegrationByInstallationId(PLATFORM.GITHUB, installationId); + const integration = await findIntegrationByInstallationId( + PLATFORM.GITHUB, + installationId, + appType + ); if (!integration) { console.warn(`Integration not found${logSuffix}:`, installationId); return NextResponse.json({ message: 'Integration not found' }, { status: 404 }); @@ -382,7 +398,11 @@ export async function handleGitHubWebhook( } const installationId = parseResult.data.installation.id.toString(); - const integration = await findIntegrationByInstallationId(PLATFORM.GITHUB, installationId); + const integration = await findIntegrationByInstallationId( + PLATFORM.GITHUB, + installationId, + appType + ); if (!integration) { console.warn(`Integration not found${logSuffix}:`, installationId); @@ -395,7 +415,7 @@ export async function handleGitHubWebhook( return NextResponse.json({ message: 'Duplicate event' }, { status: 200 }); } - const result = await handleInstallationRepositories(parseResult.data); + const result = await handleInstallationRepositories(parseResult.data, appType); // Mark webhook event as processed if (logResult.webhookEventId) { @@ -423,7 +443,11 @@ export async function handleGitHubWebhook( return NextResponse.json({ message: 'Missing installation ID' }, { status: 400 }); } - const integration = await findIntegrationByInstallationId(PLATFORM.GITHUB, installationId); + const integration = await findIntegrationByInstallationId( + PLATFORM.GITHUB, + installationId, + appType + ); if (!integration) { console.warn(`Integration not found for installation${logSuffix}:`, installationId); diff --git a/apps/web/src/lib/integrations/platforms/github/webhook-handlers/installation-handler.test.ts b/apps/web/src/lib/integrations/platforms/github/webhook-handlers/installation-handler.test.ts new file mode 100644 index 0000000000..04ffd7bd94 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/github/webhook-handlers/installation-handler.test.ts @@ -0,0 +1,382 @@ +import { beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals'; +import type { + InstallationDeletedPayload, + InstallationRepositoriesPayload, + InstallationSuspendPayload, + InstallationUnsuspendPayload, +} from '../webhook-schemas'; +import type { GitHubAppType } from '../app-selector'; + +type GitHubIntegrationRow = { + id: string; + owned_by_organization_id: string | null; + owned_by_user_id: string | null; + repositories?: { id: number; name: string; full_name: string; private: boolean }[] | null; +}; + +type GitHubOwner = { type: 'user' | 'org'; id: string }; +type PlatformRepository = { id: number; name: string; full_name: string; private: boolean }; + +const mockFindIntegrationByInstallationId = + jest.fn< + ( + platform: string, + installationId: string, + appType: GitHubAppType + ) => Promise + >(); +const mockDeleteIntegration = + jest.fn< + ( + organizationId: string, + platform: string, + appType: GitHubAppType, + installationId?: string + ) => Promise + >(); +const mockDeleteIntegrationForOwner = + jest.fn< + ( + owner: GitHubOwner, + platform: string, + appType: GitHubAppType, + installationId?: string + ) => Promise + >(); +const mockSuspendIntegration = + jest.fn< + ( + organizationId: string, + platform: string, + suspendedBy: string, + appType: GitHubAppType, + installationId?: string + ) => Promise + >(); +const mockSuspendIntegrationForOwner = + jest.fn< + ( + owner: GitHubOwner, + platform: string, + suspendedBy: string, + appType: GitHubAppType, + installationId?: string + ) => Promise + >(); +const mockUnsuspendIntegration = + jest.fn< + ( + organizationId: string, + platform: string, + appType: GitHubAppType, + installationId?: string + ) => Promise + >(); +const mockUnsuspendIntegrationForOwner = + jest.fn< + ( + owner: GitHubOwner, + platform: string, + appType: GitHubAppType, + installationId?: string + ) => Promise + >(); +const mockUpdateIntegrationRepositories = + jest.fn< + ( + platform: string, + installationId: string, + repositories: PlatformRepository[], + appType: GitHubAppType + ) => Promise + >(); +const mockBotInitialize = jest.fn<() => Promise>(); +const mockBotGetState = jest.fn<() => unknown>(); +const mockUnlinkTeamKiloUsers = + jest.fn<(state: unknown, platform: string, teamId: string) => Promise>(); +const mockCaptureException = jest.fn<(...args: unknown[]) => void>(); +const mockLogExceptInTest = jest.fn<(...args: unknown[]) => void>(); + +jest.mock('@/lib/integrations/db/platform-integrations', () => ({ + findIntegrationByInstallationId: ( + platform: string, + installationId: string, + appType: GitHubAppType + ) => mockFindIntegrationByInstallationId(platform, installationId, appType), + autoCompleteInstallation: jest.fn(), + deleteIntegration: ( + organizationId: string, + platform: string, + appType: GitHubAppType, + installationId?: string + ) => mockDeleteIntegration(organizationId, platform, appType, installationId), + deleteIntegrationForOwner: ( + owner: GitHubOwner, + platform: string, + appType: GitHubAppType, + installationId?: string + ) => mockDeleteIntegrationForOwner(owner, platform, appType, installationId), + suspendIntegration: ( + organizationId: string, + platform: string, + suspendedBy: string, + appType: GitHubAppType, + installationId?: string + ) => mockSuspendIntegration(organizationId, platform, suspendedBy, appType, installationId), + suspendIntegrationForOwner: ( + owner: GitHubOwner, + platform: string, + suspendedBy: string, + appType: GitHubAppType, + installationId?: string + ) => mockSuspendIntegrationForOwner(owner, platform, suspendedBy, appType, installationId), + unsuspendIntegration: ( + organizationId: string, + platform: string, + appType: GitHubAppType, + installationId?: string + ) => mockUnsuspendIntegration(organizationId, platform, appType, installationId), + unsuspendIntegrationForOwner: ( + owner: GitHubOwner, + platform: string, + appType: GitHubAppType, + installationId?: string + ) => mockUnsuspendIntegrationForOwner(owner, platform, appType, installationId), + updateRepositoriesForIntegration: jest.fn(), + updateIntegrationRepositories: ( + platform: string, + installationId: string, + repositories: PlatformRepository[], + appType: GitHubAppType + ) => mockUpdateIntegrationRepositories(platform, installationId, repositories, appType), +})); + +jest.mock('@/lib/bot', () => ({ + bot: { + initialize: () => mockBotInitialize(), + getState: () => mockBotGetState(), + }, +})); + +jest.mock('@/lib/bot-identity', () => ({ + unlinkTeamKiloUsers: (state: unknown, platform: string, teamId: string) => + mockUnlinkTeamKiloUsers(state, platform, teamId), +})); + +jest.mock('@sentry/nextjs', () => ({ + captureException: (...args: unknown[]) => mockCaptureException(...args), +})); + +jest.mock('@/lib/utils.server', () => ({ + logExceptInTest: (...args: unknown[]) => mockLogExceptInTest(...args), +})); + +let handleInstallationDeleted: ( + payload: InstallationDeletedPayload, + appType: GitHubAppType +) => Promise; +let handleInstallationSuspend: ( + payload: InstallationSuspendPayload, + appType: GitHubAppType +) => Promise; +let handleInstallationUnsuspend: ( + payload: InstallationUnsuspendPayload, + appType: GitHubAppType +) => Promise; +let handleInstallationRepositories: ( + payload: InstallationRepositoriesPayload, + appType: GitHubAppType +) => Promise; + +beforeAll(async () => { + ({ handleInstallationDeleted, handleInstallationSuspend, handleInstallationUnsuspend } = + await import('./installation-handler')); + ({ handleInstallationRepositories } = await import('./installation-repositories-handler')); +}); + +const deletedPayload = { action: 'deleted', installation: { id: 98765 } } as const; +const suspendPayload = { + action: 'suspend', + installation: { id: 98765 }, + sender: { id: 1, login: 'octocat' }, +} as const; +const unsuspendPayload = { action: 'unsuspend', installation: { id: 98765 } } as const; + +const orgIntegration: GitHubIntegrationRow = { + id: 'pi_org', + owned_by_organization_id: 'org_1', + owned_by_user_id: null, +}; +const userIntegration: GitHubIntegrationRow = { + id: 'pi_user', + owned_by_organization_id: null, + owned_by_user_id: 'user_1', +}; + +describe('handleInstallationDeleted', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockBotInitialize.mockResolvedValue(undefined); + mockBotGetState.mockReturnValue({}); + mockUnlinkTeamKiloUsers.mockResolvedValue(0); + mockDeleteIntegration.mockResolvedValue(undefined); + mockDeleteIntegrationForOwner.mockResolvedValue(undefined); + }); + + it('standard app deletion unlinks bot identities and passes the app type', async () => { + mockFindIntegrationByInstallationId.mockResolvedValue(orgIntegration); + + const response = await handleInstallationDeleted(deletedPayload, 'standard'); + + expect(response.status).toBe(200); + expect(mockFindIntegrationByInstallationId).toHaveBeenCalledWith('github', '98765', 'standard'); + expect(mockBotInitialize).toHaveBeenCalled(); + expect(mockUnlinkTeamKiloUsers).toHaveBeenCalledWith(expect.anything(), 'github', '98765'); + expect(mockDeleteIntegration).toHaveBeenCalledWith('org_1', 'github', 'standard', '98765'); + expect(mockDeleteIntegrationForOwner).not.toHaveBeenCalled(); + }); + + it('lite app deletion does not unlink bot identities and passes the app type', async () => { + mockFindIntegrationByInstallationId.mockResolvedValue(userIntegration); + + const response = await handleInstallationDeleted(deletedPayload, 'lite'); + + expect(response.status).toBe(200); + expect(mockFindIntegrationByInstallationId).toHaveBeenCalledWith('github', '98765', 'lite'); + expect(mockBotInitialize).not.toHaveBeenCalled(); + expect(mockUnlinkTeamKiloUsers).not.toHaveBeenCalled(); + expect(mockDeleteIntegrationForOwner).toHaveBeenCalledWith( + { type: 'user', id: 'user_1' }, + 'github', + 'lite', + '98765' + ); + expect(mockDeleteIntegration).not.toHaveBeenCalled(); + }); +}); + +describe('handleInstallationSuspend', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockSuspendIntegration.mockResolvedValue(undefined); + mockSuspendIntegrationForOwner.mockResolvedValue(undefined); + }); + + it('passes the webhook app type to the organization suspend helper', async () => { + mockFindIntegrationByInstallationId.mockResolvedValue(orgIntegration); + + const response = await handleInstallationSuspend(suspendPayload, 'standard'); + + expect(response.status).toBe(200); + expect(mockFindIntegrationByInstallationId).toHaveBeenCalledWith('github', '98765', 'standard'); + expect(mockSuspendIntegration).toHaveBeenCalledWith( + 'org_1', + 'github', + 'octocat', + 'standard', + '98765' + ); + expect(mockSuspendIntegrationForOwner).not.toHaveBeenCalled(); + }); + + it('passes the webhook app type to the user suspend helper', async () => { + mockFindIntegrationByInstallationId.mockResolvedValue(userIntegration); + + const response = await handleInstallationSuspend(suspendPayload, 'lite'); + + expect(response.status).toBe(200); + expect(mockSuspendIntegrationForOwner).toHaveBeenCalledWith( + { type: 'user', id: 'user_1' }, + 'github', + 'octocat', + 'lite', + '98765' + ); + expect(mockSuspendIntegration).not.toHaveBeenCalled(); + }); +}); + +describe('handleInstallationUnsuspend', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUnsuspendIntegration.mockResolvedValue(undefined); + mockUnsuspendIntegrationForOwner.mockResolvedValue(undefined); + }); + + it('passes the webhook app type to the organization unsuspend helper', async () => { + mockFindIntegrationByInstallationId.mockResolvedValue(orgIntegration); + + const response = await handleInstallationUnsuspend(unsuspendPayload, 'lite'); + + expect(response.status).toBe(200); + expect(mockFindIntegrationByInstallationId).toHaveBeenCalledWith('github', '98765', 'lite'); + expect(mockUnsuspendIntegration).toHaveBeenCalledWith('org_1', 'github', 'lite', '98765'); + expect(mockUnsuspendIntegrationForOwner).not.toHaveBeenCalled(); + }); + + it('passes the webhook app type to the user unsuspend helper', async () => { + mockFindIntegrationByInstallationId.mockResolvedValue(userIntegration); + + const response = await handleInstallationUnsuspend(unsuspendPayload, 'standard'); + + expect(response.status).toBe(200); + expect(mockUnsuspendIntegrationForOwner).toHaveBeenCalledWith( + { type: 'user', id: 'user_1' }, + 'github', + 'standard', + '98765' + ); + expect(mockUnsuspendIntegration).not.toHaveBeenCalled(); + }); +}); + +describe('handleInstallationRepositories', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockFindIntegrationByInstallationId.mockResolvedValue({ + id: 'pi_org', + owned_by_organization_id: 'org_1', + owned_by_user_id: null, + repositories: [{ id: 1, name: 'keep', full_name: 'acme/keep', private: false }], + }); + mockUpdateIntegrationRepositories.mockResolvedValue(undefined); + }); + + it('merges added repositories and passes the webhook app type', async () => { + const response = await handleInstallationRepositories( + { + action: 'added', + installation: { id: 98765 }, + repositories_added: [{ id: 2, name: 'new', full_name: 'acme/new', private: true }], + }, + 'standard' + ); + + expect(response.status).toBe(200); + expect(mockFindIntegrationByInstallationId).toHaveBeenCalledWith('github', '98765', 'standard'); + expect(mockUpdateIntegrationRepositories).toHaveBeenCalledWith( + 'github', + '98765', + [ + { id: 1, name: 'keep', full_name: 'acme/keep', private: false }, + { id: 2, name: 'new', full_name: 'acme/new', private: true }, + ], + 'standard' + ); + }); + + it('removes repositories and passes the lite app type', async () => { + const response = await handleInstallationRepositories( + { + action: 'removed', + installation: { id: 98765 }, + repositories_removed: [{ id: 1, name: 'keep', full_name: 'acme/keep', private: false }], + }, + 'lite' + ); + + expect(response.status).toBe(200); + expect(mockFindIntegrationByInstallationId).toHaveBeenCalledWith('github', '98765', 'lite'); + expect(mockUpdateIntegrationRepositories).toHaveBeenCalledWith('github', '98765', [], 'lite'); + }); +}); diff --git a/apps/web/src/lib/integrations/platforms/github/webhook-handlers/installation-handler.ts b/apps/web/src/lib/integrations/platforms/github/webhook-handlers/installation-handler.ts index b4e9f7792c..100bad605e 100644 --- a/apps/web/src/lib/integrations/platforms/github/webhook-handlers/installation-handler.ts +++ b/apps/web/src/lib/integrations/platforms/github/webhook-handlers/installation-handler.ts @@ -26,6 +26,7 @@ import { logExceptInTest } from '@/lib/utils.server'; import { captureException } from '@sentry/nextjs'; import { bot } from '@/lib/bot'; import { unlinkTeamKiloUsers } from '@/lib/bot-identity'; +import type { GitHubAppType } from '../app-selector'; /** * GitHub Installation Event Handlers @@ -114,18 +115,28 @@ export async function handleInstallationCreated(payload: InstallationCreatedPayl ); } -export async function handleInstallationDeleted(payload: InstallationDeletedPayload) { +export async function handleInstallationDeleted( + payload: InstallationDeletedPayload, + appType: GitHubAppType +) { const installationIdStr = payload.installation.id.toString(); - // Find and delete the integration (whether completed or pending) + // Find and delete the integration (whether completed or pending). The + // webhook is signed by a specific GitHub App, so scope the lookup by app + // type to avoid deleting the other app's row for the same installation ID. const integrationToDelete = await findIntegrationByInstallationId( PLATFORM.GITHUB, - installationIdStr + installationIdStr, + appType ); try { - await bot.initialize(); - await unlinkTeamKiloUsers(bot.getState(), PLATFORM.GITHUB, installationIdStr); + // The bot identity store has no app-type dimension and the lite app has no + // bot-link flow, so only the standard app unlinks team bot identities. + if (appType !== 'lite') { + await bot.initialize(); + await unlinkTeamKiloUsers(bot.getState(), PLATFORM.GITHUB, installationIdStr); + } } catch (error) { captureException(error, { tags: { component: 'kilo-bot', op: 'github-installation-deleted-unlink' }, @@ -136,7 +147,12 @@ export async function handleInstallationDeleted(payload: InstallationDeletedPayl if (integrationToDelete) { // Determine owner from the integration record if (integrationToDelete.owned_by_organization_id) { - await deleteIntegration(integrationToDelete.owned_by_organization_id, PLATFORM.GITHUB); + await deleteIntegration( + integrationToDelete.owned_by_organization_id, + PLATFORM.GITHUB, + appType, + installationIdStr + ); logExceptInTest('Deleted organization installation:', { installation_id: installationIdStr, owned_by_organization_id: integrationToDelete.owned_by_organization_id, @@ -144,7 +160,9 @@ export async function handleInstallationDeleted(payload: InstallationDeletedPayl } else if (integrationToDelete.owned_by_user_id) { await deleteIntegrationForOwner( { type: 'user', id: integrationToDelete.owned_by_user_id }, - PLATFORM.GITHUB + PLATFORM.GITHUB, + appType, + installationIdStr ); logExceptInTest('Deleted user installation:', { installation_id: installationIdStr, @@ -158,10 +176,15 @@ export async function handleInstallationDeleted(payload: InstallationDeletedPayl return NextResponse.json({ message: 'Installation removed' }, { status: 200 }); } -export async function handleInstallationSuspend(payload: InstallationSuspendPayload) { +export async function handleInstallationSuspend( + payload: InstallationSuspendPayload, + appType: GitHubAppType +) { + const installationIdStr = payload.installation.id.toString(); const integrationToSuspend = await findIntegrationByInstallationId( PLATFORM.GITHUB, - payload.installation.id.toString() + installationIdStr, + appType ); if (integrationToSuspend) { @@ -172,7 +195,9 @@ export async function handleInstallationSuspend(payload: InstallationSuspendPayl await suspendIntegration( integrationToSuspend.owned_by_organization_id, PLATFORM.GITHUB, - suspendedBy + suspendedBy, + appType, + installationIdStr ); logExceptInTest('GitHub App suspended (organization):', { installation_id: payload.installation.id, @@ -182,7 +207,9 @@ export async function handleInstallationSuspend(payload: InstallationSuspendPayl await suspendIntegrationForOwner( { type: 'user', id: integrationToSuspend.owned_by_user_id }, PLATFORM.GITHUB, - suspendedBy + suspendedBy, + appType, + installationIdStr ); logExceptInTest('GitHub App suspended (user):', { installation_id: payload.installation.id, @@ -196,16 +223,26 @@ export async function handleInstallationSuspend(payload: InstallationSuspendPayl return NextResponse.json({ message: 'Installation suspended' }, { status: 200 }); } -export async function handleInstallationUnsuspend(payload: InstallationUnsuspendPayload) { +export async function handleInstallationUnsuspend( + payload: InstallationUnsuspendPayload, + appType: GitHubAppType +) { + const installationIdStr = payload.installation.id.toString(); const integrationToUnsuspend = await findIntegrationByInstallationId( PLATFORM.GITHUB, - payload.installation.id.toString() + installationIdStr, + appType ); if (integrationToUnsuspend) { // Determine owner from the integration record if (integrationToUnsuspend.owned_by_organization_id) { - await unsuspendIntegration(integrationToUnsuspend.owned_by_organization_id, PLATFORM.GITHUB); + await unsuspendIntegration( + integrationToUnsuspend.owned_by_organization_id, + PLATFORM.GITHUB, + appType, + installationIdStr + ); logExceptInTest('GitHub App unsuspended (organization):', { installation_id: payload.installation.id, owned_by_organization_id: integrationToUnsuspend.owned_by_organization_id, @@ -213,7 +250,9 @@ export async function handleInstallationUnsuspend(payload: InstallationUnsuspend } else if (integrationToUnsuspend.owned_by_user_id) { await unsuspendIntegrationForOwner( { type: 'user', id: integrationToUnsuspend.owned_by_user_id }, - PLATFORM.GITHUB + PLATFORM.GITHUB, + appType, + installationIdStr ); logExceptInTest('GitHub App unsuspended (user):', { installation_id: payload.installation.id, diff --git a/apps/web/src/lib/integrations/platforms/github/webhook-handlers/installation-repositories-handler.ts b/apps/web/src/lib/integrations/platforms/github/webhook-handlers/installation-repositories-handler.ts index 515bf918d3..8d18813dc8 100644 --- a/apps/web/src/lib/integrations/platforms/github/webhook-handlers/installation-repositories-handler.ts +++ b/apps/web/src/lib/integrations/platforms/github/webhook-handlers/installation-repositories-handler.ts @@ -10,18 +10,23 @@ import { import type { InstallationRepositoriesPayload } from '../webhook-schemas'; import { PLATFORM, GITHUB_ACTION } from '@/lib/integrations/core/constants'; import { logExceptInTest } from '@/lib/utils.server'; +import type { GitHubAppType } from '../app-selector'; /** * GitHub Installation Repositories Event Handler * Handles: repositories added/removed */ -export async function handleInstallationRepositories(payload: InstallationRepositoriesPayload) { +export async function handleInstallationRepositories( + payload: InstallationRepositoriesPayload, + appType: GitHubAppType +) { const { installation, action, repositories_added, repositories_removed } = payload; const integration = await findIntegrationByInstallationId( PLATFORM.GITHUB, - installation.id.toString() + installation.id.toString(), + appType ); if (!integration) { @@ -46,7 +51,12 @@ export async function handleInstallationRepositories(payload: InstallationReposi updatedRepos = currentRepos.filter((repo: PlatformRepository) => !removedIds.includes(repo.id)); } - await updateIntegrationRepositories(PLATFORM.GITHUB, installation.id.toString(), updatedRepos); + await updateIntegrationRepositories( + PLATFORM.GITHUB, + installation.id.toString(), + updatedRepos, + appType + ); logExceptInTest('Installation repositories updated:', { installation_id: installation.id, diff --git a/apps/web/src/lib/integrations/validate-return-path.test.ts b/apps/web/src/lib/integrations/validate-return-path.test.ts index b8c476ad99..cbf7d7d0db 100644 --- a/apps/web/src/lib/integrations/validate-return-path.test.ts +++ b/apps/web/src/lib/integrations/validate-return-path.test.ts @@ -51,6 +51,26 @@ describe('validateReturnPath', () => { expect(validateReturnPath('/')).toBe('/'); }); + it('accepts the C13 /cloud/sessions universal-link route', () => { + expect(validateReturnPath('/cloud/sessions')).toBe('/cloud/sessions'); + }); + + it('accepts /cloud/sessions with query params (C13 return-outcome payload)', () => { + expect(validateReturnPath('/cloud/sessions?github_install=success')).toBe( + '/cloud/sessions?github_install=success' + ); + }); + + it('accepts /cloud/sessions with error query param', () => { + expect(validateReturnPath('/cloud/sessions?error=install_state_user_mismatch')).toBe( + '/cloud/sessions?error=install_state_user_mismatch' + ); + }); + + it('rejects a crafted returnTo that mimics /cloud/ but redirects externally', () => { + expect(validateReturnPath('/cloud/\nhttps://evil.com')).toBeNull(); + }); + it('rejects triple-slash paths', () => { expect(validateReturnPath('///foo')).toBeNull(); }); diff --git a/apps/web/src/lib/tokens.test.ts b/apps/web/src/lib/tokens.test.ts new file mode 100644 index 0000000000..f470fa67fe --- /dev/null +++ b/apps/web/src/lib/tokens.test.ts @@ -0,0 +1,36 @@ +import { describe, test, expect } from '@jest/globals'; +import jwt from 'jsonwebtoken'; +import { TOKEN_EXPIRY, validateAuthorizationHeader, JWT_TOKEN_VERSION } from './tokens'; +import { getEnvVariable } from '@/lib/dotenvx'; + +describe('TOKEN_EXPIRY', () => { + test('default is five years in seconds', () => { + const FIVE_YEARS_IN_SECONDS = 5 * 365 * 24 * 60 * 60; + expect(TOKEN_EXPIRY.default).toBe(FIVE_YEARS_IN_SECONDS); + }); +}); + +describe('validateAuthorizationHeader (C15 device-session compatibility)', () => { + test('accepts a signed Bearer JWT carrying deviceSessionId', () => { + const token = jwt.sign( + { + env: process.env.NODE_ENV, + kiloUserId: 'test-user-c15', + apiTokenPepper: 'test-pepper', + version: JWT_TOKEN_VERSION, + deviceSessionId: 'device-session-c15-test', + }, + getEnvVariable('NEXTAUTH_SECRET'), + { algorithm: 'HS256', expiresIn: '5y' } + ); + + const headers = new Headers(); + headers.set('authorization', `Bearer ${token}`); + + const result = validateAuthorizationHeader(headers); + + expect(result.error).toBeUndefined(); + expect(result.kiloUserId).toBe('test-user-c15'); + expect(result.apiTokenPepper).toBe('test-pepper'); + }); +}); diff --git a/apps/web/src/lib/tokens.ts b/apps/web/src/lib/tokens.ts index e38c1f847e..fcdca36909 100644 --- a/apps/web/src/lib/tokens.ts +++ b/apps/web/src/lib/tokens.ts @@ -12,6 +12,7 @@ const jwtSigningAlgorithm = 'HS256'; export type JWTTokenExtraPayload = { deviceAuthRequestCode?: string; + deviceSessionId?: string; botId?: string; organizationId?: string; organizationRole?: OrganizationRole; diff --git a/apps/web/src/lib/user/block.test.ts b/apps/web/src/lib/user/block.test.ts index 046ecb14fd..27fb0555f6 100644 --- a/apps/web/src/lib/user/block.test.ts +++ b/apps/web/src/lib/user/block.test.ts @@ -1,6 +1,12 @@ import { describe, test, expect } from '@jest/globals'; import { eq } from 'drizzle-orm'; -import { kilocode_users } from '@kilocode/db/schema'; +import { + kilocode_users, + device_auth_requests, + device_sessions, + device_refresh_tokens, + native_attested_keys, +} from '@kilocode/db/schema'; import { db } from '@/lib/drizzle'; import { insertTestUser } from '@/tests/helpers/user.helper'; import { blockUser } from '@/lib/user/block'; @@ -101,4 +107,171 @@ describe('blockUser (integration)', () => { expect(after?.blocked_reason).toBeNull(); expect(after?.api_token_pepper).toBe('initial-pepper'); }); + + test('denies pending and approved device auth requests for a blocked user', async () => { + const user = await insertTestUser({ api_token_pepper: 'initial-pepper' }); + + // Insert a pending request + const [pendingReq] = await db + .insert(device_auth_requests) + .values({ + code: 'PENDING-BLOCK', + kilo_user_id: user.id, + status: 'pending', + expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(), + }) + .returning({ id: device_auth_requests.id }); + + // Insert an approved request + const [approvedReq] = await db + .insert(device_auth_requests) + .values({ + code: 'APPROVED-BLOCK', + kilo_user_id: user.id, + status: 'approved', + expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(), + }) + .returning({ id: device_auth_requests.id }); + + const didBlock = await blockUser({ kiloUserId: user.id, reason: 'device block' }); + + expect(didBlock).toBe(true); + + const pending = await db.query.device_auth_requests.findFirst({ + where: eq(device_auth_requests.id, pendingReq.id), + }); + const approved = await db.query.device_auth_requests.findFirst({ + where: eq(device_auth_requests.id, approvedReq.id), + }); + + expect(pending?.status).toBe('denied'); + expect(approved?.status).toBe('denied'); + }); + + test('revokes device sessions and deletes unconsumed refresh tokens for a blocked user', async () => { + const user = await insertTestUser({ api_token_pepper: 'initial-pepper' }); + + // Create a device session + const [session] = await db + .insert(device_sessions) + .values({ + kilo_user_id: user.id, + user_agent: 'BlockTest/1.0', + }) + .returning({ id: device_sessions.id }); + + // Create an unconsumed refresh token + const tokenHash = 'block-test-token-hash'; + await db.insert(device_refresh_tokens).values({ + token_hash: tokenHash, + device_session_id: session.id, + expires_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + }); + + const didBlock = await blockUser({ kiloUserId: user.id, reason: 'device block' }); + + expect(didBlock).toBe(true); + + // Session should be revoked + const afterSession = await db.query.device_sessions.findFirst({ + where: eq(device_sessions.id, session.id), + }); + expect(afterSession?.revoked_at).not.toBeNull(); + expect(afterSession?.revoked_reason).toBe('user_blocked'); + + // Refresh token should be deleted + const afterToken = await db.query.device_refresh_tokens.findFirst({ + where: eq(device_refresh_tokens.token_hash, tokenHash), + }); + expect(afterToken).toBeUndefined(); + }); + + test('deletes native attested keys for a blocked user', async () => { + const user = await insertTestUser({ api_token_pepper: 'initial-pepper' }); + + // Insert a native attested key + await db.insert(native_attested_keys).values({ + key_id: 'test-key-block', + kilo_user_id: user.id, + platform: 'ios', + public_key: 'base64pubkey', + sign_count: 0, + attested_at: new Date().toISOString(), + }); + + const didBlock = await blockUser({ kiloUserId: user.id, reason: 'device block' }); + + expect(didBlock).toBe(true); + + const afterKey = await db.query.native_attested_keys.findFirst({ + where: eq(native_attested_keys.key_id, 'test-key-block'), + }); + expect(afterKey).toBeUndefined(); + }); + + test('does not revoke an already-revoked session', async () => { + const user = await insertTestUser({ api_token_pepper: 'initial-pepper' }); + + const [session] = await db + .insert(device_sessions) + .values({ + kilo_user_id: user.id, + revoked_at: new Date(Date.now() - 60_000).toISOString(), + revoked_reason: 'manual_revoke', + }) + .returning({ id: device_sessions.id }); + + await blockUser({ kiloUserId: user.id, reason: 'device block' }); + + const after = await db.query.device_sessions.findFirst({ + where: eq(device_sessions.id, session.id), + }); + expect(after?.revoked_reason).toBe('manual_revoke'); + }); + + test('rolls back device row invalidation when the transaction throws', async () => { + const user = await insertTestUser({ api_token_pepper: 'initial-pepper' }); + + const [session] = await db + .insert(device_sessions) + .values({ + kilo_user_id: user.id, + user_agent: 'RollbackTest/1.0', + }) + .returning({ id: device_sessions.id }); + + const [request] = await db + .insert(device_auth_requests) + .values({ + code: 'ROLLBACK-BLOCK', + kilo_user_id: user.id, + status: 'pending', + expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(), + }) + .returning({ id: device_auth_requests.id }); + + await expect( + db.transaction(async tx => { + await blockUser({ kiloUserId: user.id, reason: 'tx block', dbOrTx: tx }); + throw new Error('boom'); + }) + ).rejects.toThrow('boom'); + + // User should still be unblocked + const afterUser = await getUser(user.id); + expect(afterUser?.blocked_reason).toBeNull(); + expect(afterUser?.api_token_pepper).toBe('initial-pepper'); + + // Session should still be active + const afterSession = await db.query.device_sessions.findFirst({ + where: eq(device_sessions.id, session.id), + }); + expect(afterSession?.revoked_at).toBeNull(); + + // Request should still be pending + const afterRequest = await db.query.device_auth_requests.findFirst({ + where: eq(device_auth_requests.id, request.id), + }); + expect(afterRequest?.status).toBe('pending'); + }); }); diff --git a/apps/web/src/lib/user/block.ts b/apps/web/src/lib/user/block.ts index ea970bdef1..dc983ef968 100644 --- a/apps/web/src/lib/user/block.ts +++ b/apps/web/src/lib/user/block.ts @@ -1,6 +1,12 @@ import { randomUUID } from 'crypto'; -import { and, eq, isNull } from 'drizzle-orm'; -import { kilocode_users } from '@kilocode/db/schema'; +import { and, eq, isNull, or, inArray } from 'drizzle-orm'; +import { + kilocode_users, + device_auth_requests, + device_sessions, + device_refresh_tokens, + native_attested_keys, +} from '@kilocode/db/schema'; import { db, type DrizzleTransaction } from '@/lib/drizzle'; export type BlockUserParams = { @@ -21,20 +27,81 @@ export type BlockUserParams = { * overwritten — the original block reason is preserved and callers can rely on * the return value to detect the unblocked->blocked transition. * + * Also in the same transaction: + * - Denies every pending or approved device auth request for this user. + * - Revokes every non-revoked device session for this user. + * - Deletes every unconsumed device refresh token owned by those sessions. + * * @returns `true` if this call transitioned the user from unblocked to blocked, * `false` if the user was already blocked (or does not exist). */ export async function blockUser(params: BlockUserParams): Promise { - const executor = params.dbOrTx ?? db; - const rows = await executor - .update(kilocode_users) - .set({ - blocked_reason: params.reason, - blocked_at: new Date().toISOString(), - blocked_by_kilo_user_id: params.blockedByKiloUserId ?? null, - api_token_pepper: randomUUID(), - }) - .where(and(eq(kilocode_users.id, params.kiloUserId), isNull(kilocode_users.blocked_reason))) - .returning({ id: kilocode_users.id }); - return rows.length > 0; + const executor = params.dbOrTx; + + async function run(tx: typeof db | DrizzleTransaction): Promise { + const rows = await tx + .update(kilocode_users) + .set({ + blocked_reason: params.reason, + blocked_at: new Date().toISOString(), + blocked_by_kilo_user_id: params.blockedByKiloUserId ?? null, + api_token_pepper: randomUUID(), + }) + .where(and(eq(kilocode_users.id, params.kiloUserId), isNull(kilocode_users.blocked_reason))) + .returning({ id: kilocode_users.id }); + + if (rows.length === 0) return false; + + const now = new Date().toISOString(); + + // Deny every pending or approved device auth request for this user. + await tx + .update(device_auth_requests) + .set({ status: 'denied' }) + .where( + and( + eq(device_auth_requests.kilo_user_id, params.kiloUserId), + or( + eq(device_auth_requests.status, 'pending'), + eq(device_auth_requests.status, 'approved') + ) + ) + ); + + // Revoke every non-revoked device session for this user. + await tx + .update(device_sessions) + .set({ revoked_at: now, revoked_reason: 'user_blocked' }) + .where( + and(eq(device_sessions.kilo_user_id, params.kiloUserId), isNull(device_sessions.revoked_at)) + ); + + // Delete every unconsumed device refresh token owned by those sessions. + await tx + .delete(device_refresh_tokens) + .where( + and( + inArray( + device_refresh_tokens.device_session_id, + tx + .select({ id: device_sessions.id }) + .from(device_sessions) + .where(eq(device_sessions.kilo_user_id, params.kiloUserId)) + ), + isNull(device_refresh_tokens.consumed_at) + ) + ); + + // Delete every native attested key for this user. + await tx + .delete(native_attested_keys) + .where(eq(native_attested_keys.kilo_user_id, params.kiloUserId)); + + return true; + } + + if (executor) { + return run(executor); + } + return db.transaction(tx => run(tx)); } diff --git a/apps/web/src/lib/user/index.test.ts b/apps/web/src/lib/user/index.test.ts index 531858b79b..3f9bf3ff7e 100644 --- a/apps/web/src/lib/user/index.test.ts +++ b/apps/web/src/lib/user/index.test.ts @@ -32,6 +32,10 @@ import { cloud_agent_feedback, user_admin_notes, magic_link_tokens, + device_sessions, + device_refresh_tokens, + native_attested_keys, + native_admission_challenges, stytch_fingerprints, kiloclaw_instances, kiloclaw_google_oauth_connections, @@ -77,6 +81,7 @@ import { impact_advocate_reward_redemptions, impact_conversion_reports, github_branch_pull_requests, + github_install_states, code_review_feedback_events, code_review_memory_proposals, user_github_app_tokens, @@ -122,7 +127,7 @@ import { import { hashNormalizedEmailForDeletionTombstone } from '@/lib/impact/referral'; import { generateOpenRouterDownstreamSafetyIdentifier } from '@/lib/ai-gateway/providerHash'; import { createTestPaymentMethod } from '@/tests/helpers/payment-method.helper'; -import { insertTestUser } from '@/tests/helpers/user.helper'; +import { insertTestUser, insertTestUserAndGoogleAuth } from '@/tests/helpers/user.helper'; import { createTestOrganization } from '@/tests/helpers/organization.helper'; import { forceImmediateExpirationRecomputation } from '@/lib/balanceCache'; import { randomUUID } from 'crypto'; @@ -527,6 +532,115 @@ describe('User', () => { expect(updatedUser?.web_session_pepper).toEqual(expect.any(String)); expect(updatedUser?.web_session_pepper).not.toBe('web-pepper-before-workos'); }); + + it('returns deferredSignInEvent when deferSignInAnalytics is true and user is existing', async () => { + const user = await insertTestUserAndGoogleAuth({ + google_user_email: 'existing-defer@example.com', + google_user_name: 'Deferred Existing', + hosted_domain: 'test.com', + }); + + const result = await createOrUpdateUser( + { + google_user_email: 'existing-defer@example.com', + google_user_name: 'Deferred Existing', + google_user_image_url: 'https://example.com/avatar.png', + hosted_domain: null, + provider: 'google', + provider_account_id: `google-${user.id}`, + }, + undefined, + false, + undefined, + undefined, + undefined, + true + ); + + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.deferredSignInEvent).toEqual({ + distinctId: 'existing-defer@example.com', + event: 'user_signed_in', + properties: { + name: 'Deferred Existing', + hosted_domain: null, // synced from user row by findAndSyncExistingUser + provider: 'google', + id: user.id, + }, + }); + expect(result.user.id).toBe(user.id); + expect(result.isNew).toBe(false); + }); + + it('skips deferredSignInEvent when deferSignInAnalytics is not passed (backward compat)', async () => { + const user = await insertTestUserAndGoogleAuth({ + google_user_email: 'no-defer@example.com', + google_user_name: 'No Defer', + }); + + const result = await createOrUpdateUser( + { + google_user_email: 'no-defer@example.com', + google_user_name: 'No Defer', + google_user_image_url: 'https://example.com/avatar.png', + hosted_domain: null, + provider: 'google', + provider_account_id: `google-${user.id}`, + }, + undefined, + false + ); + + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.deferredSignInEvent).toBeUndefined(); + }); + + it('returns deferredSignInEvent for auto-linked user when deferSignInAnalytics is true', async () => { + const existing = await insertTestUser({ + google_user_email: 'autolink-defer@example.com', + google_user_name: 'Original Name', + hosted_domain: 'original.com', + }); + + const result = await createOrUpdateUser( + { + google_user_email: 'autolink-defer@example.com', + google_user_name: 'New Name', + google_user_image_url: 'https://example.com/new.png', + hosted_domain: 'new.com', + provider: 'google', + provider_account_id: 'google-autolink-new', + }, + undefined, + true, // autoLinkToExistingUser + undefined, + undefined, + undefined, + true + ); + + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.deferredSignInEvent).toEqual({ + distinctId: 'autolink-defer@example.com', + event: 'user_signed_in_with_different_id_and_auto_linked', + properties: { + existing_name: 'Original Name', + existing_hosted_domain: 'original.com', + existing_id: existing.id, + new_provider: 'google', + new_provider_account_id: 'google-autolink-new', + new_name: 'New Name', + new_email: 'autolink-defer@example.com', + new_image_url: 'https://example.com/new.png', + new_hosted_domain: 'new.com', + }, + }); + expect(result.user.id).toBe(existing.id); + expect(result.isNew).toBe(false); + }); }); describe('softDeleteUser', () => { @@ -1906,6 +2020,53 @@ describe('User', () => { ).toBe(1); }); + it('should delete github_install_states for the soft-deleted user', async () => { + const user = await insertTestUser(); + const otherUser = await insertTestUser(); + + // Insert install states for both users + await db.insert(github_install_states).values([ + { + token: 'soft-delete-test-token-' + Date.now(), + kilo_user_id: user.id, + owner_type: 'user', + owner_id: user.id, + github_app_type: 'standard', + return_to: '/github-app', + expires_at: new Date(Date.now() + 600_000).toISOString(), + }, + { + token: 'soft-delete-test-token-other-' + Date.now(), + kilo_user_id: otherUser.id, + owner_type: 'org', + owner_id: 'org-999', + github_app_type: 'lite', + return_to: null, + expires_at: new Date(Date.now() + 600_000).toISOString(), + }, + ]); + + await softDeleteUser(user.id); + + // The deleted user's install states must be gone + expect( + await db + .select({ count: count() }) + .from(github_install_states) + .where(eq(github_install_states.kilo_user_id, user.id)) + .then(r => r[0].count) + ).toBe(0); + + // The other user's install states must remain + expect( + await db + .select({ count: count() }) + .from(github_install_states) + .where(eq(github_install_states.kilo_user_id, otherUser.id)) + .then(r => r[0].count) + ).toBe(1); + }); + it('should delete organization memberships and usage data', async () => { const user1 = await insertTestUser(); const user2 = await insertTestUser(); @@ -4081,6 +4242,120 @@ describe('User', () => { expect(byokKeys).toHaveLength(0); }); + it('should delete device sessions and refresh tokens', async () => { + const user = await insertTestUser(); + const otherUser = await insertTestUser(); + + // Create a device session for the user + const [session] = await db + .insert(device_sessions) + .values({ + kilo_user_id: user.id, + user_agent: 'TestAgent/1.0', + }) + .returning({ id: device_sessions.id }); + + // Create a refresh token for the session + await db.insert(device_refresh_tokens).values({ + token_hash: 'test-hash-1', + device_session_id: session.id, + expires_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + }); + + // Create a session for the other user + const [otherSession] = await db + .insert(device_sessions) + .values({ + kilo_user_id: otherUser.id, + user_agent: 'OtherAgent/1.0', + }) + .returning({ id: device_sessions.id }); + + await db.insert(device_refresh_tokens).values({ + token_hash: 'test-hash-2', + device_session_id: otherSession.id, + expires_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + }); + + await softDeleteUser(user.id); + + // User's sessions and tokens must be gone + expect( + await db + .select({ count: count() }) + .from(device_sessions) + .where(eq(device_sessions.kilo_user_id, user.id)) + .then(r => r[0].count) + ).toBe(0); + + // Other user's sessions and tokens must remain + expect( + await db + .select({ count: count() }) + .from(device_sessions) + .where(eq(device_sessions.kilo_user_id, otherUser.id)) + .then(r => r[0].count) + ).toBe(1); + expect( + await db + .select({ count: count() }) + .from(device_refresh_tokens) + .where(eq(device_refresh_tokens.device_session_id, otherSession.id)) + .then(r => r[0].count) + ).toBe(1); + }); + + it('should delete native attested keys and admission challenges', async () => { + const user = await insertTestUser(); + const otherUser = await insertTestUser(); + + // Insert a native attested key for the user + await db.insert(native_attested_keys).values({ + key_id: 'test-key-1', + kilo_user_id: user.id, + platform: 'ios', + public_key: 'base64pubkey1', + sign_count: 5, + attested_at: new Date().toISOString(), + }); + + // Insert a native attested key for the other user + await db.insert(native_attested_keys).values({ + key_id: 'test-key-2', + kilo_user_id: otherUser.id, + platform: 'android', + public_key: 'base64pubkey2', + sign_count: 3, + attested_at: new Date().toISOString(), + }); + + // Insert an admission challenge (ephemeral, no user FK) + await db.insert(native_admission_challenges).values({ + challenge: 'test-challenge-1', + expires_at: new Date(Date.now() + 60_000).toISOString(), + }); + + await softDeleteUser(user.id); + + // User's key must be gone + const userKey = await db.query.native_attested_keys.findFirst({ + where: eq(native_attested_keys.key_id, 'test-key-1'), + }); + expect(userKey).toBeUndefined(); + + // Other user's key must remain + const otherKey = await db.query.native_attested_keys.findFirst({ + where: eq(native_attested_keys.key_id, 'test-key-2'), + }); + expect(otherKey).toBeDefined(); + + // Challenge persists (cleaned by cron, not by soft-delete) + const challenge = await db.query.native_admission_challenges.findFirst({ + where: eq(native_admission_challenges.challenge, 'test-challenge-1'), + }); + expect(challenge).toBeDefined(); + }); + it('should throw SoftDeletePreconditionError for active KiloClaw instance even without live subscription', async () => { const user = await insertTestUser(); diff --git a/apps/web/src/lib/user/index.ts b/apps/web/src/lib/user/index.ts index 83e787f3e2..c29f01e835 100644 --- a/apps/web/src/lib/user/index.ts +++ b/apps/web/src/lib/user/index.ts @@ -38,6 +38,8 @@ import { organization_recommendation_dismissals, magic_link_tokens, device_auth_requests, + device_sessions, + native_attested_keys, auto_top_up_configs, platform_integrations, platform_oauth_credentials, @@ -92,6 +94,7 @@ import { impact_conversion_reports, github_branch_pull_requests, user_github_app_tokens, + github_install_states, model_eval_ingestions, stripe_dispute_actions, stripe_dispute_cases, @@ -267,6 +270,12 @@ export type CreateOrUpdateUserTrackingContext = { countryCode?: string | null; }; +export type DeferredSignInEvent = { + distinctId: string; + event: string; + properties: Record; +}; + export async function findAndSyncExistingUser(args: CreateOrUpdateUserArgs) { const timer = createTimer(); const existing_kilo_user_id = await findUserIdByAuthProvider( @@ -515,12 +524,32 @@ export async function createOrUpdateUser( autoLinkToExistingUser: boolean = false, requestHeaders?: Headers, affiliateTrackingId?: string | null, - trackingContext?: CreateOrUpdateUserTrackingContext -): Promise> { + trackingContext?: CreateOrUpdateUserTrackingContext, + deferSignInAnalytics?: boolean +): Promise< + Result<{ user: User; isNew: boolean; deferredSignInEvent?: DeferredSignInEvent }, AuthErrorType> +> { const existingUser = await findAndSyncExistingUser(args); if (existingUser) { void fireAuthEvent(existingUser, 'signin', args.provider, requestHeaders); + if (deferSignInAnalytics) { + return successResult({ + user: existingUser, + isNew: false, + deferredSignInEvent: { + distinctId: existingUser.google_user_email, + event: 'user_signed_in', + properties: { + name: existingUser.google_user_name, + hosted_domain: existingUser.hosted_domain, + provider: args.provider, + id: existingUser.id, + }, + }, + }); + } + // User signed in or is being updated posthogClient.capture({ distinctId: existingUser.google_user_email, @@ -588,6 +617,28 @@ export async function createOrUpdateUser( } void fireAuthEvent(linkedUser, 'signin', args.provider, requestHeaders); // Successfully linked account, return the existing user + if (deferSignInAnalytics) { + return successResult({ + user: linkedUser, + isNew: false, + deferredSignInEvent: { + distinctId: userByEmail.google_user_email, + event: 'user_signed_in_with_different_id_and_auto_linked', + properties: { + existing_name: userByEmail.google_user_name, + existing_hosted_domain: userByEmail.hosted_domain, + existing_id: userByEmail.id, + new_provider: args.provider, + new_provider_account_id: args.provider_account_id, + new_name: args.google_user_name, + new_email: args.google_user_email, + new_image_url: args.google_user_image_url, + new_hosted_domain: args.hosted_domain, + }, + }, + }); + } + posthogClient.capture({ distinctId: userByEmail.google_user_email, event: 'user_signed_in_with_different_id_and_auto_linked', @@ -1258,6 +1309,11 @@ export async function softDeleteUser(userId: string) { .delete(code_review_feedback_events) .where(eq(code_review_feedback_events.owned_by_user_id, userId)); await tx.delete(device_auth_requests).where(eq(device_auth_requests.kilo_user_id, userId)); + // device_sessions cascade deletes device_refresh_tokens via FK + await tx.delete(device_sessions).where(eq(device_sessions.kilo_user_id, userId)); + await tx.delete(native_attested_keys).where(eq(native_attested_keys.kilo_user_id, userId)); + // native_admission_challenges are ephemeral (cleaned by cron) and have no user FK + await tx.delete(github_install_states).where(eq(github_install_states.kilo_user_id, userId)); await tx.delete(auto_top_up_configs).where(eq(auto_top_up_configs.owned_by_user_id, userId)); await tx.delete(kiloclaw_access_codes).where(eq(kiloclaw_access_codes.kilo_user_id, userId)); await tx diff --git a/apps/web/src/routers/github-apps-router.test.ts b/apps/web/src/routers/github-apps-router.test.ts index dbba604e49..1c073215f9 100644 --- a/apps/web/src/routers/github-apps-router.test.ts +++ b/apps/web/src/routers/github-apps-router.test.ts @@ -3,6 +3,7 @@ import { createCallerFactory } from '@/lib/trpc/init'; import type { User } from '@kilocode/db/schema'; import type { Owner } from '@/lib/integrations/core/types'; import type { GitHubAppType } from '@/lib/integrations/platforms/github/app-selector'; +import type { UpsertPlatformIntegrationResult } from '@/lib/integrations/db/platform-integrations'; type TestIntegration = { id: string; @@ -22,7 +23,9 @@ type InstallationDetails = { const mockGetIntegrationForOwner = jest.fn<(owner: Owner, platform: string) => Promise>(); const mockUpsertPlatformIntegrationForOwner = - jest.fn<(owner: Owner, details: Record) => Promise>(); + jest.fn< + (owner: Owner, details: Record) => Promise + >(); const mockUpdateRepositoriesForIntegration = jest.fn<(integrationId: string, repositories: unknown[]) => Promise>(); const mockFetchGitHubInstallationDetails = @@ -87,7 +90,7 @@ describe('githubAppsRouter.refreshInstallation', () => { created_at: '2026-01-01T00:00:00.000Z', }); mockFetchGitHubRepositories.mockResolvedValue([]); - mockUpsertPlatformIntegrationForOwner.mockResolvedValue(undefined); + mockUpsertPlatformIntegrationForOwner.mockResolvedValue({ ok: true }); mockUpdateRepositoriesForIntegration.mockResolvedValue(undefined); }); diff --git a/apps/web/src/routers/github-apps-router.ts b/apps/web/src/routers/github-apps-router.ts index 1a40f1d02d..66253d84ea 100644 --- a/apps/web/src/routers/github-apps-router.ts +++ b/apps/web/src/routers/github-apps-router.ts @@ -32,6 +32,7 @@ import { getGitHubUserAuthorizationStatus, } from '@/lib/integrations/platforms/github/user-authorization'; import { seedUserGithubToken } from '@/lib/github-pr-review/dev-seed'; +import { createInstallState } from '@/lib/integrations/github/install-state'; export const githubAppsRouter = createTRPCRouter({ // List all integrations @@ -87,6 +88,35 @@ export const githubAppsRouter = createTRPCRouter({ return getGitHubAppTypeForOrganization(input?.organizationId ?? null); }), + // Mint a one-time install state token for the signed-in user. + mintInstallState: baseProcedure + .input( + z.object({ + organizationId: z.string().uuid().optional(), + returnTo: z.string().optional(), + }) + ) + .mutation(async ({ ctx, input }) => { + // Any org member can start an install, matching the pre-C1 callback, + // which called ensureOrganizationAccess with no role filter. + const owner = await resolveAuthorizedOwner(ctx, input.organizationId, [ + 'owner', + 'billing_manager', + 'member', + ]); + const appType = await getGitHubAppTypeForOrganization(input.organizationId ?? null); + + const token = await createInstallState({ + kiloUserId: ctx.user.id, + ownerType: owner.type, + ownerId: owner.id, + githubAppType: appType, + returnTo: input.returnTo ?? null, + }); + + return { token }; + }), + // Get GitHub App installation status getInstallation: baseProcedure.input(optionalOrgInput).query(async ({ ctx, input }) => { if (input?.organizationId) { @@ -293,7 +323,7 @@ export const githubAppsRouter = createTRPCRouter({ }); } - await upsertPlatformIntegrationForOwner(owner, { + const upsertResult = await upsertPlatformIntegrationForOwner(owner, { platform: 'github', integrationType: 'app', platformInstallationId: installationId, @@ -303,8 +333,18 @@ export const githubAppsRouter = createTRPCRouter({ scopes: installationDetails.events, repositoryAccess: installationDetails.repository_selection, installedAt: installationDetails.created_at, + // Keep the integration's app type so a lite refresh is never matched + // against (or converted into) the standard app's row. + githubAppType: appType, }); + if (!upsertResult.ok) { + throw new TRPCError({ + code: 'CONFLICT', + message: 'This GitHub installation is already claimed by another account.', + }); + } + const repositories = await fetchGitHubRepositories(installationId, appType); await updateRepositoriesForIntegration(integration.id, repositories); @@ -348,7 +388,7 @@ export const githubAppsRouter = createTRPCRouter({ const owner = resolveOwner(ctx, input.organizationId); - await upsertPlatformIntegrationForOwner(owner, { + const devUpsertResult = await upsertPlatformIntegrationForOwner(owner, { platform: 'github', integrationType: 'app', platformInstallationId: input.installationId, @@ -361,6 +401,13 @@ export const githubAppsRouter = createTRPCRouter({ githubAppType: appType, }); + if (!devUpsertResult.ok) { + throw new TRPCError({ + code: 'CONFLICT', + message: 'This GitHub installation is already claimed by another account.', + }); + } + const integration = await getIntegrationForOwner(owner, 'github'); if (integration) { const repositories = await fetchGitHubRepositories(input.installationId, appType); diff --git a/apps/web/src/sentry.server.config.test.ts b/apps/web/src/sentry.server.config.test.ts new file mode 100644 index 0000000000..b485443ba4 --- /dev/null +++ b/apps/web/src/sentry.server.config.test.ts @@ -0,0 +1,278 @@ +import { describe, test, expect } from '@jest/globals'; +import type { Event } from '@sentry/nextjs'; +import { sanitizeSentryRequestData } from '../sentry.server.config'; + +describe('sanitizeSentryRequestData', () => { + test('removes the GitHub OAuth state token from request URL and query string', () => { + const event: Event = { + request: { + url: 'https://app.kilo.sh/api/integrations/github/callback?code=auth-code&state=raw-state-token&installation_id=12345', + query_string: 'code=auth-code&state=raw-state-token&installation_id=12345', + method: 'GET', + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.url).toBe( + 'https://app.kilo.sh/api/integrations/github/callback?code=auth-code&installation_id=12345' + ); + expect(result.request?.query_string).toBe('code=auth-code&installation_id=12345'); + }); + + test('keeps unrelated parameters when no state is present', () => { + const event: Event = { + request: { + url: 'https://app.kilo.sh/api/integrations/github/callback?code=auth-code&installation_id=12345', + query_string: 'code=auth-code&installation_id=12345', + method: 'GET', + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.url).toBe( + 'https://app.kilo.sh/api/integrations/github/callback?code=auth-code&installation_id=12345' + ); + expect(result.request?.query_string).toBe('code=auth-code&installation_id=12345'); + }); + + test('removes state from an object-form query string', () => { + const event: Event = { + request: { + url: 'https://app.kilo.sh/api/integrations/github/callback', + query_string: { code: 'auth-code', state: 'raw-state-token' }, + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.query_string).toEqual({ code: 'auth-code' }); + }); + + test('removes percent-encoded state keys from the request URL and query string', () => { + const event: Event = { + request: { + url: 'https://app.kilo.sh/api/integrations/github/callback?code=auth-code&%73tate=raw-state-token', + query_string: 'code=auth-code&%73tate=raw-state-token', + method: 'GET', + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.url).toBe( + 'https://app.kilo.sh/api/integrations/github/callback?code=auth-code' + ); + expect(result.request?.query_string).toBe('code=auth-code'); + }); + + test('removes every duplicate and mixed-case state key while keeping encoded values', () => { + const event: Event = { + request: { + query_string: + 'code=auth-code&state=first&STATE=second&state=third&st%61te=fourth&redirect=%2Fhome', + method: 'GET', + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.query_string).toBe('code=auth-code&redirect=%2Fhome'); + }); + + test('removes mixed-case state keys from the request URL', () => { + const event: Event = { + request: { + url: 'https://app.kilo.sh/api/integrations/github/callback?code=auth-code&State=token', + method: 'GET', + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.url).toBe( + 'https://app.kilo.sh/api/integrations/github/callback?code=auth-code' + ); + }); + + test('removes mixed-case state keys from an object-form query string', () => { + const event: Event = { + request: { + url: 'https://app.kilo.sh/api/integrations/github/callback', + query_string: { code: 'auth-code', State: 'token' }, + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.query_string).toEqual({ code: 'auth-code' }); + }); + + test('removes percent-encoded state keys from an array-form query string', () => { + const event: Event = { + request: { + query_string: [ + ['code', 'auth-code'], + ['%73tate', 'raw-state-token'], + ['St%61te', 'second-token'], + ], + method: 'GET', + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.query_string).toEqual([['code', 'auth-code']]); + }); + + test('removes percent-encoded state keys from an object-form query string', () => { + const event: Event = { + request: { + query_string: { code: 'auth-code', '%73tate': 'raw-state-token' }, + method: 'GET', + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.query_string).toEqual({ code: 'auth-code' }); + }); + + test('sanitizes a relative request URL', () => { + const event: Event = { + request: { + url: '/api/integrations/github/callback?code=auth-code&state=raw-state-token', + method: 'GET', + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.url).toBe('/api/integrations/github/callback?code=auth-code'); + }); + + test('sanitizes a relative request URL with encoded keys while keeping the fragment', () => { + const event: Event = { + request: { + url: '/api/integrations/github/callback?code=auth-code&%73tate=raw-state-token#fragment', + method: 'GET', + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.url).toBe('/api/integrations/github/callback?code=auth-code#fragment'); + }); + + test('keeps a relative request URL without a query string untouched', () => { + const event: Event = { + request: { + url: '/api/integrations/github/callback', + method: 'GET', + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.url).toBe('/api/integrations/github/callback'); + }); + + test('removes the app flow installState bearer from the request URL and query string', () => { + const event: Event = { + request: { + url: 'https://app.kilo.sh/github-app?organizationId=org_123&installState=abc-token-123&fromApp=1', + query_string: 'organizationId=org_123&installState=abc-token-123&fromApp=1', + method: 'GET', + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.url).toBe( + 'https://app.kilo.sh/github-app?organizationId=org_123&fromApp=1' + ); + expect(result.request?.query_string).toBe('organizationId=org_123&fromApp=1'); + }); + + test('removes percent-encoded installState keys from the request URL and query string', () => { + const event: Event = { + request: { + url: 'https://app.kilo.sh/github-app?organizationId=org_123&%69nstallState=abc-token-123', + query_string: 'organizationId=org_123&%69nstallState=abc-token-123', + method: 'GET', + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.url).toBe('https://app.kilo.sh/github-app?organizationId=org_123'); + expect(result.request?.query_string).toBe('organizationId=org_123'); + }); + + test('removes installState from an object-form query string', () => { + const event: Event = { + request: { + url: 'https://app.kilo.sh/github-app', + query_string: { organizationId: 'org_123', installState: 'abc-token-123' }, + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.query_string).toEqual({ organizationId: 'org_123' }); + }); + + test('removes percent-encoded installState keys from an array-form query string', () => { + const event: Event = { + request: { + query_string: [ + ['organizationId', 'org_123'], + ['%69nstallState', 'abc-token-123'], + ], + method: 'GET', + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.query_string).toEqual([['organizationId', 'org_123']]); + }); + + test('removes both state and installState while keeping unrelated parameters', () => { + const event: Event = { + request: { + url: 'https://app.kilo.sh/github-app?organizationId=org_123&installState=abc-token-123&state=oauth-token&fromApp=1', + query_string: + 'organizationId=org_123&installState=abc-token-123&state=oauth-token&fromApp=1', + method: 'GET', + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.url).toBe( + 'https://app.kilo.sh/github-app?organizationId=org_123&fromApp=1' + ); + expect(result.request?.query_string).toBe('organizationId=org_123&fromApp=1'); + }); + + test('sanitizes a relative request URL with an installState bearer', () => { + const event: Event = { + request: { + url: '/github-app?organizationId=org_123&installState=abc-token-123&fromApp=1', + method: 'GET', + }, + }; + + const result = sanitizeSentryRequestData(event); + + expect(result.request?.url).toBe('/github-app?organizationId=org_123&fromApp=1'); + }); + + test('returns an event without request data untouched', () => { + const event: Event = { message: 'no request data' }; + + expect(sanitizeSentryRequestData(event)).toBe(event); + }); +}); diff --git a/apps/web/src/tests/setup/__mocks__/lib/integrations/platforms/github/adapter.ts b/apps/web/src/tests/setup/__mocks__/lib/integrations/platforms/github/adapter.ts index 6b02cb437c..18367d3041 100644 --- a/apps/web/src/tests/setup/__mocks__/lib/integrations/platforms/github/adapter.ts +++ b/apps/web/src/tests/setup/__mocks__/lib/integrations/platforms/github/adapter.ts @@ -22,8 +22,8 @@ export async function deleteGitHubInstallation(_installationId: string): Promise export async function exchangeGitHubOAuthCode( _code: string, _appType: GitHubAppType = 'standard' -): Promise<{ id: string; login: string }> { - return { id: '12345', login: 'octocat' }; +): Promise<{ id: string; login: string; accessToken: string }> { + return { id: '12345', login: 'octocat', accessToken: 'mock-access-token' }; } export async function getCollaboratorPermissionLevel( diff --git a/dev/local/tmux.test.ts b/dev/local/tmux.test.ts index 0d14674cae..2cf2c3c50b 100644 --- a/dev/local/tmux.test.ts +++ b/dev/local/tmux.test.ts @@ -130,6 +130,7 @@ test( { skip: !hasTmux }, () => { const sessionName = `kilo-tmux-test-${process.pid}-${Date.now()}`; + const otherSessionName = `${sessionName}-other`; const serviceName = 'nextjs'; const tmux = (...args: string[]) => execFileSync('tmux', args, { stdio: 'ignore' }); const tmuxOutput = (...args: string[]) => @@ -159,6 +160,12 @@ test( `${sessionName}:0.0` ); + // A second active session makes the tmux current session ambiguous. The + // old unqualified break-pane created the new window in the current + // session, which could be the other session; the session-qualified + // production fix always targets the requested source session. + tmux('new-session', '-d', '-s', otherSessionName, '-n', 'other', 'sleep 120'); + const newWindowIndex = breakPane(sessionName, 0, 1, serviceName); const window = listWindows(sessionName).find(entry => entry.index === newWindowIndex); @@ -173,12 +180,21 @@ test( ), '0' ); + assert.ok( + listWindows(otherSessionName).every(entry => entry.name !== serviceName), + 'broken-out window must not land in the other active session' + ); } finally { try { tmux('kill-session', '-t', sessionName); } catch { // Session may already be gone if tmux fails during setup. } + try { + tmux('kill-session', '-t', otherSessionName); + } catch { + // Session may already be gone if tmux fails during setup. + } } } ); diff --git a/packages/db/src/migrations/0205_device_auth_hardening.sql b/packages/db/src/migrations/0205_device_auth_hardening.sql new file mode 100644 index 0000000000..0e837706d0 --- /dev/null +++ b/packages/db/src/migrations/0205_device_auth_hardening.sql @@ -0,0 +1,147 @@ +CREATE TABLE "device_refresh_tokens" ( + "token_hash" text PRIMARY KEY NOT NULL, + "device_session_id" uuid NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "consumed_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "device_sessions" ( + "id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL, + "kilo_user_id" text NOT NULL, + "device_auth_request_id" uuid, + "user_agent" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "last_seen_at" timestamp with time zone DEFAULT now() NOT NULL, + "revoked_at" timestamp with time zone, + "revoked_reason" text +); +--> statement-breakpoint +CREATE TABLE "github_install_states" ( + "token" text PRIMARY KEY NOT NULL, + "kilo_user_id" text NOT NULL, + "owner_type" text NOT NULL, + "owner_id" text NOT NULL, + "github_app_type" text NOT NULL, + "return_to" text, + "expires_at" timestamp with time zone NOT NULL, + "consumed_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "github_install_states_owner_type_check" CHECK ("github_install_states"."owner_type" IN ('org', 'user')) +); +--> statement-breakpoint +CREATE TABLE "native_admission_challenges" ( + "challenge" text PRIMARY KEY NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "consumed_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "native_attested_keys" ( + "key_id" text PRIMARY KEY NOT NULL, + "kilo_user_id" text NOT NULL, + "platform" text NOT NULL, + "public_key" text NOT NULL, + "sign_count" integer DEFAULT 0 NOT NULL, + "last_used_at" timestamp with time zone, + "attested_at" timestamp with time zone NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "native_attested_keys_platform_check" CHECK ("native_attested_keys"."platform" IN ('ios', 'android')) +); +--> statement-breakpoint +ALTER TABLE "device_auth_requests" ADD COLUMN "consumed_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "device_auth_requests" ADD COLUMN "user_code" text;--> statement-breakpoint +ALTER TABLE "device_auth_requests" ADD COLUMN "device_code_hash" text;--> statement-breakpoint +ALTER TABLE "magic_link_tokens" ADD COLUMN "reserved_until" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "magic_link_tokens" ADD COLUMN "challenge_id" uuid;--> statement-breakpoint +ALTER TABLE "device_refresh_tokens" ADD CONSTRAINT "device_refresh_tokens_device_session_id_device_sessions_id_fk" FOREIGN KEY ("device_session_id") REFERENCES "public"."device_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "device_sessions" ADD CONSTRAINT "device_sessions_kilo_user_id_kilocode_users_id_fk" FOREIGN KEY ("kilo_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "github_install_states" ADD CONSTRAINT "github_install_states_kilo_user_id_kilocode_users_id_fk" FOREIGN KEY ("kilo_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "native_attested_keys" ADD CONSTRAINT "native_attested_keys_kilo_user_id_kilocode_users_id_fk" FOREIGN KEY ("kilo_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "IDX_device_refresh_tokens_device_session_id" ON "device_refresh_tokens" USING btree ("device_session_id");--> statement-breakpoint +CREATE INDEX "IDX_device_refresh_tokens_expires_at" ON "device_refresh_tokens" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "IDX_device_sessions_kilo_user_id" ON "device_sessions" USING btree ("kilo_user_id");--> statement-breakpoint +CREATE INDEX "IDX_device_sessions_revoked_at" ON "device_sessions" USING btree ("revoked_at");--> statement-breakpoint +CREATE INDEX "IDX_github_install_states_expires_at" ON "github_install_states" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "IDX_native_admission_challenges_expires_at" ON "native_admission_challenges" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "IDX_native_attested_keys_kilo_user_id" ON "native_attested_keys" USING btree ("kilo_user_id");--> statement-breakpoint +COMMIT;--> statement-breakpoint +CREATE UNIQUE INDEX CONCURRENTLY "UQ_device_auth_requests_device_code_hash" ON "device_auth_requests" USING btree ("device_code_hash") WHERE "device_auth_requests"."device_code_hash" IS NOT NULL;--> statement-breakpoint +CREATE INDEX CONCURRENTLY "IDX_device_auth_requests_user_code" ON "device_auth_requests" USING btree ("user_code") WHERE "device_auth_requests"."user_code" IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX CONCURRENTLY "UQ_magic_link_tokens_challenge_id" ON "magic_link_tokens" USING btree ("challenge_id") WHERE "magic_link_tokens"."challenge_id" IS NOT NULL;--> statement-breakpoint +BEGIN;--> statement-breakpoint +-- Backfill: survey duplicate GitHub installation rows and resolve them. +-- Rows grouped by (platform, github_app_type, platform_installation_id) +-- where platform = 'github' and platform_installation_id is not null. +-- Winner: newest installed_at, tie-broken by greatest id. +-- Losers: platform_installation_id set to NULL, integration_status = 'suspended'. +DO $$ +DECLARE + dup_group RECORD; + winner RECORD; + loser RECORD; + dup_count integer; +BEGIN + RAISE NOTICE '[github-dedup] Scanning for duplicate GitHub installation rows...'; + + SELECT count(*) INTO dup_count FROM ( + SELECT platform, github_app_type, platform_installation_id + FROM platform_integrations + WHERE platform = 'github' + AND platform_installation_id IS NOT NULL + GROUP BY platform, github_app_type, platform_installation_id + HAVING count(*) > 1 + ) sub; + + RAISE NOTICE '[github-dedup] Found % duplicate groups.', dup_count; + + FOR dup_group IN + SELECT platform, github_app_type, platform_installation_id + FROM platform_integrations + WHERE platform = 'github' + AND platform_installation_id IS NOT NULL + GROUP BY platform, github_app_type, platform_installation_id + HAVING count(*) > 1 + LOOP + -- Identify the winner: newest installed_at, tie-broken by greatest id. + SELECT id, installed_at INTO winner + FROM platform_integrations + WHERE platform = 'github' + AND github_app_type = dup_group.github_app_type + AND platform_installation_id = dup_group.platform_installation_id + ORDER BY installed_at DESC NULLS LAST, id DESC + LIMIT 1; + + RAISE NOTICE '[github-dedup] Group (app_type=%, inst_id=%): winner id=%', dup_group.github_app_type, dup_group.platform_installation_id, winner.id; + + -- Null the installation id and suspend every loser. + FOR loser IN + SELECT id, platform_installation_id, owned_by_user_id, owned_by_organization_id + FROM platform_integrations + WHERE platform = 'github' + AND github_app_type = dup_group.github_app_type + AND platform_installation_id = dup_group.platform_installation_id + AND id != winner.id + LOOP + UPDATE platform_integrations + SET platform_installation_id = NULL, + integration_status = 'suspended', + suspended_at = now(), + suspended_by = 'migration-0205-github-dedup', + metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object( + 'github_dedup', jsonb_build_object( + 'suspended_at', now(), + 'reason', 'Duplicate installation resolved by migration 0205', + 'original_installation_id', loser.platform_installation_id + ) + ), + updated_at = now() + WHERE id = loser.id; + + RAISE NOTICE '[github-dedup] Suspended loser id=%, original_installation_id=%', loser.id, loser.platform_installation_id; + END LOOP; + END LOOP; +END $$;--> statement-breakpoint +COMMIT;--> statement-breakpoint +CREATE UNIQUE INDEX CONCURRENTLY "UQ_platform_integrations_github_platform_inst" ON "platform_integrations" USING btree ("platform","github_app_type","platform_installation_id") WHERE "platform_integrations"."platform" = 'github' AND "platform_integrations"."platform_installation_id" IS NOT NULL;--> statement-breakpoint +BEGIN; diff --git a/packages/db/src/migrations/meta/0205_snapshot.json b/packages/db/src/migrations/meta/0205_snapshot.json new file mode 100644 index 0000000000..466ad4c769 --- /dev/null +++ b/packages/db/src/migrations/meta/0205_snapshot.json @@ -0,0 +1,36329 @@ +{ + "id": "608de91d-8833-4991-b4e2-e32402f69d60", + "prevId": "edb9a758-95b4-4b80-9cf4-b8477452be43", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agent_configs": { + "name": "agent_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_type": { + "name": "agent_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "runtime_state": { + "name": "runtime_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_configs_org_id": { + "name": "IDX_agent_configs_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_owned_by_user_id": { + "name": "IDX_agent_configs_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_agent_type": { + "name": "IDX_agent_configs_agent_type", + "columns": [ + { + "expression": "agent_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_platform": { + "name": "IDX_agent_configs_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_configs_owned_by_organization_id_organizations_id_fk": { + "name": "agent_configs_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_configs", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_configs_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_configs_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_configs_org_agent_platform": { + "name": "UQ_agent_configs_org_agent_platform", + "nullsNotDistinct": false, + "columns": [ + "owned_by_organization_id", + "agent_type", + "platform" + ] + }, + "UQ_agent_configs_user_agent_platform": { + "name": "UQ_agent_configs_user_agent_platform", + "nullsNotDistinct": false, + "columns": [ + "owned_by_user_id", + "agent_type", + "platform" + ] + } + }, + "policies": {}, + "checkConstraints": { + "agent_configs_owner_check": { + "name": "agent_configs_owner_check", + "value": "(\n (\"agent_configs\".\"owned_by_user_id\" IS NOT NULL AND \"agent_configs\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_configs\".\"owned_by_user_id\" IS NULL AND \"agent_configs\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "agent_configs_agent_type_check": { + "name": "agent_configs_agent_type_check", + "value": "\"agent_configs\".\"agent_type\" IN ('code_review', 'auto_triage', 'auto_fix', 'security_scan')" + } + }, + "isRLSEnabled": false + }, + "public.agent_environment_profile_agents": { + "name": "agent_environment_profile_agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_agents_profile_id": { + "name": "IDX_agent_env_profile_agents_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_agents_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_agents_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_agents", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_agents_profile_slug": { + "name": "UQ_agent_env_profile_agents_profile_slug", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_commands": { + "name": "agent_environment_profile_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_commands_profile_id": { + "name": "IDX_agent_env_profile_commands_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_commands_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_commands_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_commands", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_commands_profile_sequence": { + "name": "UQ_agent_env_profile_commands_profile_sequence", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "sequence" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_kilo_commands": { + "name": "agent_environment_profile_kilo_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subtask": { + "name": "subtask", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_kilo_cmds_profile_id": { + "name": "IDX_agent_env_profile_kilo_cmds_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_kilo_commands_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_kilo_commands_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_kilo_commands", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_kilo_cmds_profile_name": { + "name": "UQ_agent_env_profile_kilo_cmds_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_mcp_servers": { + "name": "agent_environment_profile_mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_mcp_servers_profile_id": { + "name": "IDX_agent_env_profile_mcp_servers_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_mcp_servers_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_mcp_servers_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_mcp_servers", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_mcp_servers_profile_name": { + "name": "UQ_agent_env_profile_mcp_servers_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_repo_bindings": { + "name": "agent_environment_profile_repo_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_agent_env_profile_repo_bindings_user": { + "name": "UQ_agent_env_profile_repo_bindings_user", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profile_repo_bindings_org": { + "name": "UQ_agent_env_profile_repo_bindings_org", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_repo_bindings_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_repo_bindings_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profile_repo_bindings_owned_by_organization_id_organizations_id_fk": { + "name": "agent_environment_profile_repo_bindings_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profile_repo_bindings_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_environment_profile_repo_bindings_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_env_profile_repo_bindings_owner_check": { + "name": "agent_env_profile_repo_bindings_owner_check", + "value": "(\n (\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" IS NOT NULL AND \"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" IS NULL AND \"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.agent_environment_profile_skills": { + "name": "agent_environment_profile_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_markdown": { + "name": "raw_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_skills_profile_id": { + "name": "IDX_agent_env_profile_skills_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_skills_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_skills_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_skills", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_skills_profile_name": { + "name": "UQ_agent_env_profile_skills_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_vars": { + "name": "agent_environment_profile_vars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_vars_profile_id": { + "name": "IDX_agent_env_profile_vars_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_vars_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_vars_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_vars", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_vars_profile_key": { + "name": "UQ_agent_env_profile_vars_profile_key", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profiles": { + "name": "agent_environment_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_agent_env_profiles_org_name": { + "name": "UQ_agent_env_profiles_org_name", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_user_name": { + "name": "UQ_agent_env_profiles_user_name", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_org_default": { + "name": "UQ_agent_env_profiles_org_default", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"is_default\" = true AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_user_default": { + "name": "UQ_agent_env_profiles_user_default", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"is_default\" = true AND \"agent_environment_profiles\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_org_id": { + "name": "IDX_agent_env_profiles_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_user_id": { + "name": "IDX_agent_env_profiles_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_created_by_user_id": { + "name": "IDX_agent_env_profiles_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profiles_owned_by_organization_id_organizations_id_fk": { + "name": "agent_environment_profiles_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_environment_profiles", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profiles_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_environment_profiles_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_environment_profiles", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_env_profiles_owner_check": { + "name": "agent_env_profiles_owner_check", + "value": "(\n (\"agent_environment_profiles\".\"owned_by_user_id\" IS NOT NULL AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_environment_profiles\".\"owned_by_user_id\" IS NULL AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.api_kind": { + "name": "api_kind", + "schema": "", + "columns": { + "api_kind_id": { + "name": "api_kind_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "api_kind": { + "name": "api_kind", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_api_kind": { + "name": "UQ_api_kind", + "columns": [ + { + "expression": "api_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_request_compress_log": { + "name": "api_request_compress_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_api_request_compress_log_created_at": { + "name": "idx_api_request_compress_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_request_log": { + "name": "api_request_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vercel_request_id": { + "name": "vercel_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_api_request_log_created_at": { + "name": "idx_api_request_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_builder_feedback": { + "name": "app_builder_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_status": { + "name": "preview_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_streaming": { + "name": "is_streaming", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recent_messages": { + "name": "recent_messages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_app_builder_feedback_created_at": { + "name": "IDX_app_builder_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_feedback_kilo_user_id": { + "name": "IDX_app_builder_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_feedback_project_id": { + "name": "IDX_app_builder_feedback_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_builder_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "app_builder_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "app_builder_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "app_builder_feedback_project_id_app_builder_projects_id_fk": { + "name": "app_builder_feedback_project_id_app_builder_projects_id_fk", + "tableFrom": "app_builder_feedback", + "tableTo": "app_builder_projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_builder_project_sessions": { + "name": "app_builder_project_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "worker_version": { + "name": "worker_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'v2'" + } + }, + "indexes": { + "IDX_app_builder_project_sessions_project_id": { + "name": "IDX_app_builder_project_sessions_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_builder_project_sessions_project_id_app_builder_projects_id_fk": { + "name": "app_builder_project_sessions_project_id_app_builder_projects_id_fk", + "tableFrom": "app_builder_project_sessions", + "tableTo": "app_builder_projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_app_builder_project_sessions_cloud_agent_session_id": { + "name": "UQ_app_builder_project_sessions_cloud_agent_session_id", + "nullsNotDistinct": false, + "columns": [ + "cloud_agent_session_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_builder_projects": { + "name": "app_builder_projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "git_repo_full_name": { + "name": "git_repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_platform_integration_id": { + "name": "git_platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "migrated_at": { + "name": "migrated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_app_builder_projects_created_by_user_id": { + "name": "IDX_app_builder_projects_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_owned_by_user_id": { + "name": "IDX_app_builder_projects_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_owned_by_organization_id": { + "name": "IDX_app_builder_projects_owned_by_organization_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_created_at": { + "name": "IDX_app_builder_projects_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_last_message_at": { + "name": "IDX_app_builder_projects_last_message_at", + "columns": [ + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_app_builder_projects_git_repo_integration": { + "name": "IDX_app_builder_projects_git_repo_integration", + "columns": [ + { + "expression": "git_repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"app_builder_projects\".\"git_repo_full_name\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_builder_projects_owned_by_user_id_kilocode_users_id_fk": { + "name": "app_builder_projects_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "app_builder_projects_owned_by_organization_id_organizations_id_fk": { + "name": "app_builder_projects_owned_by_organization_id_organizations_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "app_builder_projects_deployment_id_deployments_id_fk": { + "name": "app_builder_projects_deployment_id_deployments_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "app_builder_projects_git_platform_integration_id_platform_integrations_id_fk": { + "name": "app_builder_projects_git_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "app_builder_projects", + "tableTo": "platform_integrations", + "columnsFrom": [ + "git_platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "app_builder_projects_owner_check": { + "name": "app_builder_projects_owner_check", + "value": "(\n (\"app_builder_projects\".\"owned_by_user_id\" IS NOT NULL AND \"app_builder_projects\".\"owned_by_organization_id\" IS NULL) OR\n (\"app_builder_projects\".\"owned_by_user_id\" IS NULL AND \"app_builder_projects\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.app_min_versions": { + "name": "app_min_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "ios_min_version": { + "name": "ios_min_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "android_min_version": { + "name": "android_min_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_reported_messages": { + "name": "app_reported_messages", + "schema": "", + "columns": { + "report_id": { + "name": "report_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "report_type": { + "name": "report_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signature": { + "name": "signature", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "app_reported_messages_cli_session_id_cli_sessions_session_id_fk": { + "name": "app_reported_messages_cli_session_id_cli_sessions_session_id_fk", + "tableFrom": "app_reported_messages", + "tableTo": "cli_sessions", + "columnsFrom": [ + "cli_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_fix_tickets": { + "name": "auto_fix_tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "triage_ticket_id": { + "name": "triage_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issue_url": { + "name": "issue_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_body": { + "name": "issue_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_author": { + "name": "issue_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_labels": { + "name": "issue_labels", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "trigger_source": { + "name": "trigger_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'label'" + }, + "review_comment_id": { + "name": "review_comment_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "review_comment_body": { + "name": "review_comment_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "diff_hunk": { + "name": "diff_hunk", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_head_ref": { + "name": "pr_head_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "intent_summary": { + "name": "intent_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_files": { + "name": "related_files", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_branch": { + "name": "pr_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_auto_fix_tickets_repo_issue": { + "name": "UQ_auto_fix_tickets_repo_issue", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_fix_tickets\".\"trigger_source\" = 'label'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_auto_fix_tickets_repo_review_comment": { + "name": "UQ_auto_fix_tickets_repo_review_comment", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "review_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_fix_tickets\".\"review_comment_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_owned_by_org": { + "name": "IDX_auto_fix_tickets_owned_by_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_owned_by_user": { + "name": "IDX_auto_fix_tickets_owned_by_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_status": { + "name": "IDX_auto_fix_tickets_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_created_at": { + "name": "IDX_auto_fix_tickets_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_triage_ticket_id": { + "name": "IDX_auto_fix_tickets_triage_ticket_id", + "columns": [ + { + "expression": "triage_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_fix_tickets_session_id": { + "name": "IDX_auto_fix_tickets_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_fix_tickets_owned_by_organization_id_organizations_id_fk": { + "name": "auto_fix_tickets_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_fix_tickets_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_fix_tickets_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_fix_tickets_platform_integration_id_platform_integrations_id_fk": { + "name": "auto_fix_tickets_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auto_fix_tickets_triage_ticket_id_auto_triage_tickets_id_fk": { + "name": "auto_fix_tickets_triage_ticket_id_auto_triage_tickets_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "auto_triage_tickets", + "columnsFrom": [ + "triage_ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auto_fix_tickets_cli_session_id_cli_sessions_session_id_fk": { + "name": "auto_fix_tickets_cli_session_id_cli_sessions_session_id_fk", + "tableFrom": "auto_fix_tickets", + "tableTo": "cli_sessions", + "columnsFrom": [ + "cli_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_fix_tickets_owner_check": { + "name": "auto_fix_tickets_owner_check", + "value": "(\n (\"auto_fix_tickets\".\"owned_by_user_id\" IS NOT NULL AND \"auto_fix_tickets\".\"owned_by_organization_id\" IS NULL) OR\n (\"auto_fix_tickets\".\"owned_by_user_id\" IS NULL AND \"auto_fix_tickets\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "auto_fix_tickets_status_check": { + "name": "auto_fix_tickets_status_check", + "value": "\"auto_fix_tickets\".\"status\" IN ('pending', 'running', 'completed', 'failed', 'cancelled')" + }, + "auto_fix_tickets_classification_check": { + "name": "auto_fix_tickets_classification_check", + "value": "\"auto_fix_tickets\".\"classification\" IN ('bug', 'feature', 'question', 'unclear')" + }, + "auto_fix_tickets_confidence_check": { + "name": "auto_fix_tickets_confidence_check", + "value": "\"auto_fix_tickets\".\"confidence\" >= 0 AND \"auto_fix_tickets\".\"confidence\" <= 1" + }, + "auto_fix_tickets_trigger_source_check": { + "name": "auto_fix_tickets_trigger_source_check", + "value": "\"auto_fix_tickets\".\"trigger_source\" IN ('label', 'review_comment')" + } + }, + "isRLSEnabled": false + }, + "public.auto_model": { + "name": "auto_model", + "schema": "", + "columns": { + "auto_model_id": { + "name": "auto_model_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auto_model": { + "name": "auto_model", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_auto_model": { + "name": "UQ_auto_model", + "columns": [ + { + "expression": "auto_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_top_up_configs": { + "name": "auto_top_up_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_method_id": { + "name": "stripe_payment_method_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5000 + }, + "last_auto_top_up_at": { + "name": "last_auto_top_up_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_started_at": { + "name": "attempt_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_auto_top_up_configs_owned_by_user_id": { + "name": "UQ_auto_top_up_configs_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_top_up_configs\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_auto_top_up_configs_owned_by_organization_id": { + "name": "UQ_auto_top_up_configs_owned_by_organization_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_top_up_configs\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_top_up_configs_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_top_up_configs_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_top_up_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "auto_top_up_configs_owned_by_organization_id_organizations_id_fk": { + "name": "auto_top_up_configs_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_top_up_configs", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_top_up_configs_exactly_one_owner": { + "name": "auto_top_up_configs_exactly_one_owner", + "value": "(\"auto_top_up_configs\".\"owned_by_user_id\" IS NOT NULL AND \"auto_top_up_configs\".\"owned_by_organization_id\" IS NULL) OR (\"auto_top_up_configs\".\"owned_by_user_id\" IS NULL AND \"auto_top_up_configs\".\"owned_by_organization_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.auto_triage_tickets": { + "name": "auto_triage_tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issue_url": { + "name": "issue_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_body": { + "name": "issue_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_author": { + "name": "issue_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_type": { + "name": "issue_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_labels": { + "name": "issue_labels", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "intent_summary": { + "name": "intent_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_files": { + "name": "related_files", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "is_duplicate": { + "name": "is_duplicate", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "duplicate_of_ticket_id": { + "name": "duplicate_of_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "similarity_score": { + "name": "similarity_score", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "qdrant_point_id": { + "name": "qdrant_point_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "should_auto_fix": { + "name": "should_auto_fix", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "action_taken": { + "name": "action_taken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_metadata": { + "name": "action_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_auto_triage_tickets_repo_issue": { + "name": "UQ_auto_triage_tickets_repo_issue", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_owned_by_org": { + "name": "IDX_auto_triage_tickets_owned_by_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_owned_by_user": { + "name": "IDX_auto_triage_tickets_owned_by_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_status": { + "name": "IDX_auto_triage_tickets_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_created_at": { + "name": "IDX_auto_triage_tickets_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_qdrant_point_id": { + "name": "IDX_auto_triage_tickets_qdrant_point_id", + "columns": [ + { + "expression": "qdrant_point_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_owner_status_created": { + "name": "IDX_auto_triage_tickets_owner_status_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_user_status_created": { + "name": "IDX_auto_triage_tickets_user_status_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_triage_tickets_repo_classification": { + "name": "IDX_auto_triage_tickets_repo_classification", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "classification", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_triage_tickets_owned_by_organization_id_organizations_id_fk": { + "name": "auto_triage_tickets_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_triage_tickets_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_triage_tickets_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_triage_tickets_platform_integration_id_platform_integrations_id_fk": { + "name": "auto_triage_tickets_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auto_triage_tickets_duplicate_of_ticket_id_auto_triage_tickets_id_fk": { + "name": "auto_triage_tickets_duplicate_of_ticket_id_auto_triage_tickets_id_fk", + "tableFrom": "auto_triage_tickets", + "tableTo": "auto_triage_tickets", + "columnsFrom": [ + "duplicate_of_ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_triage_tickets_owner_check": { + "name": "auto_triage_tickets_owner_check", + "value": "(\n (\"auto_triage_tickets\".\"owned_by_user_id\" IS NOT NULL AND \"auto_triage_tickets\".\"owned_by_organization_id\" IS NULL) OR\n (\"auto_triage_tickets\".\"owned_by_user_id\" IS NULL AND \"auto_triage_tickets\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "auto_triage_tickets_issue_type_check": { + "name": "auto_triage_tickets_issue_type_check", + "value": "\"auto_triage_tickets\".\"issue_type\" IN ('issue', 'pull_request')" + }, + "auto_triage_tickets_classification_check": { + "name": "auto_triage_tickets_classification_check", + "value": "\"auto_triage_tickets\".\"classification\" IN ('bug', 'feature', 'question', 'duplicate', 'unclear')" + }, + "auto_triage_tickets_confidence_check": { + "name": "auto_triage_tickets_confidence_check", + "value": "\"auto_triage_tickets\".\"confidence\" >= 0 AND \"auto_triage_tickets\".\"confidence\" <= 1" + }, + "auto_triage_tickets_similarity_score_check": { + "name": "auto_triage_tickets_similarity_score_check", + "value": "\"auto_triage_tickets\".\"similarity_score\" >= 0 AND \"auto_triage_tickets\".\"similarity_score\" <= 1" + }, + "auto_triage_tickets_status_check": { + "name": "auto_triage_tickets_status_check", + "value": "\"auto_triage_tickets\".\"status\" IN ('pending', 'analyzing', 'actioned', 'failed', 'skipped')" + }, + "auto_triage_tickets_action_taken_check": { + "name": "auto_triage_tickets_action_taken_check", + "value": "\"auto_triage_tickets\".\"action_taken\" IN ('pr_created', 'comment_posted', 'closed_duplicate', 'needs_clarification')" + } + }, + "isRLSEnabled": false + }, + "public.bot_request_cloud_agent_sessions": { + "name": "bot_request_cloud_agent_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "bot_request_id": { + "name": "bot_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "spawn_group_id": { + "name": "spawn_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlab_project": { + "name": "gitlab_project", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "callback_step": { + "name": "callback_step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "final_message": { + "name": "final_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "final_message_fetched_at": { + "name": "final_message_fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "final_message_error": { + "name": "final_message_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "continuation_started_at": { + "name": "continuation_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_bot_request_cas_cloud_agent_session_id": { + "name": "UQ_bot_request_cas_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id": { + "name": "IDX_bot_request_cas_bot_request_id", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id_spawn_group_id": { + "name": "IDX_bot_request_cas_bot_request_id_spawn_group_id", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spawn_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id_spawn_group_id_status": { + "name": "IDX_bot_request_cas_bot_request_id_spawn_group_id_status", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spawn_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_request_cloud_agent_sessions_bot_request_id_bot_requests_id_fk": { + "name": "bot_request_cloud_agent_sessions_bot_request_id_bot_requests_id_fk", + "tableFrom": "bot_request_cloud_agent_sessions", + "tableTo": "bot_requests", + "columnsFrom": [ + "bot_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_requests": { + "name": "bot_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_thread_id": { + "name": "platform_thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_message_id": { + "name": "platform_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message": { + "name": "user_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "steps": { + "name": "steps", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_bot_requests_created_at": { + "name": "IDX_bot_requests_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_created_by": { + "name": "IDX_bot_requests_created_by", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_organization_id": { + "name": "IDX_bot_requests_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_platform_integration_id": { + "name": "IDX_bot_requests_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_status": { + "name": "IDX_bot_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_requests_created_by_kilocode_users_id_fk": { + "name": "bot_requests_created_by_kilocode_users_id_fk", + "tableFrom": "bot_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_requests_organization_id_organizations_id_fk": { + "name": "bot_requests_organization_id_organizations_id_fk", + "tableFrom": "bot_requests", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_requests_platform_integration_id_platform_integrations_id_fk": { + "name": "bot_requests_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "bot_requests", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.byok_api_keys": { + "name": "byok_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "management_source": { + "name": "management_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_byok_api_keys_organization_id": { + "name": "IDX_byok_api_keys_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_byok_api_keys_kilo_user_id": { + "name": "IDX_byok_api_keys_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_byok_api_keys_provider_id": { + "name": "IDX_byok_api_keys_provider_id", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "byok_api_keys_organization_id_organizations_id_fk": { + "name": "byok_api_keys_organization_id_organizations_id_fk", + "tableFrom": "byok_api_keys", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "byok_api_keys_kilo_user_id_kilocode_users_id_fk": { + "name": "byok_api_keys_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "byok_api_keys", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_byok_api_keys_org_provider": { + "name": "UQ_byok_api_keys_org_provider", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "provider_id" + ] + }, + "UQ_byok_api_keys_user_provider": { + "name": "UQ_byok_api_keys_user_provider", + "nullsNotDistinct": false, + "columns": [ + "kilo_user_id", + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "byok_api_keys_management_source_check": { + "name": "byok_api_keys_management_source_check", + "value": "\"byok_api_keys\".\"management_source\" IN ('user', 'coding_plan')" + }, + "byok_api_keys_owner_check": { + "name": "byok_api_keys_owner_check", + "value": "(\n (\"byok_api_keys\".\"kilo_user_id\" IS NOT NULL AND \"byok_api_keys\".\"organization_id\" IS NULL) OR\n (\"byok_api_keys\".\"kilo_user_id\" IS NULL AND \"byok_api_keys\".\"organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.cli_sessions": { + "name": "cli_sessions", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_on_platform": { + "name": "created_on_platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "api_conversation_history_blob_url": { + "name": "api_conversation_history_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_metadata_blob_url": { + "name": "task_metadata_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ui_messages_blob_url": { + "name": "ui_messages_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_state_blob_url": { + "name": "git_state_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "forked_from": { + "name": "forked_from", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_session_id": { + "name": "parent_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_mode": { + "name": "last_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_model": { + "name": "last_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cli_sessions_kilo_user_id": { + "name": "IDX_cli_sessions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_created_at": { + "name": "IDX_cli_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_updated_at": { + "name": "IDX_cli_sessions_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_organization_id": { + "name": "IDX_cli_sessions_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_user_updated": { + "name": "IDX_cli_sessions_user_updated", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "cli_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "cli_sessions_forked_from_cli_sessions_session_id_fk": { + "name": "cli_sessions_forked_from_cli_sessions_session_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "forked_from" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_parent_session_id_cli_sessions_session_id_fk": { + "name": "cli_sessions_parent_session_id_cli_sessions_session_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "parent_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_organization_id_organizations_id_fk": { + "name": "cli_sessions_organization_id_organizations_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cli_sessions_cloud_agent_session_id_unique": { + "name": "cli_sessions_cloud_agent_session_id_unique", + "nullsNotDistinct": false, + "columns": [ + "cloud_agent_session_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cli_sessions_v2": { + "name": "cli_sessions_v2", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_session_id": { + "name": "parent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_scope_id": { + "name": "cloud_agent_session_scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_on_platform": { + "name": "created_on_platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_updated_at": { + "name": "status_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cli_sessions_v2_parent_session_id_kilo_user_id": { + "name": "IDX_cli_sessions_v2_parent_session_id_kilo_user_id", + "columns": [ + { + "expression": "parent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cli_sessions_v2_public_id": { + "name": "UQ_cli_sessions_v2_public_id", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cli_sessions_v2\".\"public_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cli_sessions_v2_cloud_agent_session_id": { + "name": "UQ_cli_sessions_v2_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cli_sessions_v2\".\"cloud_agent_session_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_organization_id": { + "name": "IDX_cli_sessions_v2_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_kilo_user_id": { + "name": "IDX_cli_sessions_v2_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_created_at": { + "name": "IDX_cli_sessions_v2_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_user_updated": { + "name": "IDX_cli_sessions_v2_user_updated", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_sessions_v2_git_url_branch_idx": { + "name": "cli_sessions_v2_git_url_branch_idx", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_sessions_v2_kilo_user_id_kilocode_users_id_fk": { + "name": "cli_sessions_v2_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "cli_sessions_v2_organization_id_organizations_id_fk": { + "name": "cli_sessions_v2_organization_id_organizations_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_v2_parent_session_id_kilo_user_id_fk": { + "name": "cli_sessions_v2_parent_session_id_kilo_user_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "cli_sessions_v2", + "columnsFrom": [ + "parent_session_id", + "kilo_user_id" + ], + "columnsTo": [ + "session_id", + "kilo_user_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "cli_sessions_v2_session_id_kilo_user_id_pk": { + "name": "cli_sessions_v2_session_id_kilo_user_id_pk", + "columns": [ + "session_id", + "kilo_user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_agent_code_review_attempts": { + "name": "cloud_agent_code_review_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code_review_id": { + "name": "code_review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "attempt_number": { + "name": "attempt_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "retry_of_attempt_id": { + "name": "retry_of_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retry_reason": { + "name": "retry_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analytics_enabled_at_dispatch": { + "name": "analytics_enabled_at_dispatch", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_reason": { + "name": "terminal_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cloud_agent_code_review_attempts_review_attempt_number": { + "name": "UQ_cloud_agent_code_review_attempts_review_attempt_number", + "columns": [ + { + "expression": "code_review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_code_review_id": { + "name": "idx_cloud_agent_code_review_attempts_code_review_id", + "columns": [ + { + "expression": "code_review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_session_id": { + "name": "idx_cloud_agent_code_review_attempts_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_cli_session_id": { + "name": "idx_cloud_agent_code_review_attempts_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_status": { + "name": "idx_cloud_agent_code_review_attempts_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_retry_reason": { + "name": "idx_cloud_agent_code_review_attempts_retry_reason", + "columns": [ + { + "expression": "retry_reason", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_code_review_attempts_code_review_id_cloud_agent_code_reviews_id_fk": { + "name": "cloud_agent_code_review_attempts_code_review_id_cloud_agent_code_reviews_id_fk", + "tableFrom": "cloud_agent_code_review_attempts", + "tableTo": "cloud_agent_code_reviews", + "columnsFrom": [ + "code_review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_review_attempts_retry_of_attempt_id_cloud_agent_code_review_attempts_id_fk": { + "name": "cloud_agent_code_review_attempts_retry_of_attempt_id_cloud_agent_code_review_attempts_id_fk", + "tableFrom": "cloud_agent_code_review_attempts", + "tableTo": "cloud_agent_code_review_attempts", + "columnsFrom": [ + "retry_of_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_code_review_attempts_attempt_number_check": { + "name": "cloud_agent_code_review_attempts_attempt_number_check", + "value": "\"cloud_agent_code_review_attempts\".\"attempt_number\" >= 1" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_code_reviews": { + "name": "cloud_agent_code_reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "manual_config": { + "name": "manual_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "review_type": { + "name": "review_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "trigger_source": { + "name": "trigger_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "council_result": { + "name": "council_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_title": { + "name": "pr_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_author": { + "name": "pr_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_author_github_id": { + "name": "pr_author_github_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_ref": { + "name": "head_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "platform_project_id": { + "name": "platform_project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "dispatch_reservation_id": { + "name": "dispatch_reservation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_reason": { + "name": "terminal_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'v1'" + }, + "check_run_id": { + "name": "check_run_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "repository_review_instructions_used": { + "name": "repository_review_instructions_used", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "repository_review_instructions_ref": { + "name": "repository_review_instructions_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_review_instructions_truncated": { + "name": "repository_review_instructions_truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previous_summary_body": { + "name": "previous_summary_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_summary_head_sha": { + "name": "previous_summary_head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_tokens_in": { + "name": "total_tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_tokens_out": { + "name": "total_tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_cost_musd": { + "name": "total_cost_musd", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cloud_agent_code_reviews_webhook_integration_repo_pr_sha": { + "name": "UQ_cloud_agent_code_reviews_webhook_integration_repo_pr_sha", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "head_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_code_reviews\".\"manual_config\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_code_reviews_active_provider_publisher": { + "name": "UQ_cloud_agent_code_reviews_active_provider_publisher", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_code_reviews\".\"platform_integration_id\" IS NOT NULL\n AND \"cloud_agent_code_reviews\".\"status\" IN ('pending', 'queued', 'running')\n AND (\"cloud_agent_code_reviews\".\"manual_config\" IS NULL OR \"cloud_agent_code_reviews\".\"manual_config\"->>'outputMode' = 'provider')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_owned_by_org_id": { + "name": "idx_cloud_agent_code_reviews_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_owned_by_user_id": { + "name": "idx_cloud_agent_code_reviews_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_session_id": { + "name": "idx_cloud_agent_code_reviews_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_cli_session_id": { + "name": "idx_cloud_agent_code_reviews_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_status": { + "name": "idx_cloud_agent_code_reviews_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_repo": { + "name": "idx_cloud_agent_code_reviews_repo", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_pr_number": { + "name": "idx_cloud_agent_code_reviews_pr_number", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_created_at": { + "name": "idx_cloud_agent_code_reviews_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_pr_author_github_id": { + "name": "idx_cloud_agent_code_reviews_pr_author_github_id", + "columns": [ + { + "expression": "pr_author_github_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_code_reviews_owned_by_organization_id_organizations_id_fk": { + "name": "cloud_agent_code_reviews_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_reviews_owned_by_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_code_reviews_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_reviews_platform_integration_id_platform_integrations_id_fk": { + "name": "cloud_agent_code_reviews_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_code_reviews_owner_check": { + "name": "cloud_agent_code_reviews_owner_check", + "value": "(\n (\"cloud_agent_code_reviews\".\"owned_by_user_id\" IS NOT NULL AND \"cloud_agent_code_reviews\".\"owned_by_organization_id\" IS NULL) OR\n (\"cloud_agent_code_reviews\".\"owned_by_user_id\" IS NULL AND \"cloud_agent_code_reviews\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_feedback": { + "name": "cloud_agent_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_type": { + "name": "session_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_streaming": { + "name": "is_streaming", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recent_messages": { + "name": "recent_messages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cloud_agent_feedback_created_at": { + "name": "IDX_cloud_agent_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_feedback_kilo_user_id": { + "name": "IDX_cloud_agent_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_feedback_cloud_agent_session_id": { + "name": "IDX_cloud_agent_feedback_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "cloud_agent_feedback_organization_id_organizations_id_fk": { + "name": "cloud_agent_feedback_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_feedback", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_agent_session_runs": { + "name": "cloud_agent_session_runs", + "schema": "", + "columns": { + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapper_run_id": { + "name": "wrapper_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dispatch_accepted_at": { + "name": "dispatch_accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "agent_activity_observed_at": { + "name": "agent_activity_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_stage": { + "name": "failure_stage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_responsibility": { + "name": "failure_responsibility", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message_redacted": { + "name": "error_message_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_expires_at": { + "name": "error_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_cloud_agent_session_runs_wrapper_run_id": { + "name": "IDX_cloud_agent_session_runs_wrapper_run_id", + "columns": [ + { + "expression": "wrapper_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_session_runs\".\"wrapper_run_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_session_queued": { + "name": "IDX_cloud_agent_session_runs_session_queued", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_queued_at": { + "name": "IDX_cloud_agent_session_runs_queued_at", + "columns": [ + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_terminal_at": { + "name": "IDX_cloud_agent_session_runs_terminal_at", + "columns": [ + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_status_terminal": { + "name": "IDX_cloud_agent_session_runs_status_terminal", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_failure_terminal": { + "name": "IDX_cloud_agent_session_runs_failure_terminal", + "columns": [ + { + "expression": "failure_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_responsibility_reason_terminal": { + "name": "IDX_cloud_agent_session_runs_responsibility_reason_terminal", + "columns": [ + { + "expression": "failure_responsibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_reason", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_session_runs\".\"status\" = 'failed'", + "concurrently": true, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_error_expires_at": { + "name": "IDX_cloud_agent_session_runs_error_expires_at", + "columns": [ + { + "expression": "error_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_session_runs\".\"error_expires_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_session_runs_cloud_agent_session_id_cloud_agent_sessions_cloud_agent_session_id_fk": { + "name": "cloud_agent_session_runs_cloud_agent_session_id_cloud_agent_sessions_cloud_agent_session_id_fk", + "tableFrom": "cloud_agent_session_runs", + "tableTo": "cloud_agent_sessions", + "columnsFrom": [ + "cloud_agent_session_id" + ], + "columnsTo": [ + "cloud_agent_session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "cloud_agent_session_runs_cloud_agent_session_id_message_id_pk": { + "name": "cloud_agent_session_runs_cloud_agent_session_id_message_id_pk", + "columns": [ + "cloud_agent_session_id", + "message_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_session_runs_status_check": { + "name": "cloud_agent_session_runs_status_check", + "value": "\"cloud_agent_session_runs\".\"status\" IN ('queued', 'accepted', 'completed', 'failed', 'interrupted')" + }, + "cloud_agent_session_runs_error_message_bounded_check": { + "name": "cloud_agent_session_runs_error_message_bounded_check", + "value": "\"cloud_agent_session_runs\".\"error_message_redacted\" IS NULL OR char_length(\"cloud_agent_session_runs\".\"error_message_redacted\") <= 4096" + }, + "cloud_agent_session_runs_error_expiry_check": { + "name": "cloud_agent_session_runs_error_expiry_check", + "value": "(\"cloud_agent_session_runs\".\"error_message_redacted\" IS NULL AND \"cloud_agent_session_runs\".\"error_expires_at\" IS NULL) OR\n (\"cloud_agent_session_runs\".\"error_message_redacted\" IS NOT NULL AND \"cloud_agent_session_runs\".\"error_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_sessions": { + "name": "cloud_agent_sessions", + "schema": "", + "columns": { + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initial_message_id": { + "name": "initial_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "failure_at": { + "name": "failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_stage": { + "name": "failure_stage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_responsibility": { + "name": "failure_responsibility", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message_redacted": { + "name": "error_message_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_expires_at": { + "name": "error_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_cloud_agent_sessions_kilo_session_id": { + "name": "UQ_cloud_agent_sessions_kilo_session_id", + "columns": [ + { + "expression": "kilo_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_sessions_initial_message_id": { + "name": "UQ_cloud_agent_sessions_initial_message_id", + "columns": [ + { + "expression": "initial_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_sandbox_id": { + "name": "IDX_cloud_agent_sessions_sandbox_id", + "columns": [ + { + "expression": "sandbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"sandbox_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_created_at": { + "name": "IDX_cloud_agent_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_failure_created": { + "name": "IDX_cloud_agent_sessions_failure_created", + "columns": [ + { + "expression": "failure_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_failure_at": { + "name": "IDX_cloud_agent_sessions_failure_at", + "columns": [ + { + "expression": "failure_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"failure_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_failure_classification_at": { + "name": "IDX_cloud_agent_sessions_failure_classification_at", + "columns": [ + { + "expression": "failure_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"failure_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_error_expires_at": { + "name": "IDX_cloud_agent_sessions_error_expires_at", + "columns": [ + { + "expression": "error_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"error_expires_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_sessions_failure_classification_check": { + "name": "cloud_agent_sessions_failure_classification_check", + "value": "(\"cloud_agent_sessions\".\"failure_at\" IS NULL AND \"cloud_agent_sessions\".\"failure_stage\" IS NULL AND \"cloud_agent_sessions\".\"failure_code\" IS NULL) OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'sandbox_identity' AND \"cloud_agent_sessions\".\"failure_code\" = 'sandbox_id_derivation_failed') OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'registration' AND \"cloud_agent_sessions\".\"failure_code\" = 'do_registration_rejected') OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'initial_admission' AND \"cloud_agent_sessions\".\"failure_code\" IN ('initial_admission_rejected', 'initial_queue_full', 'invalid_initial_intent')) OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'transport' AND \"cloud_agent_sessions\".\"failure_code\" = 'do_rpc_outcome_unknown')" + }, + "cloud_agent_sessions_error_message_bounded_check": { + "name": "cloud_agent_sessions_error_message_bounded_check", + "value": "\"cloud_agent_sessions\".\"error_message_redacted\" IS NULL OR char_length(\"cloud_agent_sessions\".\"error_message_redacted\") <= 4096" + }, + "cloud_agent_sessions_error_expiry_check": { + "name": "cloud_agent_sessions_error_expiry_check", + "value": "(\"cloud_agent_sessions\".\"error_message_redacted\" IS NULL AND \"cloud_agent_sessions\".\"error_expires_at\" IS NULL) OR\n (\"cloud_agent_sessions\".\"error_message_redacted\" IS NOT NULL AND \"cloud_agent_sessions\".\"error_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_webhook_triggers": { + "name": "cloud_agent_webhook_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trigger_id": { + "name": "trigger_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'cloud_agent'" + }, + "kiloclaw_instance_id": { + "name": "kiloclaw_instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "activation_mode": { + "name": "activation_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'webhook'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_timezone": { + "name": "cron_timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'UTC'" + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_cloud_agent_webhook_triggers_user_trigger": { + "name": "UQ_cloud_agent_webhook_triggers_user_trigger", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_webhook_triggers\".\"user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_webhook_triggers_org_trigger": { + "name": "UQ_cloud_agent_webhook_triggers_org_trigger", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_webhook_triggers\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_user": { + "name": "IDX_cloud_agent_webhook_triggers_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_org": { + "name": "IDX_cloud_agent_webhook_triggers_org", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_active": { + "name": "IDX_cloud_agent_webhook_triggers_active", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_profile": { + "name": "IDX_cloud_agent_webhook_triggers_profile", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_webhook_triggers_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_webhook_triggers_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_webhook_triggers_organization_id_organizations_id_fk": { + "name": "cloud_agent_webhook_triggers_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_webhook_triggers_kiloclaw_instance_id_kiloclaw_instances_id_fk": { + "name": "cloud_agent_webhook_triggers_kiloclaw_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "kiloclaw_instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cloud_agent_webhook_triggers_profile_id_agent_environment_profiles_id_fk": { + "name": "cloud_agent_webhook_triggers_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_cloud_agent_webhook_triggers_owner": { + "name": "CHK_cloud_agent_webhook_triggers_owner", + "value": "(\n (\"cloud_agent_webhook_triggers\".\"user_id\" IS NOT NULL AND \"cloud_agent_webhook_triggers\".\"organization_id\" IS NULL) OR\n (\"cloud_agent_webhook_triggers\".\"user_id\" IS NULL AND \"cloud_agent_webhook_triggers\".\"organization_id\" IS NOT NULL)\n )" + }, + "CHK_cloud_agent_webhook_triggers_cloud_agent_fields": { + "name": "CHK_cloud_agent_webhook_triggers_cloud_agent_fields", + "value": "(\n \"cloud_agent_webhook_triggers\".\"target_type\" != 'cloud_agent' OR\n (\"cloud_agent_webhook_triggers\".\"github_repo\" IS NOT NULL AND \"cloud_agent_webhook_triggers\".\"profile_id\" IS NOT NULL)\n )" + }, + "CHK_cloud_agent_webhook_triggers_kiloclaw_fields": { + "name": "CHK_cloud_agent_webhook_triggers_kiloclaw_fields", + "value": "(\n \"cloud_agent_webhook_triggers\".\"target_type\" != 'kiloclaw_chat' OR\n \"cloud_agent_webhook_triggers\".\"kiloclaw_instance_id\" IS NOT NULL\n )" + }, + "CHK_cloud_agent_webhook_triggers_scheduled_fields": { + "name": "CHK_cloud_agent_webhook_triggers_scheduled_fields", + "value": "(\n \"cloud_agent_webhook_triggers\".\"activation_mode\" != 'scheduled' OR\n \"cloud_agent_webhook_triggers\".\"cron_expression\" IS NOT NULL\n )" + } + }, + "isRLSEnabled": false + }, + "public.cloud_billing_sku": { + "name": "cloud_billing_sku", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rate_cents_per_unit": { + "name": "rate_cents_per_unit", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": true + }, + "accepts_new_usage": { + "name": "accepts_new_usage", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cloud_billing_sku_created_by_user_id_kilocode_users_id_fk": { + "name": "cloud_billing_sku_created_by_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_billing_sku", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_billing_sku_id_format": { + "name": "cloud_billing_sku_id_format", + "value": "\"cloud_billing_sku\".\"id\" ~ '^[a-z0-9][a-z0-9-]{2,79}$'" + }, + "cloud_billing_sku_name_nonempty": { + "name": "cloud_billing_sku_name_nonempty", + "value": "length(btrim(\"cloud_billing_sku\".\"name\")) > 0" + }, + "cloud_billing_sku_rate_positive": { + "name": "cloud_billing_sku_rate_positive", + "value": "\"cloud_billing_sku\".\"rate_cents_per_unit\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.code_indexing_manifest": { + "name": "code_indexing_manifest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total_lines": { + "name": "total_lines", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_ai_lines": { + "name": "total_ai_lines", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_code_indexing_manifest_organization_id": { + "name": "IDX_code_indexing_manifest_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_kilo_user_id": { + "name": "IDX_code_indexing_manifest_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_project_id": { + "name": "IDX_code_indexing_manifest_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_git_branch": { + "name": "IDX_code_indexing_manifest_git_branch", + "columns": [ + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_created_at": { + "name": "IDX_code_indexing_manifest_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_indexing_manifest_kilo_user_id_kilocode_users_id_fk": { + "name": "code_indexing_manifest_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "code_indexing_manifest", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_indexing_manifest_org_user_project_hash_branch": { + "name": "UQ_code_indexing_manifest_org_user_project_hash_branch", + "nullsNotDistinct": true, + "columns": [ + "organization_id", + "kilo_user_id", + "project_id", + "file_path", + "git_branch" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.code_indexing_search": { + "name": "code_indexing_search", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_code_indexing_search_organization_id": { + "name": "IDX_code_indexing_search_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_search_kilo_user_id": { + "name": "IDX_code_indexing_search_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_search_project_id": { + "name": "IDX_code_indexing_search_project_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_search_created_at": { + "name": "IDX_code_indexing_search_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_indexing_search_kilo_user_id_kilocode_users_id_fk": { + "name": "code_indexing_search_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "code_indexing_search", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.code_review_analytics_findings": { + "name": "code_review_analytics_findings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "analytics_result_id": { + "name": "analytics_result_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "security_class": { + "name": "security_class", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "code_review_analytics_findings_analytics_result_id_code_review_analytics_results_id_fk": { + "name": "code_review_analytics_findings_analytics_result_id_code_review_analytics_results_id_fk", + "tableFrom": "code_review_analytics_findings", + "tableTo": "code_review_analytics_results", + "columnsFrom": [ + "analytics_result_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_review_analytics_findings_result_ordinal": { + "name": "UQ_code_review_analytics_findings_result_ordinal", + "nullsNotDistinct": false, + "columns": [ + "analytics_result_id", + "ordinal" + ] + } + }, + "policies": {}, + "checkConstraints": { + "code_review_analytics_findings_severity_check": { + "name": "code_review_analytics_findings_severity_check", + "value": "\"code_review_analytics_findings\".\"severity\" IN ('critical', 'warning', 'suggestion')" + }, + "code_review_analytics_findings_category_check": { + "name": "code_review_analytics_findings_category_check", + "value": "\"code_review_analytics_findings\".\"category\" IN ('security', 'correctness', 'reliability', 'data_integrity', 'performance', 'compatibility', 'maintainability', 'test_quality', 'documentation', 'accessibility', 'other')" + }, + "code_review_analytics_findings_security_class_check": { + "name": "code_review_analytics_findings_security_class_check", + "value": "\"code_review_analytics_findings\".\"security_class\" IN ('auth_access', 'injection', 'data_protection', 'request_resource_boundary', 'deserialization_object_integrity', 'dependency_supply_chain', 'memory_safety', 'availability', 'concurrency', 'security_configuration', 'other')" + }, + "code_review_analytics_findings_ordinal_check": { + "name": "code_review_analytics_findings_ordinal_check", + "value": "\"code_review_analytics_findings\".\"ordinal\" >= 0" + }, + "code_review_analytics_findings_security_class_presence_check": { + "name": "code_review_analytics_findings_security_class_presence_check", + "value": "(\n (\"code_review_analytics_findings\".\"category\" = 'security' AND \"code_review_analytics_findings\".\"security_class\" IS NOT NULL) OR\n (\"code_review_analytics_findings\".\"category\" <> 'security' AND \"code_review_analytics_findings\".\"security_class\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_review_analytics_results": { + "name": "code_review_analytics_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code_review_id": { + "name": "code_review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_attempt_id": { + "name": "source_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "capture_status": { + "name": "capture_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "taxonomy_version": { + "name": "taxonomy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "change_type": { + "name": "change_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_level": { + "name": "impact_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "complexity_level": { + "name": "complexity_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "classification_confidence": { + "name": "classification_confidence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finalized_at": { + "name": "finalized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_code_review_analytics_results_source_attempt_id": { + "name": "idx_code_review_analytics_results_source_attempt_id", + "columns": [ + { + "expression": "source_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_analytics_results_finalized_at": { + "name": "idx_code_review_analytics_results_finalized_at", + "columns": [ + { + "expression": "finalized_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_review_analytics_results_code_review_id_cloud_agent_code_reviews_id_fk": { + "name": "code_review_analytics_results_code_review_id_cloud_agent_code_reviews_id_fk", + "tableFrom": "code_review_analytics_results", + "tableTo": "cloud_agent_code_reviews", + "columnsFrom": [ + "code_review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_review_analytics_results_source_attempt_id_cloud_agent_code_review_attempts_id_fk": { + "name": "code_review_analytics_results_source_attempt_id_cloud_agent_code_review_attempts_id_fk", + "tableFrom": "code_review_analytics_results", + "tableTo": "cloud_agent_code_review_attempts", + "columnsFrom": [ + "source_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_review_analytics_results_code_review_id": { + "name": "UQ_code_review_analytics_results_code_review_id", + "nullsNotDistinct": false, + "columns": [ + "code_review_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "code_review_analytics_results_capture_status_check": { + "name": "code_review_analytics_results_capture_status_check", + "value": "\"code_review_analytics_results\".\"capture_status\" IN ('captured', 'missing', 'invalid', 'omitted')" + }, + "code_review_analytics_results_change_type_check": { + "name": "code_review_analytics_results_change_type_check", + "value": "\"code_review_analytics_results\".\"change_type\" IN ('bug_fix', 'feature', 'refactor', 'maintenance', 'dependency', 'test', 'documentation', 'mixed', 'other')" + }, + "code_review_analytics_results_impact_level_check": { + "name": "code_review_analytics_results_impact_level_check", + "value": "\"code_review_analytics_results\".\"impact_level\" IN ('low', 'medium', 'high')" + }, + "code_review_analytics_results_complexity_level_check": { + "name": "code_review_analytics_results_complexity_level_check", + "value": "\"code_review_analytics_results\".\"complexity_level\" IN ('low', 'medium', 'high')" + }, + "code_review_analytics_results_classification_confidence_check": { + "name": "code_review_analytics_results_classification_confidence_check", + "value": "\"code_review_analytics_results\".\"classification_confidence\" IN ('low', 'medium', 'high')" + }, + "code_review_analytics_results_classification_presence_check": { + "name": "code_review_analytics_results_classification_presence_check", + "value": "(\n (\n \"code_review_analytics_results\".\"capture_status\" = 'captured'\n AND \"code_review_analytics_results\".\"change_type\" IS NOT NULL\n AND \"code_review_analytics_results\".\"impact_level\" IS NOT NULL\n AND \"code_review_analytics_results\".\"complexity_level\" IS NOT NULL\n AND \"code_review_analytics_results\".\"classification_confidence\" IS NOT NULL\n ) OR (\n \"code_review_analytics_results\".\"capture_status\" <> 'captured'\n AND \"code_review_analytics_results\".\"change_type\" IS NULL\n AND \"code_review_analytics_results\".\"impact_level\" IS NULL\n AND \"code_review_analytics_results\".\"complexity_level\" IS NULL\n AND \"code_review_analytics_results\".\"classification_confidence\" IS NULL\n )\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_review_feedback_events": { + "name": "code_review_feedback_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "kilo_comment_id": { + "name": "kilo_comment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_excerpt": { + "name": "reply_excerpt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_comment_excerpt": { + "name": "kilo_comment_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dedupe_hash": { + "name": "dedupe_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_code_review_feedback_events_owned_by_org_id": { + "name": "idx_code_review_feedback_events_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_feedback_events_owned_by_user_id": { + "name": "idx_code_review_feedback_events_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_feedback_events_platform_repo": { + "name": "idx_code_review_feedback_events_platform_repo", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_feedback_events_created_at": { + "name": "idx_code_review_feedback_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_review_feedback_events_owned_by_organization_id_organizations_id_fk": { + "name": "code_review_feedback_events_owned_by_organization_id_organizations_id_fk", + "tableFrom": "code_review_feedback_events", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_review_feedback_events_owned_by_user_id_kilocode_users_id_fk": { + "name": "code_review_feedback_events_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "code_review_feedback_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_review_feedback_events_dedupe_hash": { + "name": "UQ_code_review_feedback_events_dedupe_hash", + "nullsNotDistinct": false, + "columns": [ + "dedupe_hash" + ] + } + }, + "policies": {}, + "checkConstraints": { + "code_review_feedback_events_owner_check": { + "name": "code_review_feedback_events_owner_check", + "value": "(\n (\"code_review_feedback_events\".\"owned_by_user_id\" IS NOT NULL AND \"code_review_feedback_events\".\"owned_by_organization_id\" IS NULL) OR\n (\"code_review_feedback_events\".\"owned_by_user_id\" IS NULL AND \"code_review_feedback_events\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_review_memory_proposals": { + "name": "code_review_memory_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rationale": { + "name": "rationale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_markdown": { + "name": "proposed_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "positive_count": { + "name": "positive_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "negative_count": { + "name": "negative_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "neutral_count": { + "name": "neutral_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "change_request_url": { + "name": "change_request_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_code_review_memory_proposals_owned_by_org_id": { + "name": "idx_code_review_memory_proposals_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_memory_proposals_owned_by_user_id": { + "name": "idx_code_review_memory_proposals_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_memory_proposals_platform_repo_status": { + "name": "idx_code_review_memory_proposals_platform_repo_status", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_memory_proposals_updated_at": { + "name": "idx_code_review_memory_proposals_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_code_review_memory_proposals_org_active_scope": { + "name": "UQ_code_review_memory_proposals_org_active_scope", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"code_review_memory_proposals\".\"owned_by_organization_id\" IS NOT NULL AND \"code_review_memory_proposals\".\"status\" IN ('open', 'edited', 'opening_change_request')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_code_review_memory_proposals_user_active_scope": { + "name": "UQ_code_review_memory_proposals_user_active_scope", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"code_review_memory_proposals\".\"owned_by_user_id\" IS NOT NULL AND \"code_review_memory_proposals\".\"status\" IN ('open', 'edited', 'opening_change_request')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_review_memory_proposals_owned_by_organization_id_organizations_id_fk": { + "name": "code_review_memory_proposals_owned_by_organization_id_organizations_id_fk", + "tableFrom": "code_review_memory_proposals", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_review_memory_proposals_owned_by_user_id_kilocode_users_id_fk": { + "name": "code_review_memory_proposals_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "code_review_memory_proposals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "code_review_memory_proposals_owner_check": { + "name": "code_review_memory_proposals_owner_check", + "value": "(\n (\"code_review_memory_proposals\".\"owned_by_user_id\" IS NOT NULL AND \"code_review_memory_proposals\".\"owned_by_organization_id\" IS NULL) OR\n (\"code_review_memory_proposals\".\"owned_by_user_id\" IS NULL AND \"code_review_memory_proposals\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_availability_intents": { + "name": "coding_plan_availability_intents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_availability_intents_user_plan": { + "name": "UQ_coding_plan_availability_intents_user_plan", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_availability_intents_plan": { + "name": "IDX_coding_plan_availability_intents_plan", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_availability_intents_user_id_kilocode_users_id_fk": { + "name": "coding_plan_availability_intents_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_availability_intents", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coding_plan_key_inventory": { + "name": "coding_plan_key_inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upstream_plan_id": { + "name": "upstream_plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_fingerprint": { + "name": "credential_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "assigned_to_user_id": { + "name": "assigned_to_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_requested_at": { + "name": "revocation_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_attempt_count": { + "name": "revocation_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_revocation_error": { + "name": "last_revocation_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_key_inv_fingerprint": { + "name": "UQ_coding_plan_key_inv_fingerprint", + "columns": [ + { + "expression": "credential_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_key_inv_plan_status": { + "name": "IDX_coding_plan_key_inv_plan_status", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_key_inv_available": { + "name": "IDX_coding_plan_key_inv_available", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"coding_plan_key_inventory\".\"status\" = 'available'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_key_inventory_assigned_to_user_id_kilocode_users_id_fk": { + "name": "coding_plan_key_inventory_assigned_to_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_key_inventory", + "tableTo": "kilocode_users", + "columnsFrom": [ + "assigned_to_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_key_inventory_status_check": { + "name": "coding_plan_key_inventory_status_check", + "value": "\"coding_plan_key_inventory\".\"status\" IN ('available', 'assigned', 'revocation_pending', 'revoked', 'revocation_failed')" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_subscriptions": { + "name": "coding_plan_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_inventory_id": { + "name": "key_inventory_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "installed_byok_key_id": { + "name": "installed_byok_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "billing_period_days": { + "name": "billing_period_days", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "credit_renewal_at": { + "name": "credit_renewal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "past_due_started_at": { + "name": "past_due_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "payment_grace_expires_at": { + "name": "payment_grace_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_top_up_attempted_for_due": { + "name": "auto_top_up_attempted_for_due", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_reason": { + "name": "cancellation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_sub_live_user_plan": { + "name": "UQ_coding_plan_sub_live_user_plan", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_coding_plan_sub_live_user_provider": { + "name": "UQ_coding_plan_sub_live_user_provider", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_status": { + "name": "IDX_coding_plan_sub_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_renewal": { + "name": "IDX_coding_plan_sub_renewal", + "columns": [ + { + "expression": "credit_renewal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_inventory": { + "name": "IDX_coding_plan_sub_inventory", + "columns": [ + { + "expression": "key_inventory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_subscriptions_user_id_kilocode_users_id_fk": { + "name": "coding_plan_subscriptions_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_subscriptions_key_inventory_id_coding_plan_key_inventory_id_fk": { + "name": "coding_plan_subscriptions_key_inventory_id_coding_plan_key_inventory_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "coding_plan_key_inventory", + "columnsFrom": [ + "key_inventory_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "coding_plan_subscriptions_installed_byok_key_id_byok_api_keys_id_fk": { + "name": "coding_plan_subscriptions_installed_byok_key_id_byok_api_keys_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "byok_api_keys", + "columnsFrom": [ + "installed_byok_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_subscriptions_status_check": { + "name": "coding_plan_subscriptions_status_check", + "value": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due', 'canceled')" + }, + "coding_plan_subscriptions_live_access_check": { + "name": "coding_plan_subscriptions_live_access_check", + "value": "\"coding_plan_subscriptions\".\"status\" = 'canceled' OR \"coding_plan_subscriptions\".\"key_inventory_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_terms": { + "name": "coding_plan_terms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "credit_transaction_id": { + "name": "credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_terms_request": { + "name": "UQ_coding_plan_terms_request", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_terms_subscription": { + "name": "IDX_coding_plan_terms_subscription", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_terms_subscription_id_coding_plan_subscriptions_id_fk": { + "name": "coding_plan_terms_subscription_id_coding_plan_subscriptions_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "coding_plan_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_terms_user_id_kilocode_users_id_fk": { + "name": "coding_plan_terms_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_terms_credit_transaction_id_credit_transactions_id_fk": { + "name": "coding_plan_terms_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "credit_transactions", + "columnsFrom": [ + "credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_terms_kind_check": { + "name": "coding_plan_terms_kind_check", + "value": "\"coding_plan_terms\".\"kind\" IN ('activation', 'extension', 'renewal')" + } + }, + "isRLSEnabled": false + }, + "public.container_usage_interval": { + "name": "container_usage_interval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_epoch_ms": { + "name": "start_epoch_ms", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cloud_billing_sku_id": { + "name": "cloud_billing_sku_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_fingerprint": { + "name": "context_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_heartbeat_seq": { + "name": "last_heartbeat_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "confirmed_seconds": { + "name": "confirmed_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "final_stop_seq": { + "name": "final_stop_seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_container_usage_interval_sweep": { + "name": "IDX_container_usage_interval_sweep", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_container_usage_interval_subject_started": { + "name": "IDX_container_usage_interval_subject_started", + "columns": [ + { + "expression": "subject_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_container_usage_interval_single_open": { + "name": "UQ_container_usage_interval_single_open", + "columns": [ + { + "expression": "service", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"container_usage_interval\".\"status\" = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_usage_interval_cloud_billing_sku_id_cloud_billing_sku_id_fk": { + "name": "container_usage_interval_cloud_billing_sku_id_cloud_billing_sku_id_fk", + "tableFrom": "container_usage_interval", + "tableTo": "cloud_billing_sku", + "columnsFrom": [ + "cloud_billing_sku_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "container_usage_interval_subject_type": { + "name": "container_usage_interval_subject_type", + "value": "\"container_usage_interval\".\"subject_type\" IN ('user', 'org')" + }, + "container_usage_interval_actor_type": { + "name": "container_usage_interval_actor_type", + "value": "\"container_usage_interval\".\"actor_type\" IN ('user', 'bot')" + }, + "container_usage_interval_context_fingerprint": { + "name": "container_usage_interval_context_fingerprint", + "value": "\"container_usage_interval\".\"context_fingerprint\" ~ '^[a-f0-9]{64}$'" + }, + "container_usage_interval_attribution": { + "name": "container_usage_interval_attribution", + "value": "\"container_usage_interval\".\"actor_type\" = 'bot' OR (\"container_usage_interval\".\"actor_type\" = 'user' AND (\"container_usage_interval\".\"subject_type\" <> 'user' OR \"container_usage_interval\".\"actor_id\" = \"container_usage_interval\".\"subject_id\"))" + }, + "container_usage_interval_status": { + "name": "container_usage_interval_status", + "value": "\"container_usage_interval\".\"status\" IN ('open', 'closed')" + }, + "container_usage_interval_open_closed_shape": { + "name": "container_usage_interval_open_closed_shape", + "value": "(\"container_usage_interval\".\"status\" = 'open' AND \"container_usage_interval\".\"stopped_at\" IS NULL AND \"container_usage_interval\".\"close_reason\" IS NULL) OR (\"container_usage_interval\".\"status\" = 'closed' AND \"container_usage_interval\".\"stopped_at\" IS NOT NULL AND \"container_usage_interval\".\"close_reason\" IS NOT NULL)" + }, + "container_usage_interval_time_order": { + "name": "container_usage_interval_time_order", + "value": "\"container_usage_interval\".\"last_seen_at\" >= \"container_usage_interval\".\"started_at\" AND (\"container_usage_interval\".\"stopped_at\" IS NULL OR (\"container_usage_interval\".\"stopped_at\" >= \"container_usage_interval\".\"started_at\" AND \"container_usage_interval\".\"stopped_at\" <= \"container_usage_interval\".\"last_seen_at\"))" + }, + "container_usage_interval_last_heartbeat_seq_nonnegative": { + "name": "container_usage_interval_last_heartbeat_seq_nonnegative", + "value": "\"container_usage_interval\".\"last_heartbeat_seq\" >= 0" + }, + "container_usage_interval_confirmed_seconds_nonnegative": { + "name": "container_usage_interval_confirmed_seconds_nonnegative", + "value": "\"container_usage_interval\".\"confirmed_seconds\" >= 0" + }, + "container_usage_interval_final_stop_seq_positive": { + "name": "container_usage_interval_final_stop_seq_positive", + "value": "\"container_usage_interval\".\"final_stop_seq\" IS NULL OR \"container_usage_interval\".\"final_stop_seq\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.container_usage_segment": { + "name": "container_usage_segment", + "schema": "", + "columns": { + "interval_id": { + "name": "interval_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reported_seconds": { + "name": "reported_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "usage_seconds": { + "name": "usage_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_container_usage_segment_received": { + "name": "IDX_container_usage_segment_received", + "columns": [ + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_usage_segment_interval_id_container_usage_interval_id_fk": { + "name": "container_usage_segment_interval_id_container_usage_interval_id_fk", + "tableFrom": "container_usage_segment", + "tableTo": "container_usage_interval", + "columnsFrom": [ + "interval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "container_usage_segment_interval_id_seq_pk": { + "name": "container_usage_segment_interval_id_seq_pk", + "columns": [ + "interval_id", + "seq" + ] + } + }, + "uniqueConstraints": { + "container_usage_segment_idempotency_key_unique": { + "name": "container_usage_segment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "container_usage_segment_seq_positive": { + "name": "container_usage_segment_seq_positive", + "value": "\"container_usage_segment\".\"seq\" > 0" + }, + "container_usage_segment_reported_seconds_nonnegative": { + "name": "container_usage_segment_reported_seconds_nonnegative", + "value": "\"container_usage_segment\".\"reported_seconds\" >= 0" + }, + "container_usage_segment_usage_seconds_nonnegative": { + "name": "container_usage_segment_usage_seconds_nonnegative", + "value": "\"container_usage_segment\".\"usage_seconds\" >= 0" + }, + "container_usage_segment_usage_within_reported": { + "name": "container_usage_segment_usage_within_reported", + "value": "\"container_usage_segment\".\"usage_seconds\" <= \"container_usage_segment\".\"reported_seconds\"" + } + }, + "isRLSEnabled": false + }, + "public.contributor_champion_contributors": { + "name": "contributor_champion_contributors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "github_login": { + "name": "github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_profile_url": { + "name": "github_profile_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_user_id": { + "name": "github_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "first_contribution_at": { + "name": "first_contribution_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_contribution_at": { + "name": "last_contribution_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "all_time_contributions": { + "name": "all_time_contributions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "manual_email": { + "name": "manual_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_contributor_champion_contributors_last_contribution_at": { + "name": "IDX_contributor_champion_contributors_last_contribution_at", + "columns": [ + { + "expression": "last_contribution_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_contributors_manual_email": { + "name": "IDX_contributor_champion_contributors_manual_email", + "columns": [ + { + "expression": "manual_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_contributor_champion_contributors_github_login": { + "name": "UQ_contributor_champion_contributors_github_login", + "nullsNotDistinct": false, + "columns": [ + "github_login" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contributor_champion_events": { + "name": "contributor_champion_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "contributor_id": { + "name": "contributor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_pr_number": { + "name": "github_pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "github_pr_url": { + "name": "github_pr_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_pr_title": { + "name": "github_pr_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_author_login": { + "name": "github_author_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_author_email": { + "name": "github_author_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merged_at": { + "name": "merged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_contributor_champion_events_contributor_id": { + "name": "IDX_contributor_champion_events_contributor_id", + "columns": [ + { + "expression": "contributor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_events_merged_at": { + "name": "IDX_contributor_champion_events_merged_at", + "columns": [ + { + "expression": "merged_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_events_author_email": { + "name": "IDX_contributor_champion_events_author_email", + "columns": [ + { + "expression": "github_author_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contributor_champion_events_contributor_id_contributor_champion_contributors_id_fk": { + "name": "contributor_champion_events_contributor_id_contributor_champion_contributors_id_fk", + "tableFrom": "contributor_champion_events", + "tableTo": "contributor_champion_contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_contributor_champion_events_repo_pr": { + "name": "UQ_contributor_champion_events_repo_pr", + "nullsNotDistinct": false, + "columns": [ + "repo_full_name", + "github_pr_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contributor_champion_memberships": { + "name": "contributor_champion_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "contributor_id": { + "name": "contributor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "selected_tier": { + "name": "selected_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrolled_tier": { + "name": "enrolled_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credit_amount_microdollars": { + "name": "credit_amount_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "credits_last_granted_at": { + "name": "credits_last_granted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "linked_kilo_user_id": { + "name": "linked_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_contributor_champion_memberships_credits_due": { + "name": "IDX_contributor_champion_memberships_credits_due", + "columns": [ + { + "expression": "credits_last_granted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"contributor_champion_memberships\".\"enrolled_tier\" IS NOT NULL AND \"contributor_champion_memberships\".\"credit_amount_microdollars\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_memberships_linked_kilo_user_id": { + "name": "IDX_contributor_champion_memberships_linked_kilo_user_id", + "columns": [ + { + "expression": "linked_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contributor_champion_memberships_contributor_id_contributor_champion_contributors_id_fk": { + "name": "contributor_champion_memberships_contributor_id_contributor_champion_contributors_id_fk", + "tableFrom": "contributor_champion_memberships", + "tableTo": "contributor_champion_contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "contributor_champion_memberships_linked_kilo_user_id_kilocode_users_id_fk": { + "name": "contributor_champion_memberships_linked_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "contributor_champion_memberships", + "tableTo": "kilocode_users", + "columnsFrom": [ + "linked_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_contributor_champion_memberships_contributor_id": { + "name": "UQ_contributor_champion_memberships_contributor_id", + "nullsNotDistinct": false, + "columns": [ + "contributor_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "contributor_champion_memberships_selected_tier_check": { + "name": "contributor_champion_memberships_selected_tier_check", + "value": "\"contributor_champion_memberships\".\"selected_tier\" IS NULL OR \"contributor_champion_memberships\".\"selected_tier\" IN ('contributor', 'ambassador', 'champion')" + }, + "contributor_champion_memberships_enrolled_tier_check": { + "name": "contributor_champion_memberships_enrolled_tier_check", + "value": "\"contributor_champion_memberships\".\"enrolled_tier\" IS NULL OR \"contributor_champion_memberships\".\"enrolled_tier\" IN ('contributor', 'ambassador', 'champion')" + } + }, + "isRLSEnabled": false + }, + "public.contributor_champion_sync_state": { + "name": "contributor_champion_sync_state", + "schema": "", + "columns": { + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_merged_at": { + "name": "last_merged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credit_campaigns": { + "name": "credit_campaigns", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credit_category": { + "name": "credit_category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_microdollars": { + "name": "amount_microdollars", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credit_expiry_hours": { + "name": "credit_expiry_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_ends_at": { + "name": "campaign_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_redemptions_allowed": { + "name": "total_redemptions_allowed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_credit_campaigns_slug": { + "name": "UQ_credit_campaigns_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_credit_campaigns_credit_category": { + "name": "UQ_credit_campaigns_credit_category", + "columns": [ + { + "expression": "credit_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credit_campaigns_slug_format_check": { + "name": "credit_campaigns_slug_format_check", + "value": "\"credit_campaigns\".\"slug\" ~ '^[a-z0-9-]{5,40}$'" + }, + "credit_campaigns_amount_positive_check": { + "name": "credit_campaigns_amount_positive_check", + "value": "\"credit_campaigns\".\"amount_microdollars\" > 0" + }, + "credit_campaigns_credit_expiry_hours_positive_check": { + "name": "credit_campaigns_credit_expiry_hours_positive_check", + "value": "\"credit_campaigns\".\"credit_expiry_hours\" IS NULL OR \"credit_campaigns\".\"credit_expiry_hours\" > 0" + }, + "credit_campaigns_total_redemptions_allowed_positive_check": { + "name": "credit_campaigns_total_redemptions_allowed_positive_check", + "value": "\"credit_campaigns\".\"total_redemptions_allowed\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.credit_transactions": { + "name": "credit_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_microdollars": { + "name": "amount_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expiration_baseline_microdollars_used": { + "name": "expiration_baseline_microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "original_baseline_microdollars_used": { + "name": "original_baseline_microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_transaction_id": { + "name": "original_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_id": { + "name": "stripe_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coinbase_credit_block_id": { + "name": "coinbase_credit_block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credit_category": { + "name": "credit_category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiry_date": { + "name": "expiry_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "check_category_uniqueness": { + "name": "check_category_uniqueness", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "IDX_credit_transactions_created_at": { + "name": "IDX_credit_transactions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_is_free": { + "name": "IDX_credit_transactions_is_free", + "columns": [ + { + "expression": "is_free", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_kilo_user_id": { + "name": "IDX_credit_transactions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_credit_category": { + "name": "IDX_credit_transactions_credit_category", + "columns": [ + { + "expression": "credit_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_stripe_payment_id": { + "name": "IDX_credit_transactions_stripe_payment_id", + "columns": [ + { + "expression": "stripe_payment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_original_transaction_id": { + "name": "IDX_credit_transactions_original_transaction_id", + "columns": [ + { + "expression": "original_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_coinbase_credit_block_id": { + "name": "IDX_credit_transactions_coinbase_credit_block_id", + "columns": [ + { + "expression": "coinbase_credit_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_organization_id": { + "name": "IDX_credit_transactions_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_unique_category": { + "name": "IDX_credit_transactions_unique_category", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credit_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credit_transactions\".\"check_category_uniqueness\" = TRUE", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credit_transactions_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "credit_transactions_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "credit_transactions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_llm2": { + "name": "custom_llm2", + "schema": "", + "columns": { + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "definition": { + "name": "definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deleted_user_email_tombstones": { + "name": "deleted_user_email_tombstones", + "schema": "", + "columns": { + "normalized_email_hash": { + "name": "normalized_email_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_builds": { + "name": "deployment_builds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployment_builds_deployment_id": { + "name": "idx_deployment_builds_deployment_id", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_builds_status": { + "name": "idx_deployment_builds_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_builds_deployment_id_deployments_id_fk": { + "name": "deployment_builds_deployment_id_deployments_id_fk", + "tableFrom": "deployment_builds", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_env_vars": { + "name": "deployment_env_vars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployment_env_vars_deployment_id": { + "name": "idx_deployment_env_vars_deployment_id", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_env_vars_deployment_id_deployments_id_fk": { + "name": "deployment_env_vars_deployment_id_deployments_id_fk", + "tableFrom": "deployment_env_vars", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_deployment_env_vars_deployment_key": { + "name": "UQ_deployment_env_vars_deployment_key", + "nullsNotDistinct": false, + "columns": [ + "deployment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_events": { + "name": "deployment_events", + "schema": "", + "columns": { + "build_id": { + "name": "build_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'log'" + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_deployment_events_build_id": { + "name": "idx_deployment_events_build_id", + "columns": [ + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_events_timestamp": { + "name": "idx_deployment_events_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_events_type": { + "name": "idx_deployment_events_type", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_events_build_id_deployment_builds_id_fk": { + "name": "deployment_events_build_id_deployment_builds_id_fk", + "tableFrom": "deployment_events", + "tableTo": "deployment_builds", + "columnsFrom": [ + "build_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "deployment_events_build_id_event_id_pk": { + "name": "deployment_events_build_id_event_id_pk", + "columns": [ + "build_id", + "event_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_threat_detections": { + "name": "deployment_threat_detections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deployment_id": { + "name": "deployment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "build_id": { + "name": "build_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "threat_type": { + "name": "threat_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployment_threat_detections_deployment_id": { + "name": "idx_deployment_threat_detections_deployment_id", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_threat_detections_created_at": { + "name": "idx_deployment_threat_detections_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_threat_detections_deployment_id_deployments_id_fk": { + "name": "deployment_threat_detections_deployment_id_deployments_id_fk", + "tableFrom": "deployment_threat_detections", + "tableTo": "deployments", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_threat_detections_build_id_deployment_builds_id_fk": { + "name": "deployment_threat_detections_build_id_deployment_builds_id_fk", + "tableFrom": "deployment_threat_detections", + "tableTo": "deployment_builds", + "columnsFrom": [ + "build_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployments": { + "name": "deployments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "deployment_slug": { + "name": "deployment_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_worker_name": { + "name": "internal_worker_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_source": { + "name": "repository_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_url": { + "name": "deployment_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "git_auth_token": { + "name": "git_auth_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_deployed_at": { + "name": "last_deployed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_build_id": { + "name": "last_build_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "threat_status": { + "name": "threat_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_from": { + "name": "created_from", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_deployments_owned_by_user_id": { + "name": "idx_deployments_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_owned_by_organization_id": { + "name": "idx_deployments_owned_by_organization_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_platform_integration_id": { + "name": "idx_deployments_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_repository_source_branch": { + "name": "idx_deployments_repository_source_branch", + "columns": [ + { + "expression": "repository_source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_threat_status_pending": { + "name": "idx_deployments_threat_status_pending", + "columns": [ + { + "expression": "threat_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"deployments\".\"threat_status\" = 'pending_scan'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployments_owned_by_user_id_kilocode_users_id_fk": { + "name": "deployments_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "deployments", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "deployments_owned_by_organization_id_organizations_id_fk": { + "name": "deployments_owned_by_organization_id_organizations_id_fk", + "tableFrom": "deployments", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_deployments_deployment_slug": { + "name": "UQ_deployments_deployment_slug", + "nullsNotDistinct": false, + "columns": [ + "deployment_slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "deployments_owner_check": { + "name": "deployments_owner_check", + "value": "(\n (\"deployments\".\"owned_by_user_id\" IS NOT NULL AND \"deployments\".\"owned_by_organization_id\" IS NULL) OR\n (\"deployments\".\"owned_by_user_id\" IS NULL AND \"deployments\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "deployments_source_type_check": { + "name": "deployments_source_type_check", + "value": "\"deployments\".\"source_type\" IN ('github', 'git', 'app-builder')" + } + }, + "isRLSEnabled": false + }, + "public.deployments_ephemeral": { + "name": "deployments_ephemeral", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_worker_name": { + "name": "internal_worker_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_slug": { + "name": "deployment_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_cleanup_at": { + "name": "next_cleanup_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cleanup_claim_token": { + "name": "cleanup_claim_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cleanup_claimed_until": { + "name": "cleanup_claimed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployments_ephemeral_owned_by_user_id": { + "name": "idx_deployments_ephemeral_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_ephemeral_next_cleanup_at": { + "name": "idx_deployments_ephemeral_next_cleanup_at", + "columns": [ + { + "expression": "next_cleanup_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployments_ephemeral_owned_by_user_id_kilocode_users_id_fk": { + "name": "deployments_ephemeral_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "deployments_ephemeral", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_deployments_ephemeral_internal_worker_name": { + "name": "UQ_deployments_ephemeral_internal_worker_name", + "nullsNotDistinct": false, + "columns": [ + "internal_worker_name" + ] + }, + "UQ_deployments_ephemeral_deployment_slug": { + "name": "UQ_deployments_ephemeral_deployment_slug", + "nullsNotDistinct": false, + "columns": [ + "deployment_slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "deployments_ephemeral_source_type_check": { + "name": "deployments_ephemeral_source_type_check", + "value": "\"deployments_ephemeral\".\"source_type\" IN ('html')" + }, + "deployments_ephemeral_status_check": { + "name": "deployments_ephemeral_status_check", + "value": "\"deployments_ephemeral\".\"status\" IN ('pending', 'active', 'cleanup_retry')" + }, + "deployments_ephemeral_claim_fields_check": { + "name": "deployments_ephemeral_claim_fields_check", + "value": "(\"deployments_ephemeral\".\"cleanup_claim_token\" IS NULL) = (\"deployments_ephemeral\".\"cleanup_claimed_until\" IS NULL)" + }, + "deployments_ephemeral_active_fields_check": { + "name": "deployments_ephemeral_active_fields_check", + "value": "\"deployments_ephemeral\".\"status\" <> 'active' OR (\"deployments_ephemeral\".\"deployment_slug\" IS NOT NULL AND \"deployments_ephemeral\".\"expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.device_auth_requests": { + "name": "device_auth_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "device_code_hash": { + "name": "device_code_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_device_auth_requests_code": { + "name": "UQ_device_auth_requests_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_status": { + "name": "IDX_device_auth_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_expires_at": { + "name": "IDX_device_auth_requests_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_kilo_user_id": { + "name": "IDX_device_auth_requests_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_device_auth_requests_device_code_hash": { + "name": "UQ_device_auth_requests_device_code_hash", + "columns": [ + { + "expression": "device_code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"device_auth_requests\".\"device_code_hash\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_user_code": { + "name": "IDX_device_auth_requests_user_code", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"device_auth_requests\".\"user_code\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_auth_requests_kilo_user_id_kilocode_users_id_fk": { + "name": "device_auth_requests_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "device_auth_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_refresh_tokens": { + "name": "device_refresh_tokens", + "schema": "", + "columns": { + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_session_id": { + "name": "device_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_device_refresh_tokens_device_session_id": { + "name": "IDX_device_refresh_tokens_device_session_id", + "columns": [ + { + "expression": "device_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_refresh_tokens_expires_at": { + "name": "IDX_device_refresh_tokens_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_refresh_tokens_device_session_id_device_sessions_id_fk": { + "name": "device_refresh_tokens_device_session_id_device_sessions_id_fk", + "tableFrom": "device_refresh_tokens", + "tableTo": "device_sessions", + "columnsFrom": [ + "device_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_sessions": { + "name": "device_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_auth_request_id": { + "name": "device_auth_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_reason": { + "name": "revoked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_device_sessions_kilo_user_id": { + "name": "IDX_device_sessions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_sessions_revoked_at": { + "name": "IDX_device_sessions_revoked_at", + "columns": [ + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "device_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "device_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord_gateway_listener": { + "name": "discord_gateway_listener", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "default": 1 + }, + "listener_id": { + "name": "listener_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.editor_name": { + "name": "editor_name", + "schema": "", + "columns": { + "editor_name_id": { + "name": "editor_name_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "editor_name": { + "name": "editor_name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_editor_name": { + "name": "UQ_editor_name", + "columns": [ + { + "expression": "editor_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.enrichment_data": { + "name": "enrichment_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_enrichment_data": { + "name": "github_enrichment_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "linkedin_enrichment_data": { + "name": "linkedin_enrichment_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "clay_enrichment_data": { + "name": "clay_enrichment_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_enrichment_data_user_id": { + "name": "IDX_enrichment_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "enrichment_data_user_id_kilocode_users_id_fk": { + "name": "enrichment_data_user_id_kilocode_users_id_fk", + "tableFrom": "enrichment_data", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_enrichment_data_user_id": { + "name": "UQ_enrichment_data_user_id", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exa_monthly_usage": { + "name": "exa_monthly_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "month": { + "name": "month", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_charged_microdollars": { + "name": "total_charged_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "free_allowance_microdollars": { + "name": "free_allowance_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 10000000 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_exa_monthly_usage_personal": { + "name": "idx_exa_monthly_usage_personal", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"exa_monthly_usage\".\"organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_exa_monthly_usage_org": { + "name": "idx_exa_monthly_usage_org", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"exa_monthly_usage\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exa_usage_log": { + "name": "exa_usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "charged_to_balance": { + "name": "charged_to_balance", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_exa_usage_log_user_created": { + "name": "idx_exa_usage_log_user_created", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "exa_usage_log_id_created_at_pk": { + "name": "exa_usage_log_id_created_at_pk", + "columns": [ + "id", + "created_at" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feature": { + "name": "feature", + "schema": "", + "columns": { + "feature_id": { + "name": "feature_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_feature": { + "name": "UQ_feature", + "columns": [ + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finish_reason": { + "name": "finish_reason", + "schema": "", + "columns": { + "finish_reason_id": { + "name": "finish_reason_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "finish_reason": { + "name": "finish_reason", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_finish_reason": { + "name": "UQ_finish_reason", + "columns": [ + { + "expression": "finish_reason", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_model_usage": { + "name": "free_model_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_free_model_usage_ip_created_at": { + "name": "idx_free_model_usage_ip_created_at", + "columns": [ + { + "expression": "ip_address", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_free_model_usage_created_at": { + "name": "idx_free_model_usage_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_branch_pull_requests": { + "name": "github_branch_pull_requests", + "schema": "", + "columns": { + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_state": { + "name": "pr_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_title": { + "name": "pr_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_head_sha": { + "name": "pr_head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_review_decision": { + "name": "pr_review_decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_decision_pending": { + "name": "review_decision_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "review_decision_fetching_at": { + "name": "review_decision_fetching_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pr_last_synced_at": { + "name": "pr_last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_github_branch_prs_org": { + "name": "UQ_github_branch_prs_org", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_branch_pull_requests\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_github_branch_prs_user": { + "name": "UQ_github_branch_prs_user", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_branch_pull_requests\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_branch_pull_requests_owned_by_organization_id_organizations_id_fk": { + "name": "github_branch_pull_requests_owned_by_organization_id_organizations_id_fk", + "tableFrom": "github_branch_pull_requests", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_branch_pull_requests_owned_by_user_id_kilocode_users_id_fk": { + "name": "github_branch_pull_requests_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "github_branch_pull_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_branch_pull_requests_owner_check": { + "name": "github_branch_pull_requests_owner_check", + "value": "(\n (\"github_branch_pull_requests\".\"owned_by_organization_id\" IS NOT NULL AND \"github_branch_pull_requests\".\"owned_by_user_id\" IS NULL) OR\n (\"github_branch_pull_requests\".\"owned_by_organization_id\" IS NULL AND \"github_branch_pull_requests\".\"owned_by_user_id\" IS NOT NULL)\n )" + }, + "github_branch_pull_requests_review_decision_check": { + "name": "github_branch_pull_requests_review_decision_check", + "value": "\"github_branch_pull_requests\".\"pr_review_decision\" IS NULL OR \"github_branch_pull_requests\".\"pr_review_decision\" IN ('approved', 'changes_requested', 'review_required')" + } + }, + "isRLSEnabled": false + }, + "public.github_install_states": { + "name": "github_install_states", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "return_to": { + "name": "return_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_github_install_states_expires_at": { + "name": "IDX_github_install_states_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_install_states_kilo_user_id_kilocode_users_id_fk": { + "name": "github_install_states_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "github_install_states", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_install_states_owner_type_check": { + "name": "github_install_states_owner_type_check", + "value": "\"github_install_states\".\"owner_type\" IN ('org', 'user')" + } + }, + "isRLSEnabled": false + }, + "public.http_ip": { + "name": "http_ip", + "schema": "", + "columns": { + "http_ip_id": { + "name": "http_ip_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "http_ip": { + "name": "http_ip", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_http_ip": { + "name": "UQ_http_ip", + "columns": [ + { + "expression": "http_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.http_user_agent": { + "name": "http_user_agent", + "schema": "", + "columns": { + "http_user_agent_id": { + "name": "http_user_agent_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_http_user_agent": { + "name": "UQ_http_user_agent", + "columns": [ + { + "expression": "http_user_agent", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.impact_advocate_participants": { + "name": "impact_advocate_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "program_key": { + "name": "program_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "advocate_id": { + "name": "advocate_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "advocate_account_id": { + "name": "advocate_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opaque_referral_identifier": { + "name": "opaque_referral_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_email": { + "name": "contact_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_state": { + "name": "registration_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_registration_attempt_at": { + "name": "last_registration_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_impact_advocate_participants_program_referral_identifier": { + "name": "UQ_impact_advocate_participants_program_referral_identifier", + "columns": [ + { + "expression": "program_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opaque_referral_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"impact_advocate_participants\".\"opaque_referral_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_advocate_participants_registration_state": { + "name": "IDX_impact_advocate_participants_registration_state", + "columns": [ + { + "expression": "registration_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_advocate_participants_user_id_kilocode_users_id_fk": { + "name": "impact_advocate_participants_user_id_kilocode_users_id_fk", + "tableFrom": "impact_advocate_participants", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_advocate_participants_program_user": { + "name": "UQ_impact_advocate_participants_program_user", + "nullsNotDistinct": false, + "columns": [ + "program_key", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_advocate_participants_program_key_check": { + "name": "impact_advocate_participants_program_key_check", + "value": "\"impact_advocate_participants\".\"program_key\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_advocate_participants_registration_state_check": { + "name": "impact_advocate_participants_registration_state_check", + "value": "\"impact_advocate_participants\".\"registration_state\" IN ('pending', 'retrying', 'registered', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.impact_advocate_registration_attempts": { + "name": "impact_advocate_registration_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "program_key": { + "name": "program_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opaque_cookie_value": { + "name": "opaque_cookie_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cookie_value_length": { + "name": "cookie_value_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "delivery_state": { + "name": "delivery_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_payload": { + "name": "response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_status_code": { + "name": "response_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_advocate_registration_attempts_participant_id": { + "name": "IDX_impact_advocate_registration_attempts_participant_id", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_advocate_registration_attempts_delivery_state": { + "name": "IDX_impact_advocate_registration_attempts_delivery_state", + "columns": [ + { + "expression": "delivery_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_advocate_registration_attempts_participant_id_impact_advocate_participants_id_fk": { + "name": "impact_advocate_registration_attempts_participant_id_impact_advocate_participants_id_fk", + "tableFrom": "impact_advocate_registration_attempts", + "tableTo": "impact_advocate_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_advocate_registration_attempts_dedupe_key": { + "name": "UQ_impact_advocate_registration_attempts_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_advocate_registration_attempts_program_key_check": { + "name": "impact_advocate_registration_attempts_program_key_check", + "value": "\"impact_advocate_registration_attempts\".\"program_key\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_advocate_registration_attempts_delivery_state_check": { + "name": "impact_advocate_registration_attempts_delivery_state_check", + "value": "\"impact_advocate_registration_attempts\".\"delivery_state\" IN ('queued', 'sending', 'succeeded', 'failed')" + }, + "impact_advocate_registration_attempts_cookie_value_length_non_negative_check": { + "name": "impact_advocate_registration_attempts_cookie_value_length_non_negative_check", + "value": "\"impact_advocate_registration_attempts\".\"cookie_value_length\" >= 0" + }, + "impact_advocate_registration_attempts_attempt_count_non_negative_check": { + "name": "impact_advocate_registration_attempts_attempt_count_non_negative_check", + "value": "\"impact_advocate_registration_attempts\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_advocate_reward_redemptions": { + "name": "impact_advocate_reward_redemptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "reward_id": { + "name": "reward_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "impact_reward_id": { + "name": "impact_reward_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lookup_response_payload": { + "name": "lookup_response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "redeem_response_payload": { + "name": "redeem_response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_status_code": { + "name": "response_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_advocate_reward_redemptions_beneficiary_user_id": { + "name": "IDX_impact_advocate_reward_redemptions_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_advocate_reward_redemptions_state": { + "name": "IDX_impact_advocate_reward_redemptions_state", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_advocate_reward_redemptions_reward_id_impact_referral_rewards_id_fk": { + "name": "impact_advocate_reward_redemptions_reward_id_impact_referral_rewards_id_fk", + "tableFrom": "impact_advocate_reward_redemptions", + "tableTo": "impact_referral_rewards", + "columnsFrom": [ + "reward_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_advocate_reward_redemptions_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_advocate_reward_redemptions_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_advocate_reward_redemptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_advocate_reward_redemptions_reward_id": { + "name": "UQ_impact_advocate_reward_redemptions_reward_id", + "nullsNotDistinct": false, + "columns": [ + "reward_id" + ] + }, + "UQ_impact_advocate_reward_redemptions_dedupe_key": { + "name": "UQ_impact_advocate_reward_redemptions_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_advocate_reward_redemptions_state_check": { + "name": "impact_advocate_reward_redemptions_state_check", + "value": "\"impact_advocate_reward_redemptions\".\"state\" IN ('queued', 'retrying', 'redeemed', 'failed')" + }, + "impact_advocate_reward_redemptions_attempt_count_non_negative_check": { + "name": "impact_advocate_reward_redemptions_attempt_count_non_negative_check", + "value": "\"impact_advocate_reward_redemptions\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_attribution_touches": { + "name": "impact_attribution_touches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "program_key": { + "name": "program_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'kiloclaw'" + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anonymous_id": { + "name": "anonymous_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "touch_type": { + "name": "touch_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opaque_tracking_value": { + "name": "opaque_tracking_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tracking_value_length": { + "name": "tracking_value_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_tracking_value_accepted": { + "name": "is_tracking_value_accepted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "rs_code": { + "name": "rs_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rs_share_medium": { + "name": "rs_share_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rs_engagement_medium": { + "name": "rs_engagement_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "im_ref": { + "name": "im_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "landing_path": { + "name": "landing_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_term": { + "name": "utm_term", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "touched_at": { + "name": "touched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "sale_attributed_at": { + "name": "sale_attributed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_attribution_touches_product_user_id": { + "name": "IDX_impact_attribution_touches_product_user_id", + "columns": [ + { + "expression": "product", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_user_id": { + "name": "IDX_impact_attribution_touches_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_anonymous_id": { + "name": "IDX_impact_attribution_touches_anonymous_id", + "columns": [ + { + "expression": "anonymous_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_expires_at": { + "name": "IDX_impact_attribution_touches_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_sale_attributed_at": { + "name": "IDX_impact_attribution_touches_sale_attributed_at", + "columns": [ + { + "expression": "sale_attributed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_attribution_touches_user_id_kilocode_users_id_fk": { + "name": "impact_attribution_touches_user_id_kilocode_users_id_fk", + "tableFrom": "impact_attribution_touches", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_attribution_touches_dedupe_key": { + "name": "UQ_impact_attribution_touches_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_attribution_touches_product_check": { + "name": "impact_attribution_touches_product_check", + "value": "\"impact_attribution_touches\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_attribution_touches_program_key_check": { + "name": "impact_attribution_touches_program_key_check", + "value": "\"impact_attribution_touches\".\"program_key\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_attribution_touches_touch_type_check": { + "name": "impact_attribution_touches_touch_type_check", + "value": "\"impact_attribution_touches\".\"touch_type\" IN ('affiliate', 'referral')" + }, + "impact_attribution_touches_provider_check": { + "name": "impact_attribution_touches_provider_check", + "value": "\"impact_attribution_touches\".\"provider\" IN ('impact_performance', 'impact_advocate')" + }, + "impact_attribution_touches_tracking_value_length_non_negative_check": { + "name": "impact_attribution_touches_tracking_value_length_non_negative_check", + "value": "\"impact_attribution_touches\".\"tracking_value_length\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_conversion_reports": { + "name": "impact_conversion_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "conversion_id": { + "name": "conversion_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action_tracker_id": { + "name": "action_tracker_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "order_id": { + "name": "order_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_payload": { + "name": "response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_status_code": { + "name": "response_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_conversion_reports_conversion_id": { + "name": "IDX_impact_conversion_reports_conversion_id", + "columns": [ + { + "expression": "conversion_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_conversion_reports_state": { + "name": "IDX_impact_conversion_reports_state", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_conversion_reports_conversion_id_impact_referral_conversions_id_fk": { + "name": "impact_conversion_reports_conversion_id_impact_referral_conversions_id_fk", + "tableFrom": "impact_conversion_reports", + "tableTo": "impact_referral_conversions", + "columnsFrom": [ + "conversion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_conversion_reports_dedupe_key": { + "name": "UQ_impact_conversion_reports_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_conversion_reports_state_check": { + "name": "impact_conversion_reports_state_check", + "value": "\"impact_conversion_reports\".\"state\" IN ('queued', 'retrying', 'delivered', 'failed')" + }, + "impact_conversion_reports_attempt_count_non_negative_check": { + "name": "impact_conversion_reports_attempt_count_non_negative_check", + "value": "\"impact_conversion_reports\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_conversions": { + "name": "impact_referral_conversions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "referee_user_id": { + "name": "referee_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer_user_id": { + "name": "referrer_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_touch_id": { + "name": "source_touch_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "winning_touch_type": { + "name": "winning_touch_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credits'" + }, + "source_payment_id": { + "name": "source_payment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "qualified": { + "name": "qualified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disqualification_reason": { + "name": "disqualification_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "converted_at": { + "name": "converted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_conversions_referee_user_id": { + "name": "IDX_impact_referral_conversions_referee_user_id", + "columns": [ + { + "expression": "referee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referral_conversions_referrer_user_id": { + "name": "IDX_impact_referral_conversions_referrer_user_id", + "columns": [ + { + "expression": "referrer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_conversions_referee_user_id_kilocode_users_id_fk": { + "name": "impact_referral_conversions_referee_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_conversions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referee_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_conversions_referrer_user_id_kilocode_users_id_fk": { + "name": "impact_referral_conversions_referrer_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_conversions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referrer_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "impact_referral_conversions_source_touch_id_impact_attribution_touches_id_fk": { + "name": "impact_referral_conversions_source_touch_id_impact_attribution_touches_id_fk", + "tableFrom": "impact_referral_conversions", + "tableTo": "impact_attribution_touches", + "columnsFrom": [ + "source_touch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referral_conversions_product_payment_source": { + "name": "UQ_impact_referral_conversions_product_payment_source", + "nullsNotDistinct": false, + "columns": [ + "product", + "payment_provider", + "source_payment_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referral_conversions_product_check": { + "name": "impact_referral_conversions_product_check", + "value": "\"impact_referral_conversions\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_referral_conversions_winning_touch_type_check": { + "name": "impact_referral_conversions_winning_touch_type_check", + "value": "\"impact_referral_conversions\".\"winning_touch_type\" IN ('referral', 'affiliate', 'none')" + }, + "impact_referral_conversions_payment_provider_check": { + "name": "impact_referral_conversions_payment_provider_check", + "value": "\"impact_referral_conversions\".\"payment_provider\" IN ('stripe', 'credits', 'app_store', 'google_play')" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_reward_applications": { + "name": "impact_referral_reward_applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "reward_id": { + "name": "reward_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "previous_renewal_boundary": { + "name": "previous_renewal_boundary", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "new_renewal_boundary": { + "name": "new_renewal_boundary", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "local_operation_id": { + "name": "local_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_operation_id": { + "name": "stripe_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_idempotency_key": { + "name": "stripe_idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_reward_applications_reward_id": { + "name": "IDX_impact_referral_reward_applications_reward_id", + "columns": [ + { + "expression": "reward_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referral_reward_applications_beneficiary_user_id": { + "name": "IDX_impact_referral_reward_applications_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_reward_applications_reward_id_impact_referral_rewards_id_fk": { + "name": "impact_referral_reward_applications_reward_id_impact_referral_rewards_id_fk", + "tableFrom": "impact_referral_reward_applications", + "tableTo": "impact_referral_rewards", + "columnsFrom": [ + "reward_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_reward_applications_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_referral_reward_applications_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_reward_applications", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "impact_referral_reward_applications_product_check": { + "name": "impact_referral_reward_applications_product_check", + "value": "\"impact_referral_reward_applications\".\"product\" IN ('kiloclaw', 'kilo_pass')" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_reward_decisions": { + "name": "impact_referral_reward_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "conversion_id": { + "name": "conversion_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beneficiary_role": { + "name": "beneficiary_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_kind": { + "name": "reward_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw_free_month'" + }, + "months_granted": { + "name": "months_granted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reward_percent": { + "name": "reward_percent", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "source_tier": { + "name": "source_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_amount_usd": { + "name": "reward_amount_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_reward_decisions_beneficiary_user_id": { + "name": "IDX_impact_referral_reward_decisions_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_reward_decisions_conversion_id_impact_referral_conversions_id_fk": { + "name": "impact_referral_reward_decisions_conversion_id_impact_referral_conversions_id_fk", + "tableFrom": "impact_referral_reward_decisions", + "tableTo": "impact_referral_conversions", + "columnsFrom": [ + "conversion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_reward_decisions_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_referral_reward_decisions_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_reward_decisions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referral_reward_decisions_conversion_role": { + "name": "UQ_impact_referral_reward_decisions_conversion_role", + "nullsNotDistinct": false, + "columns": [ + "conversion_id", + "beneficiary_role" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referral_reward_decisions_product_check": { + "name": "impact_referral_reward_decisions_product_check", + "value": "\"impact_referral_reward_decisions\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_referral_reward_decisions_beneficiary_role_check": { + "name": "impact_referral_reward_decisions_beneficiary_role_check", + "value": "\"impact_referral_reward_decisions\".\"beneficiary_role\" IN ('referrer', 'referee')" + }, + "impact_referral_reward_decisions_outcome_check": { + "name": "impact_referral_reward_decisions_outcome_check", + "value": "\"impact_referral_reward_decisions\".\"outcome\" IN ('granted', 'cap_limited', 'disqualified')" + }, + "impact_referral_reward_decisions_reward_kind_check": { + "name": "impact_referral_reward_decisions_reward_kind_check", + "value": "\"impact_referral_reward_decisions\".\"reward_kind\" IN ('kiloclaw_free_month', 'kilo_pass_bonus')" + }, + "impact_referral_reward_decisions_months_granted_non_negative_check": { + "name": "impact_referral_reward_decisions_months_granted_non_negative_check", + "value": "\"impact_referral_reward_decisions\".\"months_granted\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_rewards": { + "name": "impact_referral_rewards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "conversion_id": { + "name": "conversion_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beneficiary_role": { + "name": "beneficiary_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reward_kind": { + "name": "reward_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw_free_month'" + }, + "months_granted": { + "name": "months_granted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "reward_percent": { + "name": "reward_percent", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "source_tier": { + "name": "source_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_amount_usd": { + "name": "reward_amount_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "applies_to_subscription_id": { + "name": "applies_to_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applies_to_kilo_pass_subscription_id": { + "name": "applies_to_kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "consumed_kilo_pass_issuance_id": { + "name": "consumed_kilo_pass_issuance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "consumed_kilo_pass_issuance_item_id": { + "name": "consumed_kilo_pass_issuance_item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "earned_at": { + "name": "earned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reversed_at": { + "name": "reversed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_reason": { + "name": "review_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_rewards_beneficiary_user_id": { + "name": "IDX_impact_referral_rewards_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referral_rewards_status": { + "name": "IDX_impact_referral_rewards_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_rewards_conversion_id_impact_referral_conversions_id_fk": { + "name": "impact_referral_rewards_conversion_id_impact_referral_conversions_id_fk", + "tableFrom": "impact_referral_rewards", + "tableTo": "impact_referral_conversions", + "columnsFrom": [ + "conversion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_rewards_decision_id_impact_referral_reward_decisions_id_fk": { + "name": "impact_referral_rewards_decision_id_impact_referral_reward_decisions_id_fk", + "tableFrom": "impact_referral_rewards", + "tableTo": "impact_referral_reward_decisions", + "columnsFrom": [ + "decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_rewards_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_referral_rewards_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "FK_impact_referral_rewards_kilo_pass_subscription": { + "name": "FK_impact_referral_rewards_kilo_pass_subscription", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "applies_to_kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "FK_impact_referral_rewards_kilo_pass_issuance": { + "name": "FK_impact_referral_rewards_kilo_pass_issuance", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilo_pass_issuances", + "columnsFrom": [ + "consumed_kilo_pass_issuance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "FK_impact_referral_rewards_kilo_pass_issuance_item": { + "name": "FK_impact_referral_rewards_kilo_pass_issuance_item", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilo_pass_issuance_items", + "columnsFrom": [ + "consumed_kilo_pass_issuance_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referral_rewards_conversion_role": { + "name": "UQ_impact_referral_rewards_conversion_role", + "nullsNotDistinct": false, + "columns": [ + "conversion_id", + "beneficiary_role" + ] + }, + "UQ_impact_referral_rewards_decision_id": { + "name": "UQ_impact_referral_rewards_decision_id", + "nullsNotDistinct": false, + "columns": [ + "decision_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referral_rewards_product_check": { + "name": "impact_referral_rewards_product_check", + "value": "\"impact_referral_rewards\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_referral_rewards_beneficiary_role_check": { + "name": "impact_referral_rewards_beneficiary_role_check", + "value": "\"impact_referral_rewards\".\"beneficiary_role\" IN ('referrer', 'referee')" + }, + "impact_referral_rewards_reward_kind_check": { + "name": "impact_referral_rewards_reward_kind_check", + "value": "\"impact_referral_rewards\".\"reward_kind\" IN ('kiloclaw_free_month', 'kilo_pass_bonus')" + }, + "impact_referral_rewards_status_check": { + "name": "impact_referral_rewards_status_check", + "value": "\"impact_referral_rewards\".\"status\" IN ('pending', 'earned', 'applied', 'reversed', 'expired', 'canceled', 'review_required')" + }, + "impact_referral_rewards_months_granted_non_negative_check": { + "name": "impact_referral_rewards_months_granted_non_negative_check", + "value": "\"impact_referral_rewards\".\"months_granted\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_referrals": { + "name": "impact_referrals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "referee_user_id": { + "name": "referee_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer_user_id": { + "name": "referrer_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_touch_id": { + "name": "source_touch_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "impact_referral_id": { + "name": "impact_referral_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referrals_referrer_user_id": { + "name": "IDX_impact_referrals_referrer_user_id", + "columns": [ + { + "expression": "referrer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referrals_source_touch_id": { + "name": "IDX_impact_referrals_source_touch_id", + "columns": [ + { + "expression": "source_touch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referrals_referee_user_id_kilocode_users_id_fk": { + "name": "impact_referrals_referee_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referrals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referee_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referrals_referrer_user_id_kilocode_users_id_fk": { + "name": "impact_referrals_referrer_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referrals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referrer_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "impact_referrals_source_touch_id_impact_attribution_touches_id_fk": { + "name": "impact_referrals_source_touch_id_impact_attribution_touches_id_fk", + "tableFrom": "impact_referrals", + "tableTo": "impact_attribution_touches", + "columnsFrom": [ + "source_touch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referrals_product_referee_user_id": { + "name": "UQ_impact_referrals_product_referee_user_id", + "nullsNotDistinct": false, + "columns": [ + "product", + "referee_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referrals_product_check": { + "name": "impact_referrals_product_check", + "value": "\"impact_referrals\".\"product\" IN ('kiloclaw', 'kilo_pass')" + } + }, + "isRLSEnabled": false + }, + "public.ja4_digest": { + "name": "ja4_digest", + "schema": "", + "columns": { + "ja4_digest_id": { + "name": "ja4_digest_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "ja4_digest": { + "name": "ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_ja4_digest": { + "name": "UQ_ja4_digest", + "columns": [ + { + "expression": "ja4_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilo_pass_audit_log": { + "name": "kilo_pass_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_credit_transaction_id": { + "name": "related_credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_monthly_issuance_id": { + "name": "related_monthly_issuance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "IDX_kilo_pass_audit_log_created_at": { + "name": "IDX_kilo_pass_audit_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_kilo_user_id": { + "name": "IDX_kilo_pass_audit_log_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_kilo_pass_subscription_id": { + "name": "IDX_kilo_pass_audit_log_kilo_pass_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_action": { + "name": "IDX_kilo_pass_audit_log_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_result": { + "name": "IDX_kilo_pass_audit_log_result", + "columns": [ + { + "expression": "result", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_idempotency_key": { + "name": "IDX_kilo_pass_audit_log_idempotency_key", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_stripe_event_id": { + "name": "IDX_kilo_pass_audit_log_stripe_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_stripe_invoice_id": { + "name": "IDX_kilo_pass_audit_log_stripe_invoice_id", + "columns": [ + { + "expression": "stripe_invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_stripe_subscription_id": { + "name": "IDX_kilo_pass_audit_log_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_related_credit_transaction_id": { + "name": "IDX_kilo_pass_audit_log_related_credit_transaction_id", + "columns": [ + { + "expression": "related_credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_related_monthly_issuance_id": { + "name": "IDX_kilo_pass_audit_log_related_monthly_issuance_id", + "columns": [ + { + "expression": "related_monthly_issuance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_audit_log_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_audit_log_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_audit_log_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_audit_log_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_audit_log_related_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_audit_log_related_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "credit_transactions", + "columnsFrom": [ + "related_credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_audit_log_related_monthly_issuance_id_kilo_pass_issuances_id_fk": { + "name": "kilo_pass_audit_log_related_monthly_issuance_id_kilo_pass_issuances_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "kilo_pass_issuances", + "columnsFrom": [ + "related_monthly_issuance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_audit_log_action_check": { + "name": "kilo_pass_audit_log_action_check", + "value": "\"kilo_pass_audit_log\".\"action\" IN ('stripe_webhook_received', 'kilo_pass_invoice_paid_handled', 'store_purchase_completed', 'store_notification_received', 'store_subscription_renewed', 'store_subscription_canceled', 'store_subscription_expired', 'store_subscription_refunded', 'base_credits_issued', 'bonus_credits_issued', 'bonus_credits_skipped_idempotent', 'first_month_50pct_promo_issued', 'yearly_monthly_base_cron_started', 'yearly_monthly_base_cron_completed', 'issue_yearly_remaining_credits', 'duplicate_card_subscription_canceled', 'yearly_monthly_bonus_cron_started', 'yearly_monthly_bonus_cron_completed')" + }, + "kilo_pass_audit_log_result_check": { + "name": "kilo_pass_audit_log_result_check", + "value": "\"kilo_pass_audit_log\".\"result\" IN ('success', 'skipped_idempotent', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_issuance_items": { + "name": "kilo_pass_issuance_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_issuance_id": { + "name": "kilo_pass_issuance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credit_transaction_id": { + "name": "credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true + }, + "bonus_percent_applied": { + "name": "bonus_percent_applied", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_issuance_items_issuance_id": { + "name": "IDX_kilo_pass_issuance_items_issuance_id", + "columns": [ + { + "expression": "kilo_pass_issuance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_issuance_items_credit_transaction_id": { + "name": "IDX_kilo_pass_issuance_items_credit_transaction_id", + "columns": [ + { + "expression": "credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_issuance_items_kilo_pass_issuance_id_kilo_pass_issuances_id_fk": { + "name": "kilo_pass_issuance_items_kilo_pass_issuance_id_kilo_pass_issuances_id_fk", + "tableFrom": "kilo_pass_issuance_items", + "tableTo": "kilo_pass_issuances", + "columnsFrom": [ + "kilo_pass_issuance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_issuance_items_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_issuance_items_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_issuance_items", + "tableTo": "credit_transactions", + "columnsFrom": [ + "credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilo_pass_issuance_items_credit_transaction_id_unique": { + "name": "kilo_pass_issuance_items_credit_transaction_id_unique", + "nullsNotDistinct": false, + "columns": [ + "credit_transaction_id" + ] + }, + "UQ_kilo_pass_issuance_items_issuance_kind": { + "name": "UQ_kilo_pass_issuance_items_issuance_kind", + "nullsNotDistinct": false, + "columns": [ + "kilo_pass_issuance_id", + "kind" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_issuance_items_bonus_percent_applied_range_check": { + "name": "kilo_pass_issuance_items_bonus_percent_applied_range_check", + "value": "\"kilo_pass_issuance_items\".\"bonus_percent_applied\" IS NULL OR (\"kilo_pass_issuance_items\".\"bonus_percent_applied\" >= 0 AND \"kilo_pass_issuance_items\".\"bonus_percent_applied\" <= 1)" + }, + "kilo_pass_issuance_items_amount_usd_non_negative_check": { + "name": "kilo_pass_issuance_items_amount_usd_non_negative_check", + "value": "\"kilo_pass_issuance_items\".\"amount_usd\" >= 0" + }, + "kilo_pass_issuance_items_kind_check": { + "name": "kilo_pass_issuance_items_kind_check", + "value": "\"kilo_pass_issuance_items\".\"kind\" IN ('base', 'bonus', 'promo_first_month_50pct', 'referral_bonus')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_issuances": { + "name": "kilo_pass_issuances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_month": { + "name": "issue_month", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initial_welcome_promo_eligibility_reason": { + "name": "initial_welcome_promo_eligibility_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_issuances_stripe_invoice_id": { + "name": "UQ_kilo_pass_issuances_stripe_invoice_id", + "columns": [ + { + "expression": "stripe_invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_issuances\".\"stripe_invoice_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_issuances_subscription_id": { + "name": "IDX_kilo_pass_issuances_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_issuances_issue_month": { + "name": "IDX_kilo_pass_issuances_issue_month", + "columns": [ + { + "expression": "issue_month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_issuances_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_issuances_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_issuances", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_issuances_subscription_issue_month": { + "name": "UQ_kilo_pass_issuances_subscription_issue_month", + "nullsNotDistinct": false, + "columns": [ + "kilo_pass_subscription_id", + "issue_month" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_issuances_issue_month_day_one_check": { + "name": "kilo_pass_issuances_issue_month_day_one_check", + "value": "EXTRACT(DAY FROM \"kilo_pass_issuances\".\"issue_month\") = 1" + }, + "kilo_pass_issuances_source_check": { + "name": "kilo_pass_issuances_source_check", + "value": "\"kilo_pass_issuances\".\"source\" IN ('stripe_invoice', 'app_store_transaction', 'google_play_transaction', 'cron')" + }, + "kilo_pass_issuances_initial_welcome_promo_reason_check": { + "name": "kilo_pass_issuances_initial_welcome_promo_reason_check", + "value": "\"kilo_pass_issuances\".\"initial_welcome_promo_eligibility_reason\" IN ('first_payment_fingerprint_claim', 'fingerprint_previously_claimed', 'missing_fingerprint', 'no_supported_fingerprint', 'no_positive_settlement', 'settlement_unresolved')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_agreements": { + "name": "kilo_pass_org_agreements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "parent_organization_id": { + "name": "parent_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "term_version_id": { + "name": "term_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processing_condition": { + "name": "processing_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "purchase_channel": { + "name": "purchase_channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purchased_pass_capacity": { + "name": "purchased_pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "next_purchased_pass_capacity": { + "name": "next_purchased_pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_capacity_effective_at": { + "name": "next_capacity_effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paid_from": { + "name": "paid_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paid_until": { + "name": "paid_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "issuance_anchor_at": { + "name": "issuance_anchor_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_seat_add_on_item_id": { + "name": "provider_seat_add_on_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activation_provider_event_id": { + "name": "activation_provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_contract_id": { + "name": "external_contract_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_review_required_at": { + "name": "payment_review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_effective_at": { + "name": "cancellation_effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "manually_issued_through": { + "name": "manually_issued_through", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_org_agreements_one_non_ended_parent": { + "name": "UQ_kilo_pass_org_agreements_one_non_ended_parent", + "columns": [ + { + "expression": "parent_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"state\" <> 'ended'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_provider_subscription": { + "name": "UQ_kilo_pass_org_agreements_provider_subscription", + "columns": [ + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"provider_subscription_id\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"state\" <> 'ended'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_provider_seat_add_on_item": { + "name": "UQ_kilo_pass_org_agreements_provider_seat_add_on_item", + "columns": [ + { + "expression": "provider_seat_add_on_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"provider_seat_add_on_item_id\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"state\" <> 'ended'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_external_contract": { + "name": "UQ_kilo_pass_org_agreements_external_contract", + "columns": [ + { + "expression": "external_contract_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"external_contract_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_activation_provider_event": { + "name": "UQ_kilo_pass_org_agreements_activation_provider_event", + "columns": [ + { + "expression": "activation_provider_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"activation_provider_event_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_org_agreements_processing": { + "name": "IDX_kilo_pass_org_agreements_processing", + "columns": [ + { + "expression": "processing_condition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_agreements_parent_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_agreements_parent_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_agreements", + "tableTo": "organizations", + "columnsFrom": [ + "parent_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_agreements_term_version_id_kilo_pass_org_term_versions_id_fk": { + "name": "kilo_pass_org_agreements_term_version_id_kilo_pass_org_term_versions_id_fk", + "tableFrom": "kilo_pass_org_agreements", + "tableTo": "kilo_pass_org_term_versions", + "columnsFrom": [ + "term_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_agreements_purchased_capacity_non_negative_check": { + "name": "kilo_pass_org_agreements_purchased_capacity_non_negative_check", + "value": "\"kilo_pass_org_agreements\".\"purchased_pass_capacity\" >= 0" + }, + "kilo_pass_org_agreements_next_capacity_check": { + "name": "kilo_pass_org_agreements_next_capacity_check", + "value": "(\"kilo_pass_org_agreements\".\"next_purchased_pass_capacity\" IS NULL AND \"kilo_pass_org_agreements\".\"next_capacity_effective_at\" IS NULL) OR (\"kilo_pass_org_agreements\".\"next_purchased_pass_capacity\" >= 0 AND \"kilo_pass_org_agreements\".\"next_capacity_effective_at\" IS NOT NULL)" + }, + "kilo_pass_org_agreements_paid_interval_check": { + "name": "kilo_pass_org_agreements_paid_interval_check", + "value": "(\"kilo_pass_org_agreements\".\"paid_from\" IS NULL AND \"kilo_pass_org_agreements\".\"paid_until\" IS NULL) OR (\"kilo_pass_org_agreements\".\"paid_from\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"paid_until\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"paid_from\" < \"kilo_pass_org_agreements\".\"paid_until\")" + }, + "kilo_pass_org_agreements_state_check": { + "name": "kilo_pass_org_agreements_state_check", + "value": "\"kilo_pass_org_agreements\".\"state\" IN ('pending_payment', 'active', 'cancel_at_period_end', 'ended')" + }, + "kilo_pass_org_agreements_processing_condition_check": { + "name": "kilo_pass_org_agreements_processing_condition_check", + "value": "\"kilo_pass_org_agreements\".\"processing_condition\" IN ('ready', 'manual', 'blocked', 'overallocated', 'failed', 'suspended_for_review')" + }, + "kilo_pass_org_agreements_purchase_channel_check": { + "name": "kilo_pass_org_agreements_purchase_channel_check", + "value": "\"kilo_pass_org_agreements\".\"purchase_channel\" IN ('self_serve', 'manual')" + }, + "kilo_pass_org_agreements_cadence_check": { + "name": "kilo_pass_org_agreements_cadence_check", + "value": "\"kilo_pass_org_agreements\".\"cadence\" IN ('monthly', 'yearly')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_allocation_plan_rows": { + "name": "kilo_pass_org_allocation_plan_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "allocation_plan_id": { + "name": "allocation_plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_container_organization_id": { + "name": "allocation_container_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pass_capacity": { + "name": "pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_org_allocation_plan_rows_positive_container": { + "name": "IDX_kilo_pass_org_allocation_plan_rows_positive_container", + "columns": [ + { + "expression": "allocation_container_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kilo_pass_org_allocation_plan_rows\".\"pass_capacity\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_allocation_plan_rows_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk": { + "name": "kilo_pass_org_allocation_plan_rows_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk", + "tableFrom": "kilo_pass_org_allocation_plan_rows", + "tableTo": "kilo_pass_org_allocation_plans", + "columnsFrom": [ + "allocation_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_allocation_plan_rows_allocation_container_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_allocation_plan_rows_allocation_container_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_allocation_plan_rows", + "tableTo": "organizations", + "columnsFrom": [ + "allocation_container_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_allocation_plan_rows_plan_container": { + "name": "UQ_kilo_pass_org_allocation_plan_rows_plan_container", + "nullsNotDistinct": false, + "columns": [ + "allocation_plan_id", + "allocation_container_organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_allocation_plan_rows_capacity_non_negative_check": { + "name": "kilo_pass_org_allocation_plan_rows_capacity_non_negative_check", + "value": "\"kilo_pass_org_allocation_plan_rows\".\"pass_capacity\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_allocation_plans": { + "name": "kilo_pass_org_allocation_plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "effective_window_start": { + "name": "effective_window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kilo_pass_org_allocation_plans_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_allocation_plans_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_allocation_plans", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_allocation_plans_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_allocation_plans_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_allocation_plans", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_allocation_plans_agreement_window": { + "name": "UQ_kilo_pass_org_allocation_plans_agreement_window", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "effective_window_start" + ] + }, + "UQ_kilo_pass_org_allocation_plans_agreement_version": { + "name": "UQ_kilo_pass_org_allocation_plans_agreement_version", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "version" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_allocation_plans_version_positive_check": { + "name": "kilo_pass_org_allocation_plans_version_positive_check", + "value": "\"kilo_pass_org_allocation_plans\".\"version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_audit_records": { + "name": "kilo_pass_org_audit_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_kilo_user_id": { + "name": "actor_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "before_json": { + "name": "before_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_json": { + "name": "after_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_org_audit_records_idempotency": { + "name": "UQ_kilo_pass_org_audit_records_idempotency", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_audit_records\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_org_audit_records_agreement_created": { + "name": "IDX_kilo_pass_org_audit_records_agreement_created", + "columns": [ + { + "expression": "agreement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_audit_records_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_audit_records_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_audit_records", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_org_audit_records_actor_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_audit_records_actor_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_audit_records", + "tableTo": "kilocode_users", + "columnsFrom": [ + "actor_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilo_pass_org_issuance_snapshots": { + "name": "kilo_pass_org_issuance_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "processing_run_id": { + "name": "processing_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "allocation_plan_id": { + "name": "allocation_plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "term_version_id": { + "name": "term_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_container_organization_id": { + "name": "allocation_container_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "qualifying_spend_starts_at": { + "name": "qualifying_spend_starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tranche_key": { + "name": "tranche_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allocated_pass_capacity": { + "name": "allocated_pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "base_credit_microdollars": { + "name": "base_credit_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bonus_credit_microdollars": { + "name": "bonus_credit_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "unlock_spend_microdollars": { + "name": "unlock_spend_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "qualifying_spend_microdollars": { + "name": "qualifying_spend_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bonus_mode": { + "name": "bonus_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bonus_unlocked_at": { + "name": "bonus_unlocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "repair_completed_at": { + "name": "repair_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "bonus_credit_transaction_id": { + "name": "bonus_credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "base_credit_transaction_id": { + "name": "base_credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_org_issuance_snapshots_base_credit_transaction": { + "name": "UQ_kilo_pass_org_issuance_snapshots_base_credit_transaction", + "columns": [ + { + "expression": "base_credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_issuance_snapshots\".\"base_credit_transaction_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_issuance_snapshots_bonus_credit_transaction": { + "name": "UQ_kilo_pass_org_issuance_snapshots_bonus_credit_transaction", + "columns": [ + { + "expression": "bonus_credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_issuance_snapshots\".\"bonus_credit_transaction_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_org_issuance_snapshots_window": { + "name": "IDX_kilo_pass_org_issuance_snapshots_window", + "columns": [ + { + "expression": "agreement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_issuance_snapshots_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_processing_run_id_kilo_pass_org_processing_runs_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_processing_run_id_kilo_pass_org_processing_runs_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_processing_runs", + "columnsFrom": [ + "processing_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_allocation_plans", + "columnsFrom": [ + "allocation_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_term_version_id_kilo_pass_org_term_versions_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_term_version_id_kilo_pass_org_term_versions_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_term_versions", + "columnsFrom": [ + "term_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_allocation_container_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_allocation_container_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "organizations", + "columnsFrom": [ + "allocation_container_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_bonus_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_bonus_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "credit_transactions", + "columnsFrom": [ + "bonus_credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_base_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_base_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "credit_transactions", + "columnsFrom": [ + "base_credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_issuance_snapshots_container_window_tranche": { + "name": "UQ_kilo_pass_org_issuance_snapshots_container_window_tranche", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "allocation_container_organization_id", + "window_start", + "tranche_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_issuance_snapshots_window_check": { + "name": "kilo_pass_org_issuance_snapshots_window_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"window_start\" < \"kilo_pass_org_issuance_snapshots\".\"window_end\"" + }, + "kilo_pass_org_issuance_snapshots_qualifying_spend_window_check": { + "name": "kilo_pass_org_issuance_snapshots_qualifying_spend_window_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"window_start\" <= \"kilo_pass_org_issuance_snapshots\".\"qualifying_spend_starts_at\" AND \"kilo_pass_org_issuance_snapshots\".\"qualifying_spend_starts_at\" < \"kilo_pass_org_issuance_snapshots\".\"window_end\"" + }, + "kilo_pass_org_issuance_snapshots_values_non_negative_check": { + "name": "kilo_pass_org_issuance_snapshots_values_non_negative_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"allocated_pass_capacity\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"base_credit_microdollars\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"bonus_credit_microdollars\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"unlock_spend_microdollars\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"qualifying_spend_microdollars\" >= 0" + }, + "kilo_pass_org_issuance_snapshots_kind_check": { + "name": "kilo_pass_org_issuance_snapshots_kind_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"kind\" IN ('regular', 'bridge', 'supplement')" + }, + "kilo_pass_org_issuance_snapshots_bonus_mode_check": { + "name": "kilo_pass_org_issuance_snapshots_bonus_mode_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"bonus_mode\" IN ('after_base', 'upfront')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_notification_deliveries": { + "name": "kilo_pass_org_notification_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "processing_run_id": { + "name": "processing_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_kilo_user_id": { + "name": "recipient_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_org_notification_deliveries_status": { + "name": "IDX_kilo_pass_org_notification_deliveries_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_notification_deliveries_processing_run_id_kilo_pass_org_processing_runs_id_fk": { + "name": "kilo_pass_org_notification_deliveries_processing_run_id_kilo_pass_org_processing_runs_id_fk", + "tableFrom": "kilo_pass_org_notification_deliveries", + "tableTo": "kilo_pass_org_processing_runs", + "columnsFrom": [ + "processing_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_notification_deliveries_recipient_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_notification_deliveries_recipient_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_notification_deliveries", + "tableTo": "kilocode_users", + "columnsFrom": [ + "recipient_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_notification_deliveries_run_recipient": { + "name": "UQ_kilo_pass_org_notification_deliveries_run_recipient", + "nullsNotDistinct": false, + "columns": [ + "processing_run_id", + "recipient_kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_notification_deliveries_status_check": { + "name": "kilo_pass_org_notification_deliveries_status_check", + "value": "\"kilo_pass_org_notification_deliveries\".\"status\" IN ('pending', 'sending', 'sent', 'failed')" + }, + "kilo_pass_org_notification_deliveries_attempt_count_check": { + "name": "kilo_pass_org_notification_deliveries_attempt_count_check", + "value": "\"kilo_pass_org_notification_deliveries\".\"attempt_count\" >= 0" + }, + "kilo_pass_org_notification_deliveries_sent_check": { + "name": "kilo_pass_org_notification_deliveries_sent_check", + "value": "(\"kilo_pass_org_notification_deliveries\".\"status\" = 'sent' AND \"kilo_pass_org_notification_deliveries\".\"sent_at\" IS NOT NULL AND \"kilo_pass_org_notification_deliveries\".\"lease_expires_at\" IS NULL) OR (\"kilo_pass_org_notification_deliveries\".\"status\" = 'sending' AND \"kilo_pass_org_notification_deliveries\".\"sent_at\" IS NULL AND \"kilo_pass_org_notification_deliveries\".\"lease_expires_at\" IS NOT NULL) OR (\"kilo_pass_org_notification_deliveries\".\"status\" IN ('pending', 'failed') AND \"kilo_pass_org_notification_deliveries\".\"sent_at\" IS NULL AND \"kilo_pass_org_notification_deliveries\".\"lease_expires_at\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_processing_runs": { + "name": "kilo_pass_org_processing_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_org_processing_runs_state_lease": { + "name": "IDX_kilo_pass_org_processing_runs_state_lease", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_processing_runs_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_processing_runs_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_processing_runs", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_processing_runs_agreement_window": { + "name": "UQ_kilo_pass_org_processing_runs_agreement_window", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "window_start" + ] + }, + "UQ_kilo_pass_org_processing_runs_idempotency": { + "name": "UQ_kilo_pass_org_processing_runs_idempotency", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_processing_runs_window_check": { + "name": "kilo_pass_org_processing_runs_window_check", + "value": "\"kilo_pass_org_processing_runs\".\"window_start\" < \"kilo_pass_org_processing_runs\".\"window_end\"" + }, + "kilo_pass_org_processing_runs_attempt_count_non_negative_check": { + "name": "kilo_pass_org_processing_runs_attempt_count_non_negative_check", + "value": "\"kilo_pass_org_processing_runs\".\"attempt_count\" >= 0" + }, + "kilo_pass_org_processing_runs_state_check": { + "name": "kilo_pass_org_processing_runs_state_check", + "value": "\"kilo_pass_org_processing_runs\".\"state\" IN ('pending', 'running', 'succeeded', 'blocked', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_qualifying_spend_events": { + "name": "kilo_pass_org_qualifying_spend_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "issuance_snapshot_id": { + "name": "issuance_snapshot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_container_organization_id": { + "name": "allocation_container_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credit_transaction_id": { + "name": "credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "spent_microdollars": { + "name": "spent_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_org_qualifying_spend_events_snapshot_occurred": { + "name": "IDX_kilo_pass_org_qualifying_spend_events_snapshot_occurred", + "columns": [ + { + "expression": "issuance_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_qualifying_spend_events_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk": { + "name": "kilo_pass_org_qualifying_spend_events_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk", + "tableFrom": "kilo_pass_org_qualifying_spend_events", + "tableTo": "kilo_pass_org_issuance_snapshots", + "columnsFrom": [ + "issuance_snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_qualifying_spend_events_allocation_container_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_qualifying_spend_events_allocation_container_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_qualifying_spend_events", + "tableTo": "organizations", + "columnsFrom": [ + "allocation_container_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_qualifying_spend_events_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_org_qualifying_spend_events_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_org_qualifying_spend_events", + "tableTo": "credit_transactions", + "columnsFrom": [ + "credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_qualifying_spend_events_snapshot_credit_transaction": { + "name": "UQ_kilo_pass_org_qualifying_spend_events_snapshot_credit_transaction", + "nullsNotDistinct": false, + "columns": [ + "issuance_snapshot_id", + "credit_transaction_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_qualifying_spend_events_amount_positive_check": { + "name": "kilo_pass_org_qualifying_spend_events_amount_positive_check", + "value": "\"kilo_pass_org_qualifying_spend_events\".\"spent_microdollars\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_supplements": { + "name": "kilo_pass_org_supplements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "issuance_snapshot_id": { + "name": "issuance_snapshot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_invoice_line_id": { + "name": "provider_invoice_line_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remaining_service_numerator": { + "name": "remaining_service_numerator", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "remaining_service_denominator": { + "name": "remaining_service_denominator", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kilo_pass_org_supplements_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk": { + "name": "kilo_pass_org_supplements_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk", + "tableFrom": "kilo_pass_org_supplements", + "tableTo": "kilo_pass_org_issuance_snapshots", + "columnsFrom": [ + "issuance_snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_supplements_provider_invoice_line": { + "name": "UQ_kilo_pass_org_supplements_provider_invoice_line", + "nullsNotDistinct": false, + "columns": [ + "provider_invoice_line_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_supplements_ratio_check": { + "name": "kilo_pass_org_supplements_ratio_check", + "value": "\"kilo_pass_org_supplements\".\"remaining_service_numerator\" > 0 AND \"kilo_pass_org_supplements\".\"remaining_service_denominator\" > 0 AND \"kilo_pass_org_supplements\".\"remaining_service_numerator\" <= \"kilo_pass_org_supplements\".\"remaining_service_denominator\"" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_term_transitions": { + "name": "kilo_pass_org_term_transitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "from_term_version_id": { + "name": "from_term_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "to_term_version_id": { + "name": "to_term_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kilo_pass_org_term_transitions_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_term_transitions_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_term_transitions", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_term_transitions_from_term_version_id_kilo_pass_org_term_versions_id_fk": { + "name": "kilo_pass_org_term_transitions_from_term_version_id_kilo_pass_org_term_versions_id_fk", + "tableFrom": "kilo_pass_org_term_transitions", + "tableTo": "kilo_pass_org_term_versions", + "columnsFrom": [ + "from_term_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_term_transitions_to_term_version_id_kilo_pass_org_term_versions_id_fk": { + "name": "kilo_pass_org_term_transitions_to_term_version_id_kilo_pass_org_term_versions_id_fk", + "tableFrom": "kilo_pass_org_term_transitions", + "tableTo": "kilo_pass_org_term_versions", + "columnsFrom": [ + "to_term_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_term_transitions_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_term_transitions_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_term_transitions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_term_transitions_agreement_effective": { + "name": "UQ_kilo_pass_org_term_transitions_agreement_effective", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "effective_at" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_term_transitions_changes_version_check": { + "name": "kilo_pass_org_term_transitions_changes_version_check", + "value": "\"kilo_pass_org_term_transitions\".\"from_term_version_id\" <> \"kilo_pass_org_term_transitions\".\"to_term_version_id\"" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_term_versions": { + "name": "kilo_pass_org_term_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "version_key": { + "name": "version_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_price_microdollars_per_pass": { + "name": "billing_price_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "base_credit_microdollars_per_pass": { + "name": "base_credit_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bonus_credit_microdollars_per_pass": { + "name": "bonus_credit_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "unlock_spend_microdollars_per_pass": { + "name": "unlock_spend_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bonus_mode": { + "name": "bonus_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kilo_pass_org_term_versions_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_term_versions_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_term_versions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_term_versions_version_key": { + "name": "UQ_kilo_pass_org_term_versions_version_key", + "nullsNotDistinct": false, + "columns": [ + "version_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_term_versions_amounts_non_negative_check": { + "name": "kilo_pass_org_term_versions_amounts_non_negative_check", + "value": "\"kilo_pass_org_term_versions\".\"billing_price_microdollars_per_pass\" >= 0 AND \"kilo_pass_org_term_versions\".\"base_credit_microdollars_per_pass\" >= 0 AND \"kilo_pass_org_term_versions\".\"bonus_credit_microdollars_per_pass\" >= 0 AND \"kilo_pass_org_term_versions\".\"unlock_spend_microdollars_per_pass\" >= 0" + }, + "kilo_pass_org_term_versions_tier_check": { + "name": "kilo_pass_org_term_versions_tier_check", + "value": "\"kilo_pass_org_term_versions\".\"tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_org_term_versions_cadence_check": { + "name": "kilo_pass_org_term_versions_cadence_check", + "value": "\"kilo_pass_org_term_versions\".\"cadence\" IN ('monthly', 'yearly')" + }, + "kilo_pass_org_term_versions_bonus_mode_check": { + "name": "kilo_pass_org_term_versions_bonus_mode_check", + "value": "\"kilo_pass_org_term_versions\".\"bonus_mode\" IN ('after_base', 'upfront')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_pause_events": { + "name": "kilo_pass_pause_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "resumes_at": { + "name": "resumes_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resumed_at": { + "name": "resumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_pause_events_subscription_id": { + "name": "IDX_kilo_pass_pause_events_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_pause_events_one_open_per_sub": { + "name": "UQ_kilo_pass_pause_events_one_open_per_sub", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_pause_events\".\"resumed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_pause_events_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_pause_events_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_pause_events", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_pause_events_resumed_at_after_paused_at_check": { + "name": "kilo_pass_pause_events_resumed_at_after_paused_at_check", + "value": "\"kilo_pass_pause_events\".\"resumed_at\" IS NULL OR \"kilo_pass_pause_events\".\"resumed_at\" >= \"kilo_pass_pause_events\".\"paused_at\"" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_scheduled_changes": { + "name": "kilo_pass_scheduled_changes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_tier": { + "name": "from_tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_cadence": { + "name": "from_cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_tier": { + "name": "to_tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_cadence": { + "name": "to_cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_scheduled_changes_kilo_user_id": { + "name": "IDX_kilo_pass_scheduled_changes_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_status": { + "name": "IDX_kilo_pass_scheduled_changes_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_stripe_subscription_id": { + "name": "IDX_kilo_pass_scheduled_changes_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_scheduled_changes_active_stripe_subscription_id": { + "name": "UQ_kilo_pass_scheduled_changes_active_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_scheduled_changes\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_effective_at": { + "name": "IDX_kilo_pass_scheduled_changes_effective_at", + "columns": [ + { + "expression": "effective_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_deleted_at": { + "name": "IDX_kilo_pass_scheduled_changes_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_scheduled_changes_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_scheduled_changes_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_scheduled_changes", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_scheduled_changes_stripe_subscription_id_kilo_pass_subscriptions_stripe_subscription_id_fk": { + "name": "kilo_pass_scheduled_changes_stripe_subscription_id_kilo_pass_subscriptions_stripe_subscription_id_fk", + "tableFrom": "kilo_pass_scheduled_changes", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "stripe_subscription_id" + ], + "columnsTo": [ + "stripe_subscription_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_scheduled_changes_from_tier_check": { + "name": "kilo_pass_scheduled_changes_from_tier_check", + "value": "\"kilo_pass_scheduled_changes\".\"from_tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_scheduled_changes_from_cadence_check": { + "name": "kilo_pass_scheduled_changes_from_cadence_check", + "value": "\"kilo_pass_scheduled_changes\".\"from_cadence\" IN ('monthly', 'yearly')" + }, + "kilo_pass_scheduled_changes_to_tier_check": { + "name": "kilo_pass_scheduled_changes_to_tier_check", + "value": "\"kilo_pass_scheduled_changes\".\"to_tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_scheduled_changes_to_cadence_check": { + "name": "kilo_pass_scheduled_changes_to_cadence_check", + "value": "\"kilo_pass_scheduled_changes\".\"to_cadence\" IN ('monthly', 'yearly')" + }, + "kilo_pass_scheduled_changes_status_check": { + "name": "kilo_pass_scheduled_changes_status_check", + "value": "\"kilo_pass_scheduled_changes\".\"status\" IN ('not_started', 'active', 'completed', 'released', 'canceled')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_store_events": { + "name": "kilo_pass_store_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_transaction_id": { + "name": "provider_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "app_account_token": { + "name": "app_account_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_store_events_provider_event": { + "name": "UQ_kilo_pass_store_events_provider_event", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_events_provider_subscription": { + "name": "IDX_kilo_pass_store_events_provider_subscription", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_events_app_account_token": { + "name": "IDX_kilo_pass_store_events_app_account_token", + "columns": [ + { + "expression": "app_account_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_store_events_payment_provider_check": { + "name": "kilo_pass_store_events_payment_provider_check", + "value": "\"kilo_pass_store_events\".\"payment_provider\" IN ('stripe', 'app_store', 'google_play')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_store_purchases": { + "name": "kilo_pass_store_purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_transaction_id": { + "name": "provider_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_original_transaction_id": { + "name": "provider_original_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "app_account_token": { + "name": "app_account_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "purchase_token": { + "name": "purchase_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purchased_at": { + "name": "purchased_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "raw_payload_json": { + "name": "raw_payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_store_purchases_provider_transaction": { + "name": "UQ_kilo_pass_store_purchases_provider_transaction", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_subscription_id": { + "name": "IDX_kilo_pass_store_purchases_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_user_id": { + "name": "IDX_kilo_pass_store_purchases_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_app_account_token": { + "name": "IDX_kilo_pass_store_purchases_app_account_token", + "columns": [ + { + "expression": "app_account_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_latest_subscription_purchase": { + "name": "IDX_kilo_pass_store_purchases_latest_subscription_purchase", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purchased_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_store_purchases_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_store_purchases_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_store_purchases", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_store_purchases_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_store_purchases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_store_purchases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "FK_kilo_pass_store_purchases_subscription_owner_provider": { + "name": "FK_kilo_pass_store_purchases_subscription_owner_provider", + "tableFrom": "kilo_pass_store_purchases", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id", + "kilo_user_id", + "payment_provider", + "provider_subscription_id" + ], + "columnsTo": [ + "id", + "kilo_user_id", + "payment_provider", + "provider_subscription_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_store_purchases_store_provider_check": { + "name": "kilo_pass_store_purchases_store_provider_check", + "value": "\"kilo_pass_store_purchases\".\"payment_provider\" IN ('app_store', 'google_play')" + }, + "kilo_pass_store_purchases_payment_provider_check": { + "name": "kilo_pass_store_purchases_payment_provider_check", + "value": "\"kilo_pass_store_purchases\".\"payment_provider\" IN ('stripe', 'app_store', 'google_play')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_subscriptions": { + "name": "kilo_pass_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'stripe'" + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_streak_months": { + "name": "current_streak_months", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_yearly_issue_at": { + "name": "next_yearly_issue_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_subscriptions_kilo_user_id": { + "name": "IDX_kilo_pass_subscriptions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_subscriptions_payment_provider": { + "name": "IDX_kilo_pass_subscriptions_payment_provider", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_subscriptions_status": { + "name": "IDX_kilo_pass_subscriptions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_subscriptions_cadence": { + "name": "IDX_kilo_pass_subscriptions_cadence", + "columns": [ + { + "expression": "cadence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_subscriptions_provider_subscription": { + "name": "UQ_kilo_pass_subscriptions_provider_subscription", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_subscriptions\".\"provider_subscription_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_subscriptions_store_purchase_reference": { + "name": "UQ_kilo_pass_subscriptions_store_purchase_reference", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_subscriptions_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_subscriptions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilo_pass_subscriptions_stripe_subscription_id_unique": { + "name": "kilo_pass_subscriptions_stripe_subscription_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_subscription_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_subscriptions_current_streak_months_non_negative_check": { + "name": "kilo_pass_subscriptions_current_streak_months_non_negative_check", + "value": "\"kilo_pass_subscriptions\".\"current_streak_months\" >= 0" + }, + "kilo_pass_subscriptions_provider_ids_check": { + "name": "kilo_pass_subscriptions_provider_ids_check", + "value": "(\n \"kilo_pass_subscriptions\".\"payment_provider\" = 'stripe'\n AND \"kilo_pass_subscriptions\".\"provider_subscription_id\" IS NOT NULL\n AND \"kilo_pass_subscriptions\".\"stripe_subscription_id\" IS NOT NULL\n AND \"kilo_pass_subscriptions\".\"provider_subscription_id\" = \"kilo_pass_subscriptions\".\"stripe_subscription_id\"\n ) OR (\n \"kilo_pass_subscriptions\".\"payment_provider\" IN ('app_store', 'google_play')\n AND \"kilo_pass_subscriptions\".\"provider_subscription_id\" IS NOT NULL\n AND \"kilo_pass_subscriptions\".\"stripe_subscription_id\" IS NULL\n )" + }, + "kilo_pass_subscriptions_payment_provider_check": { + "name": "kilo_pass_subscriptions_payment_provider_check", + "value": "\"kilo_pass_subscriptions\".\"payment_provider\" IN ('stripe', 'app_store', 'google_play')" + }, + "kilo_pass_subscriptions_tier_check": { + "name": "kilo_pass_subscriptions_tier_check", + "value": "\"kilo_pass_subscriptions\".\"tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_subscriptions_cadence_check": { + "name": "kilo_pass_subscriptions_cadence_check", + "value": "\"kilo_pass_subscriptions\".\"cadence\" IN ('monthly', 'yearly')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_welcome_promo_payment_fingerprint_claims": { + "name": "kilo_pass_welcome_promo_payment_fingerprint_claims", + "schema": "", + "columns": { + "stripe_payment_method_type": { + "name": "stripe_payment_method_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_fingerprint": { + "name": "stripe_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_stripe_invoice_id": { + "name": "source_stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "kilo_pass_welcome_promo_payment_fingerprint_claims_stripe_payment_method_type_stripe_fingerprint_pk": { + "name": "kilo_pass_welcome_promo_payment_fingerprint_claims_stripe_payment_method_type_stripe_fingerprint_pk", + "columns": [ + "stripe_payment_method_type", + "stripe_fingerprint" + ] + } + }, + "uniqueConstraints": { + "UQ_kilo_pass_welcome_promo_payment_fingerprint_claims_source_invoice_id": { + "name": "UQ_kilo_pass_welcome_promo_payment_fingerprint_claims_source_invoice_id", + "nullsNotDistinct": false, + "columns": [ + "source_stripe_invoice_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_welcome_promo_payment_fingerprint_claims_type_check": { + "name": "kilo_pass_welcome_promo_payment_fingerprint_claims_type_check", + "value": "\"kilo_pass_welcome_promo_payment_fingerprint_claims\".\"stripe_payment_method_type\" IN ('card', 'sepa_debit', 'us_bank_account', 'bacs_debit', 'au_becs_debit')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_access_codes": { + "name": "kiloclaw_access_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_access_codes_code": { + "name": "UQ_kiloclaw_access_codes_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_access_codes_user_status": { + "name": "IDX_kiloclaw_access_codes_user_status", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_access_codes_one_active_per_user": { + "name": "UQ_kiloclaw_access_codes_one_active_per_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_access_codes_kilo_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_access_codes_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_access_codes", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_admin_audit_logs": { + "name": "kiloclaw_admin_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kiloclaw_admin_audit_logs_target_user_id": { + "name": "IDX_kiloclaw_admin_audit_logs_target_user_id", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_admin_audit_logs_action": { + "name": "IDX_kiloclaw_admin_audit_logs_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_admin_audit_logs_created_at": { + "name": "IDX_kiloclaw_admin_audit_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_cli_runs": { + "name": "kiloclaw_cli_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "initiated_by_admin_id": { + "name": "initiated_by_admin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_kiloclaw_cli_runs_user_id": { + "name": "IDX_kiloclaw_cli_runs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_cli_runs_started_at": { + "name": "IDX_kiloclaw_cli_runs_started_at", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_cli_runs_instance_id": { + "name": "IDX_kiloclaw_cli_runs_instance_id", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_cli_runs_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_cli_runs_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_cli_runs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "kiloclaw_cli_runs_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_cli_runs_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_cli_runs", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_cli_runs_initiated_by_admin_id_kilocode_users_id_fk": { + "name": "kiloclaw_cli_runs_initiated_by_admin_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_cli_runs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "initiated_by_admin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_earlybird_purchases": { + "name": "kiloclaw_earlybird_purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manual_payment_id": { + "name": "manual_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kiloclaw_earlybird_purchases_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_earlybird_purchases_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_earlybird_purchases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_earlybird_purchases_user_id_unique": { + "name": "kiloclaw_earlybird_purchases_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "kiloclaw_earlybird_purchases_stripe_charge_id_unique": { + "name": "kiloclaw_earlybird_purchases_stripe_charge_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_charge_id" + ] + }, + "kiloclaw_earlybird_purchases_manual_payment_id_unique": { + "name": "kiloclaw_earlybird_purchases_manual_payment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "manual_payment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_email_log": { + "name": "kiloclaw_email_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email_type": { + "name": "email_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "'epoch'" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_email_log_user_type_global": { + "name": "UQ_kiloclaw_email_log_user_type_global", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_email_log\".\"instance_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_email_log_user_instance_type_period": { + "name": "UQ_kiloclaw_email_log_user_instance_type_period", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_email_log\".\"instance_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_email_log_type_sent_instance": { + "name": "IDX_kiloclaw_email_log_type_sent_instance", + "columns": [ + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sent_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_email_log\".\"instance_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_email_log_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_email_log_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_email_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_email_log_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_email_log_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_email_log", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_google_oauth_connections": { + "name": "kiloclaw_google_oauth_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'google'" + }, + "account_email": { + "name": "account_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_subject": { + "name": "account_subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_secret_encrypted": { + "name": "oauth_client_secret_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_profile": { + "name": "credential_profile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kilo_owned'" + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "grants_by_source": { + "name": "grants_by_source", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "capabilities": { + "name": "capabilities", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_at": { + "name": "last_error_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_google_oauth_connections_instance": { + "name": "UQ_kiloclaw_google_oauth_connections_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_google_oauth_connections_status": { + "name": "IDX_kiloclaw_google_oauth_connections_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_google_oauth_connections_provider": { + "name": "IDX_kiloclaw_google_oauth_connections_provider", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_google_oauth_connections_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_google_oauth_connections_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_google_oauth_connections", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kiloclaw_google_oauth_connections_status_check": { + "name": "kiloclaw_google_oauth_connections_status_check", + "value": "\"kiloclaw_google_oauth_connections\".\"status\" IN ('active', 'action_required', 'disconnected')" + }, + "kiloclaw_google_oauth_connections_credential_profile_check": { + "name": "kiloclaw_google_oauth_connections_credential_profile_check", + "value": "\"kiloclaw_google_oauth_connections\".\"credential_profile\" IN ('legacy', 'kilo_owned')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_image_catalog": { + "name": "kiloclaw_image_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "openclaw_version": { + "name": "openclaw_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variant": { + "name": "variant", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "image_tag": { + "name": "image_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_digest": { + "name": "image_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "rollout_percent": { + "name": "rollout_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_latest": { + "name": "is_latest", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "IDX_kiloclaw_image_catalog_status": { + "name": "IDX_kiloclaw_image_catalog_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_image_catalog_variant": { + "name": "IDX_kiloclaw_image_catalog_variant", + "columns": [ + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_image_catalog_one_latest_per_variant": { + "name": "UQ_kiloclaw_image_catalog_one_latest_per_variant", + "columns": [ + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_image_catalog\".\"is_latest\" = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_image_catalog_one_candidate_per_variant": { + "name": "UQ_kiloclaw_image_catalog_one_candidate_per_variant", + "columns": [ + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_image_catalog\".\"is_latest\" = false AND \"kiloclaw_image_catalog\".\"rollout_percent\" > 0 AND \"kiloclaw_image_catalog\".\"status\" = 'available'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_image_catalog_image_tag_unique": { + "name": "kiloclaw_image_catalog_image_tag_unique", + "nullsNotDistinct": false, + "columns": [ + "image_tag" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_inbound_email_aliases": { + "name": "kiloclaw_inbound_email_aliases", + "schema": "", + "columns": { + "alias": { + "name": "alias", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_kiloclaw_inbound_email_aliases_instance_id": { + "name": "IDX_kiloclaw_inbound_email_aliases_instance_id", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_inbound_email_aliases_active_instance": { + "name": "UQ_kiloclaw_inbound_email_aliases_active_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_inbound_email_aliases\".\"retired_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_inbound_email_aliases_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_inbound_email_aliases_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_inbound_email_aliases", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_inbound_email_reserved_aliases": { + "name": "kiloclaw_inbound_email_reserved_aliases", + "schema": "", + "columns": { + "alias": { + "name": "alias", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_instances": { + "name": "kiloclaw_instances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'fly'" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbound_email_enabled": { + "name": "inbound_email_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inactive_trial_stopped_at": { + "name": "inactive_trial_stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "destroyed_at": { + "name": "destroyed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tracked_image_tag": { + "name": "tracked_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instance_type": { + "name": "instance_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_size_override": { + "name": "admin_size_override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_kiloclaw_instances_active": { + "name": "UQ_kiloclaw_instances_active", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sandbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_instances\".\"destroyed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_active_personal_by_user": { + "name": "IDX_kiloclaw_instances_active_personal_by_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"organization_id\" IS NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_active_org_by_user_org": { + "name": "IDX_kiloclaw_instances_active_org_by_user_org", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"organization_id\" IS NOT NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_active_org_by_org_created": { + "name": "IDX_kiloclaw_instances_active_org_by_org_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"organization_id\" IS NOT NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_user_id_created_at": { + "name": "IDX_kiloclaw_instances_user_id_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_tracked_image_tag": { + "name": "IDX_kiloclaw_instances_tracked_image_tag", + "columns": [ + { + "expression": "tracked_image_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"destroyed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_instance_type": { + "name": "IDX_kiloclaw_instances_instance_type", + "columns": [ + { + "expression": "instance_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"destroyed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_admin_size_override": { + "name": "IDX_kiloclaw_instances_admin_size_override", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"admin_size_override\" IS NOT NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_instances_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_instances_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_instances", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_instances_organization_id_organizations_id_fk": { + "name": "kiloclaw_instances_organization_id_organizations_id_fk", + "tableFrom": "kiloclaw_instances", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_kiloclaw_instances_instance_type": { + "name": "CHK_kiloclaw_instances_instance_type", + "value": "\"kiloclaw_instances\".\"instance_type\" IS NULL OR \"kiloclaw_instances\".\"instance_type\" IN ('perf-1-3', 'perf-4-8', 'perf-4-16', 'shared-2-3', 'shared-2-4', 'custom')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_morning_briefing_configs": { + "name": "kiloclaw_morning_briefing_configs", + "schema": "", + "columns": { + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'0 7 * * *'" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "interest_topics": { + "name": "interest_topics", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kiloclaw_morning_briefing_configs_enabled": { + "name": "IDX_kiloclaw_morning_briefing_configs_enabled", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_morning_briefing_configs\".\"enabled\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_morning_briefing_configs_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_morning_briefing_configs_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_morning_briefing_configs", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_action_notifications": { + "name": "kiloclaw_scheduled_action_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'notice'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_kiloclaw_scheduled_action_notifications_target_kind_channel": { + "name": "UQ_kiloclaw_scheduled_action_notifications_target_kind_channel", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_notifications_pending": { + "name": "IDX_kiloclaw_scheduled_action_notifications_pending", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_scheduled_action_notifications\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_action_notifications_target_id_kiloclaw_scheduled_action_targets_id_fk": { + "name": "kiloclaw_scheduled_action_notifications_target_id_kiloclaw_scheduled_action_targets_id_fk", + "tableFrom": "kiloclaw_scheduled_action_notifications", + "tableTo": "kiloclaw_scheduled_action_targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_action_stages": { + "name": "kiloclaw_scheduled_action_stages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scheduled_action_id": { + "name": "scheduled_action_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_index": { + "name": "stage_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "notice_sent_at": { + "name": "notice_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "applied_count": { + "name": "applied_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "UQ_kiloclaw_scheduled_action_stages_parent_index": { + "name": "UQ_kiloclaw_scheduled_action_stages_parent_index", + "columns": [ + { + "expression": "scheduled_action_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_stages_notice_due": { + "name": "IDX_kiloclaw_scheduled_action_stages_notice_due", + "columns": [ + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_scheduled_action_stages\".\"notice_sent_at\" IS NULL AND \"kiloclaw_scheduled_action_stages\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_action_stages_scheduled_action_id_kiloclaw_scheduled_actions_id_fk": { + "name": "kiloclaw_scheduled_action_stages_scheduled_action_id_kiloclaw_scheduled_actions_id_fk", + "tableFrom": "kiloclaw_scheduled_action_stages", + "tableTo": "kiloclaw_scheduled_actions", + "columnsFrom": [ + "scheduled_action_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_action_targets": { + "name": "kiloclaw_scheduled_action_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scheduled_action_id": { + "name": "scheduled_action_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_id": { + "name": "stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_image_tag": { + "name": "source_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_image_tag": { + "name": "target_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "skip_reason": { + "name": "skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_kiloclaw_scheduled_action_targets_parent_instance": { + "name": "UQ_kiloclaw_scheduled_action_targets_parent_instance", + "columns": [ + { + "expression": "scheduled_action_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_targets_stage": { + "name": "IDX_kiloclaw_scheduled_action_targets_stage", + "columns": [ + { + "expression": "stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_targets_pending_by_instance": { + "name": "IDX_kiloclaw_scheduled_action_targets_pending_by_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_scheduled_action_targets\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_action_targets_scheduled_action_id_kiloclaw_scheduled_actions_id_fk": { + "name": "kiloclaw_scheduled_action_targets_scheduled_action_id_kiloclaw_scheduled_actions_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kiloclaw_scheduled_actions", + "columnsFrom": [ + "scheduled_action_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_action_targets_stage_id_kiloclaw_scheduled_action_stages_id_fk": { + "name": "kiloclaw_scheduled_action_targets_stage_id_kiloclaw_scheduled_action_stages_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kiloclaw_scheduled_action_stages", + "columnsFrom": [ + "stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_action_targets_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_scheduled_action_targets_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_action_targets_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_scheduled_action_targets_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_actions": { + "name": "kiloclaw_scheduled_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_image_tag": { + "name": "target_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "override_pins": { + "name": "override_pins", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notice_lead_hours": { + "name": "notice_lead_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 24 + }, + "notice_subject": { + "name": "notice_subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "notice_body": { + "name": "notice_body", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_count": { + "name": "total_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "applied_count": { + "name": "applied_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "IDX_kiloclaw_scheduled_actions_status": { + "name": "IDX_kiloclaw_scheduled_actions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_actions_action_type": { + "name": "IDX_kiloclaw_scheduled_actions_action_type", + "columns": [ + { + "expression": "action_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_actions_created_by": { + "name": "IDX_kiloclaw_scheduled_actions_created_by", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_actions_target_image_tag_kiloclaw_image_catalog_image_tag_fk": { + "name": "kiloclaw_scheduled_actions_target_image_tag_kiloclaw_image_catalog_image_tag_fk", + "tableFrom": "kiloclaw_scheduled_actions", + "tableTo": "kiloclaw_image_catalog", + "columnsFrom": [ + "target_image_tag" + ], + "columnsTo": [ + "image_tag" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_actions_created_by_kilocode_users_id_fk": { + "name": "kiloclaw_scheduled_actions_created_by_kilocode_users_id_fk", + "tableFrom": "kiloclaw_scheduled_actions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_subscription_change_log": { + "name": "kiloclaw_subscription_change_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "before_state": { + "name": "before_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_state": { + "name": "after_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_kiloclaw_subscription_change_log_subscription_created_at": { + "name": "IDX_kiloclaw_subscription_change_log_subscription_created_at", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscription_change_log_created_at": { + "name": "IDX_kiloclaw_subscription_change_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_subscription_change_log_subscription_id_kiloclaw_subscriptions_id_fk": { + "name": "kiloclaw_subscription_change_log_subscription_id_kiloclaw_subscriptions_id_fk", + "tableFrom": "kiloclaw_subscription_change_log", + "tableTo": "kiloclaw_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kiloclaw_subscription_change_log_actor_type_check": { + "name": "kiloclaw_subscription_change_log_actor_type_check", + "value": "\"kiloclaw_subscription_change_log\".\"actor_type\" IN ('user', 'system')" + }, + "kiloclaw_subscription_change_log_action_check": { + "name": "kiloclaw_subscription_change_log_action_check", + "value": "\"kiloclaw_subscription_change_log\".\"action\" IN ('created', 'status_changed', 'plan_switched', 'period_advanced', 'canceled', 'reactivated', 'suspended', 'destruction_scheduled', 'reassigned', 'backfilled', 'payment_source_changed', 'schedule_changed', 'admin_override')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_subscriptions": { + "name": "kiloclaw_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transferred_to_subscription_id": { + "name": "transferred_to_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "access_origin": { + "name": "access_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_source": { + "name": "payment_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kiloclaw_price_version": { + "name": "kiloclaw_price_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scheduled_plan": { + "name": "scheduled_plan", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_by": { + "name": "scheduled_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pending_conversion": { + "name": "pending_conversion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trial_started_at": { + "name": "trial_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credit_renewal_at": { + "name": "credit_renewal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "commit_ends_at": { + "name": "commit_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "past_due_since": { + "name": "past_due_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "destruction_deadline": { + "name": "destruction_deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_resume_requested_at": { + "name": "auto_resume_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_resume_retry_after": { + "name": "auto_resume_retry_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_resume_attempt_count": { + "name": "auto_resume_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "auto_top_up_triggered_for_period": { + "name": "auto_top_up_triggered_for_period", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kiloclaw_subscriptions_status": { + "name": "IDX_kiloclaw_subscriptions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_user_id": { + "name": "IDX_kiloclaw_subscriptions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_user_status": { + "name": "IDX_kiloclaw_subscriptions_user_status", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_price_version": { + "name": "IDX_kiloclaw_subscriptions_price_version", + "columns": [ + { + "expression": "kiloclaw_price_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_transferred_to": { + "name": "IDX_kiloclaw_subscriptions_transferred_to", + "columns": [ + { + "expression": "transferred_to_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_stripe_schedule_id": { + "name": "IDX_kiloclaw_subscriptions_stripe_schedule_id", + "columns": [ + { + "expression": "stripe_schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_auto_resume_retry_after": { + "name": "IDX_kiloclaw_subscriptions_auto_resume_retry_after", + "columns": [ + { + "expression": "auto_resume_retry_after", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_subscriptions_instance": { + "name": "UQ_kiloclaw_subscriptions_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_subscriptions\".\"instance_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_subscriptions_transferred_to": { + "name": "UQ_kiloclaw_subscriptions_transferred_to", + "columns": [ + { + "expression": "transferred_to_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_subscriptions\".\"transferred_to_subscription_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_earlybird_origin": { + "name": "IDX_kiloclaw_subscriptions_earlybird_origin", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "access_origin", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_subscriptions\".\"access_origin\" = 'earlybird'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_subscriptions_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_subscriptions_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_subscriptions_transferred_to_subscription_id_kiloclaw_subscriptions_id_fk": { + "name": "kiloclaw_subscriptions_transferred_to_subscription_id_kiloclaw_subscriptions_id_fk", + "tableFrom": "kiloclaw_subscriptions", + "tableTo": "kiloclaw_subscriptions", + "columnsFrom": [ + "transferred_to_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_subscriptions_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_subscriptions_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_subscriptions", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_subscriptions_stripe_subscription_id_unique": { + "name": "kiloclaw_subscriptions_stripe_subscription_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_subscription_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kiloclaw_subscriptions_price_version_check": { + "name": "kiloclaw_subscriptions_price_version_check", + "value": "\"kiloclaw_subscriptions\".\"kiloclaw_price_version\" IN ('2026-03-19', '2026-05-10')" + }, + "kiloclaw_subscriptions_plan_check": { + "name": "kiloclaw_subscriptions_plan_check", + "value": "\"kiloclaw_subscriptions\".\"plan\" IN ('trial', 'commit', 'standard')" + }, + "kiloclaw_subscriptions_scheduled_plan_check": { + "name": "kiloclaw_subscriptions_scheduled_plan_check", + "value": "\"kiloclaw_subscriptions\".\"scheduled_plan\" IN ('commit', 'standard')" + }, + "kiloclaw_subscriptions_scheduled_by_check": { + "name": "kiloclaw_subscriptions_scheduled_by_check", + "value": "\"kiloclaw_subscriptions\".\"scheduled_by\" IN ('auto', 'user')" + }, + "kiloclaw_subscriptions_status_check": { + "name": "kiloclaw_subscriptions_status_check", + "value": "\"kiloclaw_subscriptions\".\"status\" IN ('trialing', 'active', 'past_due', 'canceled', 'unpaid')" + }, + "kiloclaw_subscriptions_access_origin_check": { + "name": "kiloclaw_subscriptions_access_origin_check", + "value": "\"kiloclaw_subscriptions\".\"access_origin\" IN ('earlybird')" + }, + "kiloclaw_subscriptions_payment_source_check": { + "name": "kiloclaw_subscriptions_payment_source_check", + "value": "\"kiloclaw_subscriptions\".\"payment_source\" IN ('stripe', 'credits')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_terminal_renewal_failures": { + "name": "kiloclaw_terminal_renewal_failures", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "renewal_boundary": { + "name": "renewal_boundary", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unresolved'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_failure_at": { + "name": "first_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_failure_at": { + "name": "last_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_failure_code": { + "name": "last_failure_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_failure_message": { + "name": "last_failure_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_actor_type": { + "name": "resolution_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_actor_id": { + "name": "resolution_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_at": { + "name": "resolution_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolution_reason": { + "name": "resolution_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_terminal_renewal_failures_subscription_boundary": { + "name": "UQ_kiloclaw_terminal_renewal_failures_subscription_boundary", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "renewal_boundary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_terminal_renewal_failures_unresolved": { + "name": "IDX_kiloclaw_terminal_renewal_failures_unresolved", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "renewal_boundary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_terminal_renewal_failures\".\"status\" = 'unresolved'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_terminal_renewal_failures_status_last_failure_at": { + "name": "IDX_kiloclaw_terminal_renewal_failures_status_last_failure_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_failure_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_terminal_renewal_failures_subscription_id_kiloclaw_subscriptions_id_fk": { + "name": "kiloclaw_terminal_renewal_failures_subscription_id_kiloclaw_subscriptions_id_fk", + "tableFrom": "kiloclaw_terminal_renewal_failures", + "tableTo": "kiloclaw_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kiloclaw_terminal_renewal_failures_status_check": { + "name": "kiloclaw_terminal_renewal_failures_status_check", + "value": "\"kiloclaw_terminal_renewal_failures\".\"status\" IN ('unresolved', 'resolved', 'waived', 'superseded')" + }, + "kiloclaw_terminal_renewal_failures_last_failure_code_check": { + "name": "kiloclaw_terminal_renewal_failures_last_failure_code_check", + "value": "\"kiloclaw_terminal_renewal_failures\".\"last_failure_code\" IN ('credit_balance_read_failed', 'renewal_transaction_failed', 'auto_top_up_marker_write_failed', 'worker_timeout', 'poison_payload', 'queue_delivery_exhausted')" + }, + "kiloclaw_terminal_renewal_failures_resolution_actor_type_check": { + "name": "kiloclaw_terminal_renewal_failures_resolution_actor_type_check", + "value": "\"kiloclaw_terminal_renewal_failures\".\"resolution_actor_type\" IN ('operator', 'system')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_version_pins": { + "name": "kiloclaw_version_pins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "image_tag": { + "name": "image_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_by": { + "name": "pinned_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kiloclaw_version_pins_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_version_pins_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_version_pins", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_version_pins_image_tag_kiloclaw_image_catalog_image_tag_fk": { + "name": "kiloclaw_version_pins_image_tag_kiloclaw_image_catalog_image_tag_fk", + "tableFrom": "kiloclaw_version_pins", + "tableTo": "kiloclaw_image_catalog", + "columnsFrom": [ + "image_tag" + ], + "columnsTo": [ + "image_tag" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "kiloclaw_version_pins_pinned_by_kilocode_users_id_fk": { + "name": "kiloclaw_version_pins_pinned_by_kilocode_users_id_fk", + "tableFrom": "kiloclaw_version_pins", + "tableTo": "kilocode_users", + "columnsFrom": [ + "pinned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_version_pins_instance_id_unique": { + "name": "kiloclaw_version_pins_instance_id_unique", + "nullsNotDistinct": false, + "columns": [ + "instance_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilocode_users": { + "name": "kilocode_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "google_user_email": { + "name": "google_user_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "google_user_name": { + "name": "google_user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "google_user_image_url": { + "name": "google_user_image_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "hosted_domain": { + "name": "hosted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "microdollars_used": { + "name": "microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "kilo_pass_threshold": { + "name": "kilo_pass_threshold", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_store_account_token": { + "name": "app_store_account_token", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_super_admin": { + "name": "is_super_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "can_view_sessions": { + "name": "can_view_sessions", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "can_manage_credits": { + "name": "can_manage_credits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "total_microdollars_acquired": { + "name": "total_microdollars_acquired", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "next_credit_expiration_at": { + "name": "next_credit_expiration_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "has_validation_stytch": { + "name": "has_validation_stytch", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_validation_novel_card_with_hold": { + "name": "has_validation_novel_card_with_hold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_at": { + "name": "blocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "blocked_by_kilo_user_id": { + "name": "blocked_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_token_pepper": { + "name": "api_token_pepper", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_session_pepper": { + "name": "web_session_pepper", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_top_up_enabled": { + "name": "auto_top_up_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_bot": { + "name": "is_bot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "kiloclaw_early_access": { + "name": "kiloclaw_early_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cohorts": { + "name": "cohorts", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "completed_welcome_form": { + "name": "completed_welcome_form", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "linkedin_url": { + "name": "linkedin_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_url": { + "name": "github_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discord_server_membership_verified_at": { + "name": "discord_server_membership_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "openrouter_upstream_safety_identifier": { + "name": "openrouter_upstream_safety_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "openrouter_downstream_safety_identifier": { + "name": "openrouter_downstream_safety_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vercel_downstream_safety_identifier": { + "name": "vercel_downstream_safety_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_source": { + "name": "customer_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signup_ip": { + "name": "signup_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_deletion_requested_at": { + "name": "account_deletion_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_domain": { + "name": "email_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "personal_account_disabled": { + "name": "personal_account_disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "IDX_kilocode_users_signup_ip_created_at": { + "name": "IDX_kilocode_users_signup_ip_created_at", + "columns": [ + { + "expression": "signup_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_blocked_at": { + "name": "IDX_kilocode_users_blocked_at", + "columns": [ + { + "expression": "blocked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_blocked_by_kilo_user_id": { + "name": "IDX_kilocode_users_blocked_by_kilo_user_id", + "columns": [ + { + "expression": "blocked_by_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_openrouter_upstream_safety_identifier": { + "name": "UQ_kilocode_users_openrouter_upstream_safety_identifier", + "columns": [ + { + "expression": "openrouter_upstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"openrouter_upstream_safety_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_openrouter_downstream_safety_identifier": { + "name": "UQ_kilocode_users_openrouter_downstream_safety_identifier", + "columns": [ + { + "expression": "openrouter_downstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"openrouter_downstream_safety_identifier\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_vercel_downstream_safety_identifier": { + "name": "UQ_kilocode_users_vercel_downstream_safety_identifier", + "columns": [ + { + "expression": "vercel_downstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"vercel_downstream_safety_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_normalized_email": { + "name": "IDX_kilocode_users_normalized_email", + "columns": [ + { + "expression": "normalized_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_email_domain": { + "name": "IDX_kilocode_users_email_domain", + "columns": [ + { + "expression": "email_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilocode_users_app_store_account_token_unique": { + "name": "kilocode_users_app_store_account_token_unique", + "nullsNotDistinct": false, + "columns": [ + "app_store_account_token" + ] + }, + "UQ_b1afacbcf43f2c7c4cb9f7e7faa": { + "name": "UQ_b1afacbcf43f2c7c4cb9f7e7faa", + "nullsNotDistinct": false, + "columns": [ + "google_user_email" + ] + } + }, + "policies": {}, + "checkConstraints": { + "blocked_reason_not_empty": { + "name": "blocked_reason_not_empty", + "value": "length(blocked_reason) > 0" + }, + "kilocode_users_is_super_admin_requires_admin_check": { + "name": "kilocode_users_is_super_admin_requires_admin_check", + "value": "NOT \"kilocode_users\".\"is_super_admin\" OR \"kilocode_users\".\"is_admin\"" + }, + "kilocode_users_can_view_sessions_requires_admin_check": { + "name": "kilocode_users_can_view_sessions_requires_admin_check", + "value": "NOT \"kilocode_users\".\"can_view_sessions\" OR \"kilocode_users\".\"is_admin\"" + }, + "kilocode_users_can_manage_credits_requires_admin_check": { + "name": "kilocode_users_can_manage_credits_requires_admin_check", + "value": "NOT \"kilocode_users\".\"can_manage_credits\" OR \"kilocode_users\".\"is_admin\"" + } + }, + "isRLSEnabled": false + }, + "public.magic_link_tokens": { + "name": "magic_link_tokens", + "schema": "", + "columns": { + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reserved_until": { + "name": "reserved_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'magic_link'" + }, + "challenge_id": { + "name": "challenge_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_magic_link_tokens_email": { + "name": "idx_magic_link_tokens_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_magic_link_tokens_expires_at": { + "name": "idx_magic_link_tokens_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_magic_link_tokens_challenge_id": { + "name": "UQ_magic_link_tokens_challenge_id", + "columns": [ + { + "expression": "challenge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"magic_link_tokens\".\"challenge_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_expires_at_future": { + "name": "check_expires_at_future", + "value": "\"magic_link_tokens\".\"expires_at\" > \"magic_link_tokens\".\"created_at\"" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_assignments": { + "name": "mcp_gateway_assignments", + "schema": "", + "columns": { + "assignment_id": { + "name": "assignment_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by_kilo_user_id": { + "name": "assigned_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "single_user_slot": { + "name": "single_user_slot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_assignments_active": { + "name": "UQ_mcp_gateway_assignments_active", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_assignments\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_mcp_gateway_assignments_single_user_slot": { + "name": "UQ_mcp_gateway_assignments_single_user_slot", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "single_user_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_assignments\".\"revoked_at\" is null and \"mcp_gateway_assignments\".\"single_user_slot\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_assignments_config": { + "name": "IDX_mcp_gateway_assignments_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_assignments_user": { + "name": "IDX_mcp_gateway_assignments_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_assignments_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_assignments_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_assignments", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_assignments_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_assignments_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_assignments", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_assignments_assigned_by_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_assignments_assigned_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_assignments", + "tableTo": "kilocode_users", + "columnsFrom": [ + "assigned_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_gateway_audit_events": { + "name": "mcp_gateway_audit_events", + "schema": "", + "columns": { + "audit_event_id": { + "name": "audit_event_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "actor_kilo_user_id": { + "name": "actor_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connect_resource_id": { + "name": "connect_resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_metadata": { + "name": "correlation_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_mcp_gateway_audit_events_config": { + "name": "IDX_mcp_gateway_audit_events_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_audit_events_grant": { + "name": "IDX_mcp_gateway_audit_events_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_audit_events\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_audit_events_owner": { + "name": "IDX_mcp_gateway_audit_events_owner", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_audit_events_created_at": { + "name": "IDX_mcp_gateway_audit_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_audit_events_actor_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_audit_events_actor_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "actor_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_audit_events_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk": { + "name": "mcp_gateway_audit_events_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_connect_resources", + "columnsFrom": [ + "connect_resource_id" + ], + "columnsTo": [ + "connect_resource_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_audit_events_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_gateway_audit_events_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_audit_events_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_audit_events", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_audit_events_owner_scope": { + "name": "mcp_gateway_audit_events_owner_scope", + "value": "\"mcp_gateway_audit_events\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_audit_events_outcome": { + "name": "mcp_gateway_audit_events_outcome", + "value": "\"mcp_gateway_audit_events\".\"outcome\" IN ('success', 'failure', 'blocked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_authorization_codes": { + "name": "mcp_gateway_authorization_codes", + "schema": "", + "columns": { + "authorization_code_id": { + "name": "authorization_code_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_request_id": { + "name": "authorization_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'S256'" + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_authorization_codes_code_hash": { + "name": "UQ_mcp_gateway_authorization_codes_code_hash", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_codes_expires_at": { + "name": "IDX_mcp_gateway_authorization_codes_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_codes_client": { + "name": "IDX_mcp_gateway_authorization_codes_client", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_codes_grant": { + "name": "IDX_mcp_gateway_authorization_codes_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_authorization_codes\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_authorization_codes_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk": { + "name": "mcp_gateway_authorization_codes_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_authorization_requests", + "columnsFrom": [ + "authorization_request_id" + ], + "columnsTo": [ + "authorization_request_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_authorization_codes_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_authorization_codes_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_authorization_codes_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_authorization_codes_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_codes_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_authorization_codes_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_authorization_codes", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_authorization_codes_owner_scope": { + "name": "mcp_gateway_authorization_codes_owner_scope", + "value": "\"mcp_gateway_authorization_codes\".\"owner_scope\" IN ('personal', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_authorization_requests": { + "name": "mcp_gateway_authorization_requests", + "schema": "", + "columns": { + "authorization_request_id": { + "name": "authorization_request_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "request_state_hash": { + "name": "request_state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_scopes": { + "name": "requested_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "oauth_state": { + "name": "oauth_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'S256'" + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_status": { + "name": "request_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_authorization_requests_state_hash": { + "name": "UQ_mcp_gateway_authorization_requests_state_hash", + "columns": [ + { + "expression": "request_state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_config": { + "name": "IDX_mcp_gateway_authorization_requests_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_grant": { + "name": "IDX_mcp_gateway_authorization_requests_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_authorization_requests\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_user": { + "name": "IDX_mcp_gateway_authorization_requests_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_authorization_requests_expires_at": { + "name": "IDX_mcp_gateway_authorization_requests_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_authorization_requests_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_authorization_requests_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_authorization_requests_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_authorization_requests_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_authorization_requests_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_authorization_requests_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_authorization_requests_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_authorization_requests", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_authorization_requests_owner_scope": { + "name": "mcp_gateway_authorization_requests_owner_scope", + "value": "\"mcp_gateway_authorization_requests\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_authorization_requests_status": { + "name": "mcp_gateway_authorization_requests_status", + "value": "\"mcp_gateway_authorization_requests\".\"request_status\" IN ('pending', 'completed', 'error')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_config_secrets": { + "name": "mcp_gateway_config_secrets", + "schema": "", + "columns": { + "config_secret_id": { + "name": "config_secret_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "secret_kind": { + "name": "secret_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_secret": { + "name": "encrypted_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_version": { + "name": "secret_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_config_secrets_active_kind": { + "name": "UQ_mcp_gateway_config_secrets_active_kind", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_config_secrets\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_config_secrets_config": { + "name": "IDX_mcp_gateway_config_secrets_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_config_secrets_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_config_secrets_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_config_secrets", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_config_secrets_version_positive": { + "name": "mcp_gateway_config_secrets_version_positive", + "value": "\"mcp_gateway_config_secrets\".\"secret_version\" > 0" + }, + "mcp_gateway_config_secrets_kind": { + "name": "mcp_gateway_config_secrets_kind", + "value": "\"mcp_gateway_config_secrets\".\"secret_kind\" IN ('static_provider_credentials', 'dynamic_registration', 'static_headers')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_configs": { + "name": "mcp_gateway_configs", + "schema": "", + "columns": { + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_mode": { + "name": "auth_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sharing_mode": { + "name": "sharing_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_scopes": { + "name": "provider_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_scope_source": { + "name": "provider_scope_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "provider_resource": { + "name": "provider_resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "path_passthrough": { + "name": "path_passthrough", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "discovered_provider_metadata": { + "name": "discovered_provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "registry_metadata": { + "name": "registry_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "auxiliary_headers": { + "name": "auxiliary_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_mcp_gateway_configs_owner": { + "name": "IDX_mcp_gateway_configs_owner", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_configs_enabled": { + "name": "IDX_mcp_gateway_configs_enabled", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_configs_remote_url": { + "name": "IDX_mcp_gateway_configs_remote_url", + "columns": [ + { + "expression": "remote_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_configs_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_configs_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_configs_name_not_empty": { + "name": "mcp_gateway_configs_name_not_empty", + "value": "length(trim(\"mcp_gateway_configs\".\"name\")) > 0" + }, + "mcp_gateway_configs_config_version_positive": { + "name": "mcp_gateway_configs_config_version_positive", + "value": "\"mcp_gateway_configs\".\"config_version\" > 0" + }, + "mcp_gateway_configs_personal_single_user": { + "name": "mcp_gateway_configs_personal_single_user", + "value": "\"mcp_gateway_configs\".\"owner_scope\" <> 'personal' OR \"mcp_gateway_configs\".\"sharing_mode\" = 'single_user'" + }, + "mcp_gateway_configs_owner_scope": { + "name": "mcp_gateway_configs_owner_scope", + "value": "\"mcp_gateway_configs\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_configs_auth_mode": { + "name": "mcp_gateway_configs_auth_mode", + "value": "\"mcp_gateway_configs\".\"auth_mode\" IN ('none', 'static_headers', 'oauth_dynamic', 'oauth_static')" + }, + "mcp_gateway_configs_sharing_mode": { + "name": "mcp_gateway_configs_sharing_mode", + "value": "\"mcp_gateway_configs\".\"sharing_mode\" IN ('single_user', 'multi_user')" + }, + "mcp_gateway_configs_provider_scope_source": { + "name": "mcp_gateway_configs_provider_scope_source", + "value": "\"mcp_gateway_configs\".\"provider_scope_source\" IN ('none', 'discovered', 'override')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_connect_resources": { + "name": "mcp_gateway_connect_resources", + "schema": "", + "columns": { + "connect_resource_id": { + "name": "connect_resource_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_url": { + "name": "canonical_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_status": { + "name": "route_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "route_version": { + "name": "route_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_connect_resources_route_key": { + "name": "UQ_mcp_gateway_connect_resources_route_key", + "columns": [ + { + "expression": "route_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_mcp_gateway_connect_resources_active_config": { + "name": "UQ_mcp_gateway_connect_resources_active_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_connect_resources\".\"route_status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connect_resources_config": { + "name": "IDX_mcp_gateway_connect_resources_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connect_resources_canonical_url": { + "name": "IDX_mcp_gateway_connect_resources_canonical_url", + "columns": [ + { + "expression": "canonical_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_connect_resources_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_connect_resources_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_connect_resources", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_connect_resources_route_key_format": { + "name": "mcp_gateway_connect_resources_route_key_format", + "value": "\"mcp_gateway_connect_resources\".\"route_key\" ~ '^[A-Za-z0-9_-]{32,}$'" + }, + "mcp_gateway_connect_resources_route_version_positive": { + "name": "mcp_gateway_connect_resources_route_version_positive", + "value": "\"mcp_gateway_connect_resources\".\"route_version\" > 0" + }, + "mcp_gateway_connect_resources_owner_scope": { + "name": "mcp_gateway_connect_resources_owner_scope", + "value": "\"mcp_gateway_connect_resources\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_connect_resources_route_status": { + "name": "mcp_gateway_connect_resources_route_status", + "value": "\"mcp_gateway_connect_resources\".\"route_status\" IN ('active', 'rotated', 'revoked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_connection_instances": { + "name": "mcp_gateway_connection_instances", + "schema": "", + "columns": { + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_status": { + "name": "instance_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "instance_version": { + "name": "instance_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_connection_instances_non_terminal": { + "name": "UQ_mcp_gateway_connection_instances_non_terminal", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_connection_instances\".\"instance_status\" IN ('active', 'needs_reauth')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connection_instances_config": { + "name": "IDX_mcp_gateway_connection_instances_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_connection_instances_user": { + "name": "IDX_mcp_gateway_connection_instances_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_connection_instances_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_connection_instances_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_connection_instances", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_connection_instances_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_connection_instances_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_connection_instances", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_connection_instances_version_positive": { + "name": "mcp_gateway_connection_instances_version_positive", + "value": "\"mcp_gateway_connection_instances\".\"instance_version\" > 0" + }, + "mcp_gateway_connection_instances_owner_scope": { + "name": "mcp_gateway_connection_instances_owner_scope", + "value": "\"mcp_gateway_connection_instances\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_connection_instances_status": { + "name": "mcp_gateway_connection_instances_status", + "value": "\"mcp_gateway_connection_instances\".\"instance_status\" IN ('active', 'needs_reauth', 'revoked', 'removed')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_oauth_clients": { + "name": "mcp_gateway_oauth_clients", + "schema": "", + "columns": { + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_token_hash": { + "name": "registration_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret_hash": { + "name": "client_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "declared_scopes": { + "name": "declared_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "registration_access_token_expires_at": { + "name": "registration_access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_oauth_clients_client_id": { + "name": "UQ_mcp_gateway_oauth_clients_client_id", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_mcp_gateway_oauth_clients_registration_token_hash": { + "name": "UQ_mcp_gateway_oauth_clients_registration_token_hash", + "columns": [ + { + "expression": "registration_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_clients_deleted_at": { + "name": "IDX_mcp_gateway_oauth_clients_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_oauth_clients_client_id_format": { + "name": "mcp_gateway_oauth_clients_client_id_format", + "value": "\"mcp_gateway_oauth_clients\".\"client_id\" ~ '^[A-Za-z0-9._-]+:[A-Za-z0-9._-]+$'" + }, + "mcp_gateway_oauth_clients_auth_method": { + "name": "mcp_gateway_oauth_clients_auth_method", + "value": "\"mcp_gateway_oauth_clients\".\"token_endpoint_auth_method\" IN ('none', 'client_secret_post', 'client_secret_basic')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_oauth_grants": { + "name": "mcp_gateway_oauth_grants", + "schema": "", + "columns": { + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connect_resource_id": { + "name": "connect_resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grant_status": { + "name": "grant_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_oauth_grants_active_binding": { + "name": "UQ_mcp_gateway_oauth_grants_active_binding", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connect_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "redirect_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_oauth_grants\".\"revoked_at\" is null and \"mcp_gateway_oauth_grants\".\"grant_status\" in ('pending', 'active')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_client": { + "name": "IDX_mcp_gateway_oauth_grants_client", + "columns": [ + { + "expression": "oauth_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_user": { + "name": "IDX_mcp_gateway_oauth_grants_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_config": { + "name": "IDX_mcp_gateway_oauth_grants_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_owner": { + "name": "IDX_mcp_gateway_oauth_grants_owner", + "columns": [ + { + "expression": "owner_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_resource": { + "name": "IDX_mcp_gateway_oauth_grants_resource", + "columns": [ + { + "expression": "connect_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_instance": { + "name": "IDX_mcp_gateway_oauth_grants_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_oauth_grants_revoked_at": { + "name": "IDX_mcp_gateway_oauth_grants_revoked_at", + "columns": [ + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_oauth_grants_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_oauth_grants_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_oauth_grants_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_oauth_grants_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk": { + "name": "mcp_gateway_oauth_grants_connect_resource_id_mcp_gateway_connect_resources_connect_resource_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_connect_resources", + "columnsFrom": [ + "connect_resource_id" + ], + "columnsTo": [ + "connect_resource_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_oauth_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_oauth_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_oauth_grants", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_oauth_grants_config_version_positive": { + "name": "mcp_gateway_oauth_grants_config_version_positive", + "value": "\"mcp_gateway_oauth_grants\".\"config_version\" > 0" + }, + "mcp_gateway_oauth_grants_owner_scope": { + "name": "mcp_gateway_oauth_grants_owner_scope", + "value": "\"mcp_gateway_oauth_grants\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_oauth_grants_status": { + "name": "mcp_gateway_oauth_grants_status", + "value": "\"mcp_gateway_oauth_grants\".\"grant_status\" IN ('pending', 'active', 'revoked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_pending_provider_authorizations": { + "name": "mcp_gateway_pending_provider_authorizations", + "schema": "", + "columns": { + "pending_provider_authorization_id": { + "name": "pending_provider_authorization_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_request_id": { + "name": "authorization_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_mode": { + "name": "auth_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_authorization_endpoint": { + "name": "provider_authorization_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_token_endpoint": { + "name": "provider_token_endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_state": { + "name": "encrypted_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "config_version": { + "name": "config_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pending_status": { + "name": "pending_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_pending_provider_authorizations_state_hash": { + "name": "UQ_mcp_gateway_pending_provider_authorizations_state_hash", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_pending_provider_authorizations_config": { + "name": "IDX_mcp_gateway_pending_provider_authorizations_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_pending_provider_authorizations_grant": { + "name": "IDX_mcp_gateway_pending_provider_authorizations_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_pending_provider_authorizations\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_pending_provider_authorizations_expires_at": { + "name": "IDX_mcp_gateway_pending_provider_authorizations_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_pending_provider_authorizations_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_authorization_request_id_mcp_gateway_authorization_requests_authorization_request_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_authorization_requests", + "columnsFrom": [ + "authorization_request_id" + ], + "columnsTo": [ + "authorization_request_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_pending_provider_authorizations_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_pending_provider_authorizations_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_pending_provider_authorizations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_pending_provider_authorizations_config_version_positive": { + "name": "mcp_gateway_pending_provider_authorizations_config_version_positive", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"config_version\" > 0" + }, + "mcp_gateway_pending_provider_authorizations_owner_scope": { + "name": "mcp_gateway_pending_provider_authorizations_owner_scope", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"owner_scope\" IN ('personal', 'organization')" + }, + "mcp_gateway_pending_provider_authorizations_auth_mode": { + "name": "mcp_gateway_pending_provider_authorizations_auth_mode", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"auth_mode\" IN ('none', 'static_headers', 'oauth_dynamic', 'oauth_static')" + }, + "mcp_gateway_pending_provider_authorizations_status": { + "name": "mcp_gateway_pending_provider_authorizations_status", + "value": "\"mcp_gateway_pending_provider_authorizations\".\"pending_status\" IN ('pending', 'completed', 'error')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_provider_grants": { + "name": "mcp_gateway_provider_grants", + "schema": "", + "columns": { + "provider_grant_id": { + "name": "provider_grant_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "encrypted_grant": { + "name": "encrypted_grant", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subject": { + "name": "provider_subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_scope": { + "name": "grant_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "grant_status": { + "name": "grant_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "grant_version": { + "name": "grant_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_provider_grants_active_instance": { + "name": "UQ_mcp_gateway_provider_grants_active_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_gateway_provider_grants\".\"grant_status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_provider_grants_instance": { + "name": "IDX_mcp_gateway_provider_grants_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_provider_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_provider_grants_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_provider_grants", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_provider_grants_version_positive": { + "name": "mcp_gateway_provider_grants_version_positive", + "value": "\"mcp_gateway_provider_grants\".\"grant_version\" > 0" + }, + "mcp_gateway_provider_grants_status": { + "name": "mcp_gateway_provider_grants_status", + "value": "\"mcp_gateway_provider_grants\".\"grant_status\" IN ('active', 'revoked')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_rate_limit_windows": { + "name": "mcp_gateway_rate_limit_windows", + "schema": "", + "columns": { + "rate_limit_window_id": { + "name": "rate_limit_window_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "ip_hash": { + "name": "ip_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_started_at": { + "name": "window_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_rate_limit_windows_ip_window": { + "name": "UQ_mcp_gateway_rate_limit_windows_ip_window", + "columns": [ + { + "expression": "ip_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_rate_limit_windows_window": { + "name": "IDX_mcp_gateway_rate_limit_windows_window", + "columns": [ + { + "expression": "window_started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_rate_limit_windows_attempt_count_non_negative": { + "name": "mcp_gateway_rate_limit_windows_attempt_count_non_negative", + "value": "\"mcp_gateway_rate_limit_windows\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.mcp_gateway_refresh_tokens": { + "name": "mcp_gateway_refresh_tokens", + "schema": "", + "columns": { + "refresh_token_id": { + "name": "refresh_token_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rotated_from_refresh_token_id": { + "name": "rotated_from_refresh_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "oauth_grant_id": { + "name": "oauth_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "route_key": { + "name": "route_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_resource_url": { + "name": "canonical_resource_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "execution_context": { + "name": "execution_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_mcp_gateway_refresh_tokens_token_hash": { + "name": "UQ_mcp_gateway_refresh_tokens_token_hash", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_user": { + "name": "IDX_mcp_gateway_refresh_tokens_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_grant": { + "name": "IDX_mcp_gateway_refresh_tokens_grant", + "columns": [ + { + "expression": "oauth_grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_gateway_refresh_tokens\".\"oauth_grant_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_config": { + "name": "IDX_mcp_gateway_refresh_tokens_config", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_mcp_gateway_refresh_tokens_consumed_at": { + "name": "IDX_mcp_gateway_refresh_tokens_consumed_at", + "columns": [ + { + "expression": "consumed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_gateway_refresh_tokens_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk": { + "name": "mcp_gateway_refresh_tokens_oauth_client_id_mcp_gateway_oauth_clients_oauth_client_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_oauth_clients", + "columnsFrom": [ + "oauth_client_id" + ], + "columnsTo": [ + "oauth_client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk": { + "name": "mcp_gateway_refresh_tokens_oauth_grant_id_mcp_gateway_oauth_grants_oauth_grant_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_oauth_grants", + "columnsFrom": [ + "oauth_grant_id" + ], + "columnsTo": [ + "oauth_grant_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_config_id_mcp_gateway_configs_config_id_fk": { + "name": "mcp_gateway_refresh_tokens_config_id_mcp_gateway_configs_config_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "config_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_kilo_user_id_kilocode_users_id_fk": { + "name": "mcp_gateway_refresh_tokens_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_gateway_refresh_tokens_instance_id_mcp_gateway_connection_instances_instance_id_fk": { + "name": "mcp_gateway_refresh_tokens_instance_id_mcp_gateway_connection_instances_instance_id_fk", + "tableFrom": "mcp_gateway_refresh_tokens", + "tableTo": "mcp_gateway_connection_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "instance_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_gateway_refresh_tokens_owner_scope": { + "name": "mcp_gateway_refresh_tokens_owner_scope", + "value": "\"mcp_gateway_refresh_tokens\".\"owner_scope\" IN ('personal', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.microdollar_usage": { + "name": "microdollar_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_write_tokens": { + "name": "cache_write_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_hit_tokens": { + "name": "cache_hit_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_model": { + "name": "requested_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_discount": { + "name": "cache_discount", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "abuse_classification": { + "name": "abuse_classification", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inference_provider": { + "name": "inference_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_created_at": { + "name": "idx_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_abuse_classification": { + "name": "idx_abuse_classification", + "columns": [ + { + "expression": "abuse_classification", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_kilo_user_id_created_at2": { + "name": "idx_kilo_user_id_created_at2", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_organization_id": { + "name": "idx_microdollar_usage_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"microdollar_usage\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.microdollar_usage_daily": { + "name": "microdollar_usage_daily", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_microdollar_usage_daily_personal": { + "name": "idx_microdollar_usage_daily_personal", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"microdollar_usage_daily\".\"organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_daily_org": { + "name": "idx_microdollar_usage_daily_org", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"microdollar_usage_daily\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.microdollar_usage_daily_repairs": { + "name": "microdollar_usage_daily_repairs", + "schema": "", + "columns": { + "usage_id": { + "name": "usage_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_token": { + "name": "claim_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_microdollar_usage_daily_repairs_claim": { + "name": "IDX_microdollar_usage_daily_repairs_claim", + "columns": [ + { + "expression": "attempt_count", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "microdollar_usage_daily_repairs_usage_id_microdollar_usage_id_fk": { + "name": "microdollar_usage_daily_repairs_usage_id_microdollar_usage_id_fk", + "tableFrom": "microdollar_usage_daily_repairs", + "tableTo": "microdollar_usage", + "columnsFrom": [ + "usage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "microdollar_usage_daily_repairs_attempt_count_check": { + "name": "microdollar_usage_daily_repairs_attempt_count_check", + "value": "\"microdollar_usage_daily_repairs\".\"attempt_count\" >= 0" + }, + "microdollar_usage_daily_repairs_claim_token_check": { + "name": "microdollar_usage_daily_repairs_claim_token_check", + "value": "(\"microdollar_usage_daily_repairs\".\"claimed_at\" IS NULL AND \"microdollar_usage_daily_repairs\".\"claim_token\" IS NULL) OR (\"microdollar_usage_daily_repairs\".\"claimed_at\" IS NOT NULL AND \"microdollar_usage_daily_repairs\".\"claim_token\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.microdollar_usage_metadata": { + "name": "microdollar_usage_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "http_user_agent_id": { + "name": "http_user_agent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "http_ip_id": { + "name": "http_ip_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_city_id": { + "name": "vercel_ip_city_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_country_id": { + "name": "vercel_ip_country_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_latitude": { + "name": "vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_longitude": { + "name": "vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "ja4_digest_id": { + "name": "ja4_digest_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_prompt_prefix": { + "name": "user_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt_prefix_id": { + "name": "system_prompt_prefix_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "system_prompt_length": { + "name": "system_prompt_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_tokens": { + "name": "max_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_middle_out_transform": { + "name": "has_middle_out_transform", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "upstream_id": { + "name": "upstream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finish_reason_id": { + "name": "finish_reason_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency": { + "name": "latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "moderation_latency": { + "name": "moderation_latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "generation_time": { + "name": "generation_time", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_byok": { + "name": "is_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_user_byok": { + "name": "is_user_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "streamed": { + "name": "streamed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancelled": { + "name": "cancelled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "editor_name_id": { + "name": "editor_name_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "api_kind_id": { + "name": "api_kind_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "has_tools": { + "name": "has_tools", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode_id": { + "name": "mode_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_model_id": { + "name": "auto_model_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "market_cost": { + "name": "market_cost", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "abuse_delay": { + "name": "abuse_delay", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "abuse_downgraded_from": { + "name": "abuse_downgraded_from", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_microdollar_usage_metadata_created_at": { + "name": "idx_microdollar_usage_metadata_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_metadata_session_id": { + "name": "idx_microdollar_usage_metadata_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"microdollar_usage_metadata\".\"session_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "microdollar_usage_metadata_http_user_agent_id_http_user_agent_http_user_agent_id_fk": { + "name": "microdollar_usage_metadata_http_user_agent_id_http_user_agent_http_user_agent_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "http_user_agent", + "columnsFrom": [ + "http_user_agent_id" + ], + "columnsTo": [ + "http_user_agent_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_http_ip_id_http_ip_http_ip_id_fk": { + "name": "microdollar_usage_metadata_http_ip_id_http_ip_http_ip_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "http_ip", + "columnsFrom": [ + "http_ip_id" + ], + "columnsTo": [ + "http_ip_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_vercel_ip_city_id_vercel_ip_city_vercel_ip_city_id_fk": { + "name": "microdollar_usage_metadata_vercel_ip_city_id_vercel_ip_city_vercel_ip_city_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "vercel_ip_city", + "columnsFrom": [ + "vercel_ip_city_id" + ], + "columnsTo": [ + "vercel_ip_city_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_vercel_ip_country_id_vercel_ip_country_vercel_ip_country_id_fk": { + "name": "microdollar_usage_metadata_vercel_ip_country_id_vercel_ip_country_vercel_ip_country_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "vercel_ip_country", + "columnsFrom": [ + "vercel_ip_country_id" + ], + "columnsTo": [ + "vercel_ip_country_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_ja4_digest_id_ja4_digest_ja4_digest_id_fk": { + "name": "microdollar_usage_metadata_ja4_digest_id_ja4_digest_ja4_digest_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "ja4_digest", + "columnsFrom": [ + "ja4_digest_id" + ], + "columnsTo": [ + "ja4_digest_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_system_prompt_prefix_id_system_prompt_prefix_system_prompt_prefix_id_fk": { + "name": "microdollar_usage_metadata_system_prompt_prefix_id_system_prompt_prefix_system_prompt_prefix_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "system_prompt_prefix", + "columnsFrom": [ + "system_prompt_prefix_id" + ], + "columnsTo": [ + "system_prompt_prefix_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mode": { + "name": "mode", + "schema": "", + "columns": { + "mode_id": { + "name": "mode_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_mode": { + "name": "UQ_mode", + "columns": [ + { + "expression": "mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_stats": { + "name": "model_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "is_featured": { + "name": "is_featured", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_stealth": { + "name": "is_stealth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_recommended": { + "name": "is_recommended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "openrouter_id": { + "name": "openrouter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aa_slug": { + "name": "aa_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_creator": { + "name": "model_creator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_slug": { + "name": "creator_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_date": { + "name": "release_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "price_input": { + "name": "price_input", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "price_output": { + "name": "price_output", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "coding_index": { + "name": "coding_index", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "speed_tokens_per_sec": { + "name": "speed_tokens_per_sec", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_output_tokens": { + "name": "max_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "openrouter_data": { + "name": "openrouter_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "benchmarks": { + "name": "benchmarks", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "chart_data": { + "name": "chart_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_stats_openrouter_id": { + "name": "IDX_model_stats_openrouter_id", + "columns": [ + { + "expression": "openrouter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_slug": { + "name": "IDX_model_stats_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_is_active": { + "name": "IDX_model_stats_is_active", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_creator_slug": { + "name": "IDX_model_stats_creator_slug", + "columns": [ + { + "expression": "creator_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_price_input": { + "name": "IDX_model_stats_price_input", + "columns": [ + { + "expression": "price_input", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_coding_index": { + "name": "IDX_model_stats_coding_index", + "columns": [ + { + "expression": "coding_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_context_length": { + "name": "IDX_model_stats_context_length", + "columns": [ + { + "expression": "context_length", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_stats_openrouter_id_unique": { + "name": "model_stats_openrouter_id_unique", + "nullsNotDistinct": false, + "columns": [ + "openrouter_id" + ] + }, + "model_stats_slug_unique": { + "name": "model_stats_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_eval_ingestions": { + "name": "model_eval_ingestions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bench_eval_name": { + "name": "bench_eval_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bench_eval_url": { + "name": "bench_eval_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_stats_id": { + "name": "model_stats_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "variant": { + "name": "variant", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_source": { + "name": "task_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "n_total_trials": { + "name": "n_total_trials", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "n_attempts": { + "name": "n_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_score": { + "name": "total_score", + "type": "numeric(14, 6)", + "primaryKey": false, + "notNull": true + }, + "overall_score": { + "name": "overall_score", + "type": "numeric(12, 8)", + "primaryKey": false, + "notNull": true + }, + "n_errored": { + "name": "n_errored", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "avg_cost_microdollars": { + "name": "avg_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_input_tokens": { + "name": "avg_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_output_tokens": { + "name": "avg_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_cache_read_tokens": { + "name": "avg_cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_cache_read_tokens": { + "name": "total_cache_read_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_execution_ms": { + "name": "avg_execution_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "promoted_at": { + "name": "promoted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "promoted_by_email": { + "name": "promoted_by_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "promotion_note": { + "name": "promotion_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_eval_ingestions_lookup": { + "name": "IDX_model_eval_ingestions_lookup", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "promoted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_eval_ingestions_model_stats": { + "name": "IDX_model_eval_ingestions_model_stats", + "columns": [ + { + "expression": "model_stats_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_eval_ingestions_promoted_by_email_lower": { + "name": "IDX_model_eval_ingestions_promoted_by_email_lower", + "columns": [ + { + "expression": "LOWER(\"promoted_by_email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_eval_ingestions_model_stats_id_model_stats_id_fk": { + "name": "model_eval_ingestions_model_stats_id_model_stats_id_fk", + "tableFrom": "model_eval_ingestions", + "tableTo": "model_stats", + "columnsFrom": [ + "model_stats_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_eval_ingestions_bench_eval_name_unique": { + "name": "model_eval_ingestions_bench_eval_name_unique", + "nullsNotDistinct": false, + "columns": [ + "bench_eval_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_experiment": { + "name": "model_experiment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "public_model_id": { + "name": "public_model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_model_experiment_public_model_id_routing": { + "name": "UQ_model_experiment_public_model_id_routing", + "columns": [ + { + "expression": "public_model_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"model_experiment\".\"status\" IN ('active', 'paused')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_experiment_status": { + "name": "IDX_model_experiment_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_created_by_user_id_kilocode_users_id_fk": { + "name": "model_experiment_created_by_user_id_kilocode_users_id_fk", + "tableFrom": "model_experiment", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "model_experiment_status_valid": { + "name": "model_experiment_status_valid", + "value": "\"model_experiment\".\"status\" IN ('draft', 'active', 'paused', 'completed')" + }, + "model_experiment_active_not_archived": { + "name": "model_experiment_active_not_archived", + "value": "\"model_experiment\".\"status\" <> 'active' OR \"model_experiment\".\"is_archived\" = false" + } + }, + "isRLSEnabled": false + }, + "public.model_experiment_request": { + "name": "model_experiment_request", + "schema": "", + "columns": { + "usage_id": { + "name": "usage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "variant_version_id": { + "name": "variant_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_subject": { + "name": "allocation_subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_request_id": { + "name": "client_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_kind": { + "name": "request_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_body_sha256": { + "name": "request_body_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "was_truncated": { + "name": "was_truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_experiment_request_variant_version_created_at": { + "name": "IDX_model_experiment_request_variant_version_created_at", + "columns": [ + { + "expression": "variant_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_experiment_request_client_request_id": { + "name": "IDX_model_experiment_request_client_request_id", + "columns": [ + { + "expression": "client_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"model_experiment_request\".\"client_request_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_request_usage_id_microdollar_usage_id_fk": { + "name": "model_experiment_request_usage_id_microdollar_usage_id_fk", + "tableFrom": "model_experiment_request", + "tableTo": "microdollar_usage", + "columnsFrom": [ + "usage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_experiment_request_variant_version_id_model_experiment_variant_version_id_fk": { + "name": "model_experiment_request_variant_version_id_model_experiment_variant_version_id_fk", + "tableFrom": "model_experiment_request", + "tableTo": "model_experiment_variant_version", + "columnsFrom": [ + "variant_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "model_experiment_request_usage_id_created_at_pk": { + "name": "model_experiment_request_usage_id_created_at_pk", + "columns": [ + "usage_id", + "created_at" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "model_experiment_request_allocation_subject_valid": { + "name": "model_experiment_request_allocation_subject_valid", + "value": "\"model_experiment_request\".\"allocation_subject\" IN ('user', 'machine', 'ip')" + }, + "model_experiment_request_request_kind_valid": { + "name": "model_experiment_request_request_kind_valid", + "value": "\"model_experiment_request\".\"request_kind\" IN ('chat_completions', 'messages', 'responses')" + }, + "model_experiment_request_request_body_sha256_format": { + "name": "model_experiment_request_request_body_sha256_format", + "value": "\"model_experiment_request\".\"request_body_sha256\" ~ '^[0-9a-f]{64}$' OR \"model_experiment_request\".\"request_body_sha256\" IN ('__failed__', '__deleted__')" + } + }, + "isRLSEnabled": false + }, + "public.model_experiment_variant": { + "name": "model_experiment_variant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "experiment_id": { + "name": "experiment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_experiment_variant_experiment_id": { + "name": "IDX_model_experiment_variant_experiment_id", + "columns": [ + { + "expression": "experiment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_variant_experiment_id_model_experiment_id_fk": { + "name": "model_experiment_variant_experiment_id_model_experiment_id_fk", + "tableFrom": "model_experiment_variant", + "tableTo": "model_experiment", + "columnsFrom": [ + "experiment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_model_experiment_variant_experiment_label": { + "name": "UQ_model_experiment_variant_experiment_label", + "nullsNotDistinct": false, + "columns": [ + "experiment_id", + "label" + ] + } + }, + "policies": {}, + "checkConstraints": { + "model_experiment_variant_weight_positive": { + "name": "model_experiment_variant_weight_positive", + "value": "\"model_experiment_variant\".\"weight\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.model_experiment_variant_version": { + "name": "model_experiment_variant_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "variant_id": { + "name": "variant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_experiment_variant_version_variant_effective": { + "name": "IDX_model_experiment_variant_version_variant_effective", + "columns": [ + { + "expression": "variant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effective_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_experiment_variant_version_variant_id_model_experiment_variant_id_fk": { + "name": "model_experiment_variant_version_variant_id_model_experiment_variant_id_fk", + "tableFrom": "model_experiment_variant_version", + "tableTo": "model_experiment_variant", + "columnsFrom": [ + "variant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_experiment_variant_version_created_by_kilocode_users_id_fk": { + "name": "model_experiment_variant_version_created_by_kilocode_users_id_fk", + "tableFrom": "model_experiment_variant_version", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.models_by_provider": { + "name": "models_by_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "openrouter": { + "name": "openrouter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "vercel": { + "name": "vercel", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.native_admission_challenges": { + "name": "native_admission_challenges", + "schema": "", + "columns": { + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_native_admission_challenges_expires_at": { + "name": "IDX_native_admission_challenges_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.native_attested_keys": { + "name": "native_attested_keys", + "schema": "", + "columns": { + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sign_count": { + "name": "sign_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attested_at": { + "name": "attested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_native_attested_keys_kilo_user_id": { + "name": "IDX_native_attested_keys_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "native_attested_keys_kilo_user_id_kilocode_users_id_fk": { + "name": "native_attested_keys_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "native_attested_keys", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "native_attested_keys_platform_check": { + "name": "native_attested_keys_platform_check", + "value": "\"native_attested_keys\".\"platform\" IN ('ios', 'android')" + } + }, + "isRLSEnabled": false + }, + "public.organization_audit_logs": { + "name": "organization_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_audit_logs_organization_id": { + "name": "IDX_organization_audit_logs_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_audit_logs_action": { + "name": "IDX_organization_audit_logs_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_audit_logs_actor_id": { + "name": "IDX_organization_audit_logs_actor_id", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_audit_logs_created_at": { + "name": "IDX_organization_audit_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_group_memberships": { + "name": "organization_group_memberships", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by_kilo_user_id": { + "name": "assigned_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_group_memberships_organization_user": { + "name": "IDX_organization_group_memberships_organization_user", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "FK_organization_group_memberships_group": { + "name": "FK_organization_group_memberships_group", + "tableFrom": "organization_group_memberships", + "tableTo": "organization_groups", + "columnsFrom": [ + "organization_id", + "group_id" + ], + "columnsTo": [ + "organization_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "FK_organization_group_memberships_member": { + "name": "FK_organization_group_memberships_member", + "tableFrom": "organization_group_memberships", + "tableTo": "organization_memberships", + "columnsFrom": [ + "organization_id", + "kilo_user_id" + ], + "columnsTo": [ + "organization_id", + "kilo_user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "PK_organization_group_memberships": { + "name": "PK_organization_group_memberships", + "columns": [ + "organization_id", + "group_id", + "kilo_user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_group_policy_settings": { + "name": "organization_group_policy_settings", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "default_policies": { + "name": "default_policies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[{\"type\":\"model_access\",\"data\":{\"mode\":\"all\"}}]'::jsonb" + }, + "policy_revision": { + "name": "policy_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "updated_by_kilo_user_id": { + "name": "updated_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_group_policy_settings_organization_id_organizations_id_fk": { + "name": "organization_group_policy_settings_organization_id_organizations_id_fk", + "tableFrom": "organization_group_policy_settings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_group_policy_settings_revision_check": { + "name": "organization_group_policy_settings_revision_check", + "value": "\"organization_group_policy_settings\".\"policy_revision\" >= 1" + } + }, + "isRLSEnabled": false + }, + "public.organization_groups": { + "name": "organization_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policies": { + "name": "policies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_organization_groups_organization_id_canonical_name": { + "name": "UQ_organization_groups_organization_id_canonical_name", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(btrim(\"name\"))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_groups_organization_id": { + "name": "IDX_organization_groups_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_groups_organization_id_organizations_id_fk": { + "name": "organization_groups_organization_id_organizations_id_fk", + "tableFrom": "organization_groups", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_groups_organization_id_id": { + "name": "UQ_organization_groups_organization_id_id", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organization_groups_name_check": { + "name": "organization_groups_name_check", + "value": "char_length(btrim(\"organization_groups\".\"name\")) BETWEEN 1 AND 80" + }, + "organization_groups_description_check": { + "name": "organization_groups_description_check", + "value": "\"organization_groups\".\"description\" IS NULL OR char_length(\"organization_groups\".\"description\") <= 500" + } + }, + "isRLSEnabled": false + }, + "public.organization_invitations": { + "name": "organization_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "authentication_requirement": { + "name": "authentication_requirement", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "sso_source_organization_id": { + "name": "sso_source_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_organization_invitations_token": { + "name": "UQ_organization_invitations_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_invitations_org_id": { + "name": "IDX_organization_invitations_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_invitations_email": { + "name": "IDX_organization_invitations_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_invitations_expires_at": { + "name": "IDX_organization_invitations_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_invitations_sso_source_organization_id_organizations_id_fk": { + "name": "organization_invitations_sso_source_organization_id_organizations_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "organizations", + "columnsFrom": [ + "sso_source_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_membership_removals": { + "name": "organization_membership_removals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "removed_by": { + "name": "removed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_role": { + "name": "previous_role", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_org_membership_removals_org_id": { + "name": "IDX_org_membership_removals_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_org_membership_removals_user_id": { + "name": "IDX_org_membership_removals_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_org_membership_removals_org_user": { + "name": "UQ_org_membership_removals_org_user", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_memberships": { + "name": "organization_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_memberships_org_id": { + "name": "IDX_organization_memberships_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_memberships_user_id": { + "name": "IDX_organization_memberships_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_memberships_org_user": { + "name": "UQ_organization_memberships_org_user", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_recommendation_dismissals": { + "name": "organization_recommendation_dismissals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recommendation_key": { + "name": "recommendation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_by_user_id": { + "name": "dismissed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_recommendation_dismissals_owned_by_organization_id_organizations_id_fk": { + "name": "organization_recommendation_dismissals_owned_by_organization_id_organizations_id_fk", + "tableFrom": "organization_recommendation_dismissals", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_recommendation_dismissals_dismissed_by_user_id_kilocode_users_id_fk": { + "name": "organization_recommendation_dismissals_dismissed_by_user_id_kilocode_users_id_fk", + "tableFrom": "organization_recommendation_dismissals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "dismissed_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_org_recommendation_dismissals_org_key": { + "name": "UQ_org_recommendation_dismissals_org_key", + "nullsNotDistinct": false, + "columns": [ + "owned_by_organization_id", + "recommendation_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_seats_purchases": { + "name": "organization_seats_purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subscription_stripe_id": { + "name": "subscription_stripe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seat_count": { + "name": "seat_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "subscription_status": { + "name": "subscription_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "billing_cycle": { + "name": "billing_cycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'monthly'" + } + }, + "indexes": { + "IDX_organization_seats_org_id": { + "name": "IDX_organization_seats_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_expires_at": { + "name": "IDX_organization_seats_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_created_at": { + "name": "IDX_organization_seats_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_updated_at": { + "name": "IDX_organization_seats_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_starts_at": { + "name": "IDX_organization_seats_starts_at", + "columns": [ + { + "expression": "starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_seats_idempotency_key": { + "name": "UQ_organization_seats_idempotency_key", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_user_limits": { + "name": "organization_user_limits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "limit_type": { + "name": "limit_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "microdollar_limit": { + "name": "microdollar_limit", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_user_limits_org_id": { + "name": "IDX_organization_user_limits_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_user_limits_user_id": { + "name": "IDX_organization_user_limits_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_user_limits_org_user": { + "name": "UQ_organization_user_limits_org_user", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id", + "limit_type" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_user_usage": { + "name": "organization_user_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "limit_type": { + "name": "limit_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "microdollar_usage": { + "name": "microdollar_usage", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_user_daily_usage_org_id": { + "name": "IDX_organization_user_daily_usage_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_user_daily_usage_user_id": { + "name": "IDX_organization_user_daily_usage_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_user_daily_usage_org_user_date": { + "name": "UQ_organization_user_daily_usage_org_user_date", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id", + "limit_type", + "usage_date" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "microdollars_used": { + "name": "microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "microdollars_balance": { + "name": "microdollars_balance", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_microdollars_acquired": { + "name": "total_microdollars_acquired", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "next_credit_expiration_at": { + "name": "next_credit_expiration_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_top_up_enabled": { + "name": "auto_top_up_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "seat_count": { + "name": "seat_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "require_seats": { + "name": "require_seats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sso_domain": { + "name": "sso_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_organization_id": { + "name": "parent_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'teams'" + }, + "free_trial_end_at": { + "name": "free_trial_end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "company_domain": { + "name": "company_domain", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_organizations_sso_domain": { + "name": "IDX_organizations_sso_domain", + "columns": [ + { + "expression": "sso_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organizations_parent_organization_id": { + "name": "IDX_organizations_parent_organization_id", + "columns": [ + { + "expression": "parent_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organizations_parent_organization_id_organizations_id_fk": { + "name": "organizations_parent_organization_id_organizations_id_fk", + "tableFrom": "organizations", + "tableTo": "organizations", + "columnsFrom": [ + "parent_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organizations_name_not_empty_check": { + "name": "organizations_name_not_empty_check", + "value": "length(trim(\"organizations\".\"name\")) > 0" + }, + "organizations_not_parented_by_self_check": { + "name": "organizations_not_parented_by_self_check", + "value": "\"organizations\".\"parent_organization_id\" IS NULL OR \"organizations\".\"parent_organization_id\" <> \"organizations\".\"id\"" + } + }, + "isRLSEnabled": false + }, + "public.organization_modes": { + "name": "organization_modes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "IDX_organization_modes_organization_id": { + "name": "IDX_organization_modes_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_modes_org_id_slug": { + "name": "UQ_organization_modes_org_id_slug", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_methods": { + "name": "payment_methods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_fingerprint": { + "name": "stripe_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_id": { + "name": "stripe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last4": { + "name": "last4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line1": { + "name": "address_line1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line2": { + "name": "address_line2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_city": { + "name": "address_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_state": { + "name": "address_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_zip": { + "name": "address_zip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "three_d_secure_supported": { + "name": "three_d_secure_supported", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "funding": { + "name": "funding", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "regulated_status": { + "name": "regulated_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line1_check_status": { + "name": "address_line1_check_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code_check_status": { + "name": "postal_code_check_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "eligible_for_free_credits": { + "name": "eligible_for_free_credits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_data": { + "name": "stripe_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_d7d7fb15569674aaadcfbc0428": { + "name": "IDX_d7d7fb15569674aaadcfbc0428", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_e1feb919d0ab8a36381d5d5138": { + "name": "IDX_e1feb919d0ab8a36381d5d5138", + "columns": [ + { + "expression": "stripe_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_payment_methods_organization_id": { + "name": "IDX_payment_methods_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_29df1b0403df5792c96bbbfdbe6": { + "name": "UQ_29df1b0403df5792c96bbbfdbe6", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "stripe_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_impact_sale_reversals": { + "name": "pending_impact_sale_reversals", + "schema": "", + "columns": { + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "dispute_id": { + "name": "dispute_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_date": { + "name": "event_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pending_impact_sale_reversals_attempt_count_non_negative_check": { + "name": "pending_impact_sale_reversals_attempt_count_non_negative_check", + "value": "\"pending_impact_sale_reversals\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.platform_access_token_credentials": { + "name": "platform_access_token_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "integration_type": { + "name": "integration_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_encrypted": { + "name": "token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provider_credential_type": { + "name": "provider_credential_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_resource_id": { + "name": "provider_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_base_url": { + "name": "provider_base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorized_by_user_id": { + "name": "authorized_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "provider_scopes": { + "name": "provider_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_verified_at": { + "name": "provider_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_validated_at": { + "name": "last_validated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_access_token_credentials_integration_level": { + "name": "UQ_platform_access_token_credentials_integration_level", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_access_token_credentials\".\"provider_resource_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_access_token_credentials_resource": { + "name": "UQ_platform_access_token_credentials_resource", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_credential_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_access_token_credentials\".\"provider_resource_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_access_token_credentials_authorized_by_user_id": { + "name": "IDX_platform_access_token_credentials_authorized_by_user_id", + "columns": [ + { + "expression": "authorized_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_access_token_credentials_authorized_by_user_id_kilocode_users_id_fk": { + "name": "platform_access_token_credentials_authorized_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_access_token_credentials", + "tableTo": "kilocode_users", + "columnsFrom": [ + "authorized_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "FK_platform_access_token_credentials_parent": { + "name": "FK_platform_access_token_credentials_parent", + "tableFrom": "platform_access_token_credentials", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_access_token_credentials_credential_version_check": { + "name": "platform_access_token_credentials_credential_version_check", + "value": "\"platform_access_token_credentials\".\"credential_version\" > 0" + }, + "platform_access_token_credentials_resource_id_check": { + "name": "platform_access_token_credentials_resource_id_check", + "value": "\"platform_access_token_credentials\".\"provider_resource_id\" IS NULL OR \"platform_access_token_credentials\".\"provider_resource_id\" <> ''" + } + }, + "isRLSEnabled": false + }, + "public.platform_integrations": { + "name": "platform_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "integration_type": { + "name": "integration_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_installation_id": { + "name": "platform_installation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_account_id": { + "name": "platform_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_account_login": { + "name": "platform_account_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "repository_access": { + "name": "repository_access", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repositories": { + "name": "repositories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "repositories_synced_at": { + "name": "repositories_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_invalid_at": { + "name": "auth_invalid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_invalid_reason": { + "name": "auth_invalid_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "kilo_requester_user_id": { + "name": "kilo_requester_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_requester_account_id": { + "name": "platform_requester_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "integration_status": { + "name": "integration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_by": { + "name": "suspended_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'standard'" + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_integrations_owned_by_org_platform_inst": { + "name": "UQ_platform_integrations_owned_by_org_platform_inst", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_owned_by_user_platform_inst": { + "name": "UQ_platform_integrations_owned_by_user_platform_inst", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_slack_platform_inst": { + "name": "UQ_platform_integrations_slack_platform_inst", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'slack' AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_linear_platform_inst": { + "name": "UQ_platform_integrations_linear_platform_inst", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'linear' AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_github_platform_inst": { + "name": "UQ_platform_integrations_github_platform_inst", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'github' AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_user_bitbucket": { + "name": "UQ_platform_integrations_user_bitbucket", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'bitbucket' AND \"platform_integrations\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_org_bitbucket": { + "name": "UQ_platform_integrations_org_bitbucket", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'bitbucket' AND \"platform_integrations\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_org_id": { + "name": "IDX_platform_integrations_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_user_id": { + "name": "IDX_platform_integrations_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform_inst_id": { + "name": "IDX_platform_integrations_platform_inst_id", + "columns": [ + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform": { + "name": "IDX_platform_integrations_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_org_platform": { + "name": "IDX_platform_integrations_owned_by_org_platform", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_user_platform": { + "name": "IDX_platform_integrations_owned_by_user_platform", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_integration_status": { + "name": "IDX_platform_integrations_integration_status", + "columns": [ + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_kilo_requester": { + "name": "IDX_platform_integrations_kilo_requester", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_requester_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform_requester": { + "name": "IDX_platform_integrations_platform_requester", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_requester_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_integrations_owned_by_organization_id_organizations_id_fk": { + "name": "platform_integrations_owned_by_organization_id_organizations_id_fk", + "tableFrom": "platform_integrations", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_integrations_owned_by_user_id_kilocode_users_id_fk": { + "name": "platform_integrations_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_integrations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_integrations_owner_check": { + "name": "platform_integrations_owner_check", + "value": "(\n (\"platform_integrations\".\"owned_by_user_id\" IS NOT NULL AND \"platform_integrations\".\"owned_by_organization_id\" IS NULL) OR\n (\"platform_integrations\".\"owned_by_user_id\" IS NULL AND \"platform_integrations\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.platform_oauth_credentials": { + "name": "platform_oauth_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorized_by_user_id": { + "name": "authorized_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subject_login": { + "name": "provider_subject_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_base_url": { + "name": "provider_base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret_encrypted": { + "name": "oauth_client_secret_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_oauth_credentials_platform_integration_id": { + "name": "UQ_platform_oauth_credentials_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_oauth_credentials_authorized_by_user_id": { + "name": "IDX_platform_oauth_credentials_authorized_by_user_id", + "columns": [ + { + "expression": "authorized_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_oauth_credentials_platform_integration_id_platform_integrations_id_fk": { + "name": "platform_oauth_credentials_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "platform_oauth_credentials", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_oauth_credentials_authorized_by_user_id_kilocode_users_id_fk": { + "name": "platform_oauth_credentials_authorized_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_oauth_credentials", + "tableTo": "kilocode_users", + "columnsFrom": [ + "authorized_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_oauth_credentials_credential_version_check": { + "name": "platform_oauth_credentials_credential_version_check", + "value": "\"platform_oauth_credentials\".\"credential_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.referral_code_usages": { + "name": "referral_code_usages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "referring_kilo_user_id": { + "name": "referring_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redeeming_kilo_user_id": { + "name": "redeeming_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_referral_code_usages_redeeming_kilo_user_id": { + "name": "IDX_referral_code_usages_redeeming_kilo_user_id", + "columns": [ + { + "expression": "redeeming_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_referral_code_usages_redeeming_user_id_code": { + "name": "UQ_referral_code_usages_redeeming_user_id_code", + "nullsNotDistinct": false, + "columns": [ + "redeeming_kilo_user_id", + "referring_kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_codes": { + "name": "referral_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "max_redemptions": { + "name": "max_redemptions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_referral_codes_kilo_user_id": { + "name": "UQ_referral_codes_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_referral_codes_code": { + "name": "IDX_referral_codes_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_advisor_check_catalog": { + "name": "security_advisor_check_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "explanation": { + "name": "explanation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "risk": { + "name": "risk", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_check_catalog_check_id_unique": { + "name": "security_advisor_check_catalog_check_id_unique", + "nullsNotDistinct": false, + "columns": [ + "check_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "security_advisor_check_catalog_severity_check": { + "name": "security_advisor_check_catalog_severity_check", + "value": "\"security_advisor_check_catalog\".\"severity\" in ('critical', 'warn', 'info')" + } + }, + "isRLSEnabled": false + }, + "public.security_advisor_content": { + "name": "security_advisor_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_content_key_unique": { + "name": "security_advisor_content_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_advisor_kiloclaw_coverage": { + "name": "security_advisor_kiloclaw_coverage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "area": { + "name": "area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_check_ids": { + "name": "match_check_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_kiloclaw_coverage_area_unique": { + "name": "security_advisor_kiloclaw_coverage_area_unique", + "nullsNotDistinct": false, + "columns": [ + "area" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_advisor_scans": { + "name": "security_advisor_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_platform": { + "name": "source_platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_method": { + "name": "source_method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_version": { + "name": "plugin_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "openclaw_version": { + "name": "openclaw_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_ip": { + "name": "public_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "findings_critical": { + "name": "findings_critical", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "findings_warn": { + "name": "findings_warn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "findings_info": { + "name": "findings_info", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_security_advisor_scans_user_created_at": { + "name": "idx_security_advisor_scans_user_created_at", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_advisor_scans_created_at": { + "name": "idx_security_advisor_scans_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_advisor_scans_platform": { + "name": "idx_security_advisor_scans_platform", + "columns": [ + { + "expression": "source_platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_agent_commands": { + "name": "security_agent_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "command_type": { + "name": "command_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'accepted'" + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_metadata": { + "name": "result_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_security_agent_commands_org_created": { + "name": "idx_security_agent_commands_org_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_user_created": { + "name": "idx_security_agent_commands_user_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_status_updated": { + "name": "idx_security_agent_commands_status_updated", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_finding_created": { + "name": "idx_security_agent_commands_finding_created", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_agent_commands_owned_by_organization_id_organizations_id_fk": { + "name": "security_agent_commands_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_commands_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_agent_commands_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_commands_finding_id_security_findings_id_fk": { + "name": "security_agent_commands_finding_id_security_findings_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_agent_commands_owner_check": { + "name": "security_agent_commands_owner_check", + "value": "(\n (\"security_agent_commands\".\"owned_by_user_id\" IS NOT NULL AND \"security_agent_commands\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_agent_commands\".\"owned_by_user_id\" IS NULL AND \"security_agent_commands\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_agent_commands_type_check": { + "name": "security_agent_commands_type_check", + "value": "\"security_agent_commands\".\"command_type\" IN ('sync', 'dismiss_finding', 'start_analysis', 'apply_auto_remediation')" + }, + "security_agent_commands_origin_check": { + "name": "security_agent_commands_origin_check", + "value": "\"security_agent_commands\".\"origin\" IN ('manual', 'dashboard_refresh', 'enable_initial_sync', 'settings_include_existing')" + }, + "security_agent_commands_status_check": { + "name": "security_agent_commands_status_check", + "value": "\"security_agent_commands\".\"status\" IN ('accepted', 'running', 'succeeded', 'failed', 'no_op')" + } + }, + "isRLSEnabled": false + }, + "public.security_agent_repository_sync_state": { + "name": "security_agent_repository_sync_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_succeeded_at": { + "name": "last_succeeded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_failure_code": { + "name": "last_failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_agent_repository_sync_state_org_repo": { + "name": "UQ_security_agent_repository_sync_state_org_repo", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_repository_sync_state\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_agent_repository_sync_state_user_repo": { + "name": "UQ_security_agent_repository_sync_state_user_repo", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_repository_sync_state\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_agent_repository_sync_state_owned_by_organization_id_organizations_id_fk": { + "name": "security_agent_repository_sync_state_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_agent_repository_sync_state", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_repository_sync_state_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_agent_repository_sync_state_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_agent_repository_sync_state", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_agent_repository_sync_state_owner_check": { + "name": "security_agent_repository_sync_state_owner_check", + "value": "(\n (\"security_agent_repository_sync_state\".\"owned_by_user_id\" IS NOT NULL AND \"security_agent_repository_sync_state\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_agent_repository_sync_state\".\"owned_by_user_id\" IS NULL AND \"security_agent_repository_sync_state\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_analysis_owner_state": { + "name": "security_analysis_owner_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_analysis_enabled_at": { + "name": "auto_analysis_enabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "blocked_until": { + "name": "blocked_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "block_reason": { + "name": "block_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consecutive_actor_resolution_failures": { + "name": "consecutive_actor_resolution_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_actor_resolution_failure_at": { + "name": "last_actor_resolution_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_analysis_owner_state_org_owner": { + "name": "UQ_security_analysis_owner_state_org_owner", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_analysis_owner_state\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_analysis_owner_state_user_owner": { + "name": "UQ_security_analysis_owner_state_user_owner", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_analysis_owner_state\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_analysis_owner_state_owned_by_organization_id_organizations_id_fk": { + "name": "security_analysis_owner_state_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_analysis_owner_state", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_owner_state_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_analysis_owner_state_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_analysis_owner_state", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_analysis_owner_state_owner_check": { + "name": "security_analysis_owner_state_owner_check", + "value": "(\n (\"security_analysis_owner_state\".\"owned_by_user_id\" IS NOT NULL AND \"security_analysis_owner_state\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_analysis_owner_state\".\"owned_by_user_id\" IS NULL AND \"security_analysis_owner_state\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_analysis_owner_state_block_reason_check": { + "name": "security_analysis_owner_state_block_reason_check", + "value": "\"security_analysis_owner_state\".\"block_reason\" IS NULL OR \"security_analysis_owner_state\".\"block_reason\" IN ('INSUFFICIENT_CREDITS', 'ACTOR_RESOLUTION_FAILED', 'OPERATOR_PAUSE')" + } + }, + "isRLSEnabled": false + }, + "public.security_analysis_queue": { + "name": "security_analysis_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "queue_status": { + "name": "queue_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity_rank": { + "name": "severity_rank", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_by_job_id": { + "name": "claimed_by_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reopen_requeue_count": { + "name": "reopen_requeue_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_analysis_queue_finding_id": { + "name": "UQ_security_analysis_queue_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_claim_path_org": { + "name": "idx_security_analysis_queue_claim_path_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "severity_rank", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_claim_path_user": { + "name": "idx_security_analysis_queue_claim_path_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "severity_rank", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_in_flight_org": { + "name": "idx_security_analysis_queue_in_flight_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queue_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_in_flight_user": { + "name": "idx_security_analysis_queue_in_flight_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queue_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_lag_dashboards": { + "name": "idx_security_analysis_queue_lag_dashboards", + "columns": [ + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_pending_reconciliation": { + "name": "idx_security_analysis_queue_pending_reconciliation", + "columns": [ + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_running_reconciliation": { + "name": "idx_security_analysis_queue_running_reconciliation", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_failure_trend": { + "name": "idx_security_analysis_queue_failure_trend", + "columns": [ + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"failure_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_analysis_queue_finding_id_security_findings_id_fk": { + "name": "security_analysis_queue_finding_id_security_findings_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_queue_owned_by_organization_id_organizations_id_fk": { + "name": "security_analysis_queue_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_queue_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_analysis_queue_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_analysis_queue_owner_check": { + "name": "security_analysis_queue_owner_check", + "value": "(\n (\"security_analysis_queue\".\"owned_by_user_id\" IS NOT NULL AND \"security_analysis_queue\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_analysis_queue\".\"owned_by_user_id\" IS NULL AND \"security_analysis_queue\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_analysis_queue_status_check": { + "name": "security_analysis_queue_status_check", + "value": "\"security_analysis_queue\".\"queue_status\" IN ('queued', 'pending', 'running', 'failed', 'completed')" + }, + "security_analysis_queue_claim_token_required_check": { + "name": "security_analysis_queue_claim_token_required_check", + "value": "\"security_analysis_queue\".\"queue_status\" NOT IN ('pending', 'running') OR \"security_analysis_queue\".\"claim_token\" IS NOT NULL" + }, + "security_analysis_queue_attempt_count_non_negative_check": { + "name": "security_analysis_queue_attempt_count_non_negative_check", + "value": "\"security_analysis_queue\".\"attempt_count\" >= 0" + }, + "security_analysis_queue_reopen_requeue_count_non_negative_check": { + "name": "security_analysis_queue_reopen_requeue_count_non_negative_check", + "value": "\"security_analysis_queue\".\"reopen_requeue_count\" >= 0" + }, + "security_analysis_queue_severity_rank_check": { + "name": "security_analysis_queue_severity_rank_check", + "value": "\"security_analysis_queue\".\"severity_rank\" IN (0, 1, 2, 3)" + }, + "security_analysis_queue_failure_code_check": { + "name": "security_analysis_queue_failure_code_check", + "value": "\"security_analysis_queue\".\"failure_code\" IS NULL OR \"security_analysis_queue\".\"failure_code\" IN (\n 'NETWORK_TIMEOUT',\n 'UPSTREAM_5XX',\n 'TEMP_TOKEN_FAILURE',\n 'START_CALL_AMBIGUOUS',\n 'REQUEUE_TEMPORARY_PRECONDITION',\n 'ACTOR_RESOLUTION_FAILED',\n 'GITHUB_TOKEN_UNAVAILABLE',\n 'INVALID_CONFIG',\n 'MISSING_OWNERSHIP',\n 'PERMISSION_DENIED_PERMANENT',\n 'UNSUPPORTED_SEVERITY',\n 'INSUFFICIENT_CREDITS',\n 'STATE_GUARD_REJECTED',\n 'SKIPPED_ALREADY_IN_PROGRESS',\n 'SKIPPED_NO_LONGER_ELIGIBLE',\n 'REOPEN_LOOP_GUARD',\n 'RUN_LOST'\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_audit_log": { + "name": "security_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "before_state": { + "name": "before_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_state": { + "name": "after_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "source_occurred_at": { + "name": "source_occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "finding_snapshot": { + "name": "finding_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_context": { + "name": "source_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_security_audit_log_org_created": { + "name": "IDX_security_audit_log_org_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_user_created": { + "name": "IDX_security_audit_log_user_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_resource": { + "name": "IDX_security_audit_log_resource", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_actor": { + "name": "IDX_security_audit_log_actor", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_action": { + "name": "IDX_security_audit_log_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_audit_log_org_event_key": { + "name": "UQ_security_audit_log_org_event_key", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL AND \"security_audit_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_audit_log_user_event_key": { + "name": "UQ_security_audit_log_user_event_key", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_org_occurred": { + "name": "IDX_security_audit_log_org_occurred", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL AND \"security_audit_log\".\"occurred_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_user_occurred": { + "name": "IDX_security_audit_log_user_occurred", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"occurred_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_audit_log_owned_by_organization_id_organizations_id_fk": { + "name": "security_audit_log_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_audit_log", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_audit_log_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_audit_log_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_audit_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_audit_log_owner_check": { + "name": "security_audit_log_owner_check", + "value": "(\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"owned_by_organization_id\" IS NULL) OR (\"security_audit_log\".\"owned_by_user_id\" IS NULL AND \"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "security_audit_log_action_check": { + "name": "security_audit_log_action_check", + "value": "\"security_audit_log\".\"action\" IN ('security.finding.created', 'security.finding.severity_changed', 'security.finding.status_change', 'security.finding.dismissed', 'security.finding.auto_dismissed', 'security.finding.superseded', 'security.finding.analysis_started', 'security.finding.analysis_completed', 'security.finding.analysis_failed', 'security.remediation.queued', 'security.remediation.started', 'security.remediation.pr_opened', 'security.remediation.failed', 'security.remediation.blocked', 'security.remediation.no_changes_needed', 'security.remediation.cancelled', 'security.remediation.retried', 'security.finding.deleted', 'security.config.enabled', 'security.config.disabled', 'security.config.updated', 'security.sync.triggered', 'security.sync.completed', 'security.audit_log.exported', 'security.audit_report.generated')" + }, + "security_audit_log_actor_type_check": { + "name": "security_audit_log_actor_type_check", + "value": "\"security_audit_log\".\"actor_type\" IN ('customer_user', 'kilo_admin', 'system')" + }, + "security_audit_log_source_context_check": { + "name": "security_audit_log_source_context_check", + "value": "\"security_audit_log\".\"source_context\" IN ('security_sync', 'web', 'analysis_worker', 'remediation_callback', 'rollout_baseline')" + } + }, + "isRLSEnabled": false + }, + "public.security_finding_notifications": { + "name": "security_finding_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'staged'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_security_finding_notifications_finding_recipient_kind": { + "name": "uq_security_finding_notifications_finding_recipient_kind", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_pending": { + "name": "idx_security_finding_notifications_pending", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_finding_notifications\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_staged": { + "name": "idx_security_finding_notifications_staged", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_finding_notifications\".\"status\" = 'staged'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_finding_id": { + "name": "idx_security_finding_notifications_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_recipient_user_id": { + "name": "idx_security_finding_notifications_recipient_user_id", + "columns": [ + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_finding_notifications_finding_fk": { + "name": "security_finding_notifications_finding_fk", + "tableFrom": "security_finding_notifications", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_finding_notifications_recipient_fk": { + "name": "security_finding_notifications_recipient_fk", + "tableFrom": "security_finding_notifications", + "tableTo": "kilocode_users", + "columnsFrom": [ + "recipient_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_finding_notifications_kind_check": { + "name": "security_finding_notifications_kind_check", + "value": "\"security_finding_notifications\".\"kind\" IN ('new_finding', 'sla_warning', 'sla_breach')" + }, + "security_finding_notifications_status_check": { + "name": "security_finding_notifications_status_check", + "value": "\"security_finding_notifications\".\"status\" IN ('staged', 'pending', 'sending', 'sent', 'failed', 'cancelled')" + }, + "security_finding_notifications_attempt_count_check": { + "name": "security_finding_notifications_attempt_count_check", + "value": "\"security_finding_notifications\".\"attempt_count\" >= 0" + }, + "security_finding_notifications_claimed_at_check": { + "name": "security_finding_notifications_claimed_at_check", + "value": "(\n (\"security_finding_notifications\".\"status\" = 'sending' AND \"security_finding_notifications\".\"claimed_at\" IS NOT NULL) OR\n (\"security_finding_notifications\".\"status\" <> 'sending' AND \"security_finding_notifications\".\"claimed_at\" IS NULL)\n )" + }, + "security_finding_notifications_sent_at_check": { + "name": "security_finding_notifications_sent_at_check", + "value": "(\n (\"security_finding_notifications\".\"status\" = 'sent' AND \"security_finding_notifications\".\"sent_at\" IS NOT NULL) OR\n (\"security_finding_notifications\".\"status\" <> 'sent' AND \"security_finding_notifications\".\"sent_at\" IS NULL)\n )" + }, + "security_finding_notifications_error_message_length_check": { + "name": "security_finding_notifications_error_message_length_check", + "value": "\"security_finding_notifications\".\"error_message\" IS NULL OR length(\"security_finding_notifications\".\"error_message\") <= 500" + } + }, + "isRLSEnabled": false + }, + "public.security_findings": { + "name": "security_findings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ghsa_id": { + "name": "ghsa_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cve_id": { + "name": "cve_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_ecosystem": { + "name": "package_ecosystem", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vulnerable_version_range": { + "name": "vulnerable_version_range", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "patched_version": { + "name": "patched_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manifest_path": { + "name": "manifest_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "ignored_reason": { + "name": "ignored_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignored_by": { + "name": "ignored_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fixed_at": { + "name": "fixed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sla_due_at": { + "name": "sla_due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dependabot_html_url": { + "name": "dependabot_html_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwe_ids": { + "name": "cwe_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "cvss_score": { + "name": "cvss_score", + "type": "numeric(3, 1)", + "primaryKey": false, + "notNull": false + }, + "dependency_scope": { + "name": "dependency_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_status": { + "name": "analysis_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_started_at": { + "name": "analysis_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "analysis_completed_at": { + "name": "analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "analysis_error": { + "name": "analysis_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis": { + "name": "analysis", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "raw_data": { + "name": "raw_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "first_detected_at": { + "name": "first_detected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_security_findings_user_source": { + "name": "uq_security_findings_user_source", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_findings\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_security_findings_org_source": { + "name": "uq_security_findings_org_source", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_findings\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_org_id": { + "name": "idx_security_findings_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_user_id": { + "name": "idx_security_findings_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_repo": { + "name": "idx_security_findings_repo", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_severity": { + "name": "idx_security_findings_severity", + "columns": [ + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_status": { + "name": "idx_security_findings_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_package": { + "name": "idx_security_findings_package", + "columns": [ + { + "expression": "package_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_sla_due_at": { + "name": "idx_security_findings_sla_due_at", + "columns": [ + { + "expression": "sla_due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_session_id": { + "name": "idx_security_findings_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_cli_session_id": { + "name": "idx_security_findings_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_analysis_status": { + "name": "idx_security_findings_analysis_status", + "columns": [ + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_org_analysis_in_flight": { + "name": "idx_security_findings_org_analysis_in_flight", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_findings\".\"analysis_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_user_analysis_in_flight": { + "name": "idx_security_findings_user_analysis_in_flight", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_findings\".\"analysis_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_findings_owned_by_organization_id_organizations_id_fk": { + "name": "security_findings_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_findings", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_findings_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_findings_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_findings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_findings_platform_integration_id_platform_integrations_id_fk": { + "name": "security_findings_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "security_findings", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_findings_owner_check": { + "name": "security_findings_owner_check", + "value": "(\n (\"security_findings\".\"owned_by_user_id\" IS NOT NULL AND \"security_findings\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_findings\".\"owned_by_user_id\" IS NULL AND \"security_findings\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_remediation_attempts": { + "name": "security_remediation_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "remediation_id": { + "name": "remediation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_number": { + "name": "attempt_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "retry_of_attempt_id": { + "name": "retry_of_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_fingerprint": { + "name": "analysis_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "analysis_completed_at": { + "name": "analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "remediation_model_slug": { + "name": "remediation_model_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_by_job_id": { + "name": "claimed_by_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_attempt_count": { + "name": "launch_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "callback_attempt_token_hash": { + "name": "callback_attempt_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "structured_result": { + "name": "structured_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "final_assistant_message": { + "name": "final_assistant_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "validation_evidence": { + "name": "validation_evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "risk_notes": { + "name": "risk_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_reason": { + "name": "draft_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_draft": { + "name": "pr_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pr_head_branch": { + "name": "pr_head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_base_branch": { + "name": "pr_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_at": { + "name": "cancellation_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_by_user_id": { + "name": "cancellation_requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "launched_at": { + "name": "launched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_remediation_attempts_number": { + "name": "UQ_security_remediation_attempts_number", + "columns": [ + { + "expression": "remediation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_active_finding": { + "name": "UQ_security_remediation_attempts_active_finding", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_active_remediation": { + "name": "UQ_security_remediation_attempts_active_remediation", + "columns": [ + { + "expression": "remediation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_finding_fingerprint_terminal": { + "name": "UQ_security_remediation_attempts_finding_fingerprint_terminal", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running', 'pr_opened')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_org_claim": { + "name": "idx_security_remediation_attempts_org_claim", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_user_claim": { + "name": "idx_security_remediation_attempts_user_claim", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_repo_claim": { + "name": "idx_security_remediation_attempts_repo_claim", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_org_inflight": { + "name": "idx_security_remediation_attempts_org_inflight", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_user_inflight": { + "name": "idx_security_remediation_attempts_user_inflight", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_repo_inflight": { + "name": "idx_security_remediation_attempts_repo_inflight", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_cloud_agent_session": { + "name": "idx_security_remediation_attempts_cloud_agent_session", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_finding_fingerprint": { + "name": "idx_security_remediation_attempts_finding_fingerprint", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_remediation_attempts_remediation_id_security_remediations_id_fk": { + "name": "security_remediation_attempts_remediation_id_security_remediations_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "security_remediations", + "columnsFrom": [ + "remediation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_finding_id_security_findings_id_fk": { + "name": "security_remediation_attempts_finding_id_security_findings_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_owned_by_organization_id_organizations_id_fk": { + "name": "security_remediation_attempts_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_requested_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_requested_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "requested_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "security_remediation_attempts_cancellation_requested_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_cancellation_requested_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "cancellation_requested_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_remediation_attempts_owner_check": { + "name": "security_remediation_attempts_owner_check", + "value": "(\n (\"security_remediation_attempts\".\"owned_by_user_id\" IS NOT NULL AND \"security_remediation_attempts\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_remediation_attempts\".\"owned_by_user_id\" IS NULL AND \"security_remediation_attempts\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_remediation_attempts_status_check": { + "name": "security_remediation_attempts_status_check", + "value": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running', 'pr_opened', 'failed', 'blocked', 'no_changes_needed', 'cancelled')" + }, + "security_remediation_attempts_origin_check": { + "name": "security_remediation_attempts_origin_check", + "value": "\"security_remediation_attempts\".\"origin\" IN ('auto_policy', 'bulk_existing', 'manual')" + }, + "security_remediation_attempts_attempt_number_check": { + "name": "security_remediation_attempts_attempt_number_check", + "value": "\"security_remediation_attempts\".\"attempt_number\" >= 1" + }, + "security_remediation_attempts_launch_attempt_count_check": { + "name": "security_remediation_attempts_launch_attempt_count_check", + "value": "\"security_remediation_attempts\".\"launch_attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.security_remediations": { + "name": "security_remediations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "latest_attempt_id": { + "name": "latest_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_analysis_fingerprint": { + "name": "latest_analysis_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_analysis_completed_at": { + "name": "latest_analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_draft": { + "name": "pr_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pr_head_branch": { + "name": "pr_head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_base_branch": { + "name": "pr_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_summary": { + "name": "outcome_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_security_remediations_finding_id": { + "name": "UQ_security_remediations_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_org_status": { + "name": "idx_security_remediations_org_status", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_user_status": { + "name": "idx_security_remediations_user_status", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_repo_status": { + "name": "idx_security_remediations_repo_status", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_latest_attempt": { + "name": "idx_security_remediations_latest_attempt", + "columns": [ + { + "expression": "latest_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_remediations_owned_by_organization_id_organizations_id_fk": { + "name": "security_remediations_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_remediations", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediations_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_remediations_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediations_finding_id_security_findings_id_fk": { + "name": "security_remediations_finding_id_security_findings_id_fk", + "tableFrom": "security_remediations", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_remediations_owner_check": { + "name": "security_remediations_owner_check", + "value": "(\n (\"security_remediations\".\"owned_by_user_id\" IS NOT NULL AND \"security_remediations\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_remediations\".\"owned_by_user_id\" IS NULL AND \"security_remediations\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_remediations_status_check": { + "name": "security_remediations_status_check", + "value": "\"security_remediations\".\"status\" IN ('queued', 'running', 'pr_opened', 'failed', 'blocked', 'no_changes_needed', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.shared_cli_sessions": { + "name": "shared_cli_sessions", + "schema": "", + "columns": { + "share_id": { + "name": "share_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_state": { + "name": "shared_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "api_conversation_history_blob_url": { + "name": "api_conversation_history_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_metadata_blob_url": { + "name": "task_metadata_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ui_messages_blob_url": { + "name": "ui_messages_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_state_blob_url": { + "name": "git_state_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_shared_cli_sessions_session_id": { + "name": "IDX_shared_cli_sessions_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_shared_cli_sessions_created_at": { + "name": "IDX_shared_cli_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_cli_sessions_session_id_cli_sessions_session_id_fk": { + "name": "shared_cli_sessions_session_id_cli_sessions_session_id_fk", + "tableFrom": "shared_cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "shared_cli_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "shared_cli_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "shared_cli_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "shared_cli_sessions_shared_state_check": { + "name": "shared_cli_sessions_shared_state_check", + "value": "\"shared_cli_sessions\".\"shared_state\" IN ('public', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.slack_bot_requests": { + "name": "slack_bot_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "slack_team_id": { + "name": "slack_team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_team_name": { + "name": "slack_team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_channel_id": { + "name": "slack_channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_user_id": { + "name": "slack_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_thread_ts": { + "name": "slack_thread_ts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message": { + "name": "user_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message_truncated": { + "name": "user_message_truncated", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_calls_made": { + "name": "tool_calls_made", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_slack_bot_requests_created_at": { + "name": "idx_slack_bot_requests_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_slack_team_id": { + "name": "idx_slack_bot_requests_slack_team_id", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_owned_by_org_id": { + "name": "idx_slack_bot_requests_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_owned_by_user_id": { + "name": "idx_slack_bot_requests_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_status": { + "name": "idx_slack_bot_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_event_type": { + "name": "idx_slack_bot_requests_event_type", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_team_created": { + "name": "idx_slack_bot_requests_team_created", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_bot_requests_owned_by_organization_id_organizations_id_fk": { + "name": "slack_bot_requests_owned_by_organization_id_organizations_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_bot_requests_owned_by_user_id_kilocode_users_id_fk": { + "name": "slack_bot_requests_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_bot_requests_platform_integration_id_platform_integrations_id_fk": { + "name": "slack_bot_requests_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "slack_bot_requests_owner_check": { + "name": "slack_bot_requests_owner_check", + "value": "(\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NOT NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NULL) OR\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NOT NULL) OR\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.source_embeddings": { + "name": "source_embeddings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "is_base_branch": { + "name": "is_base_branch", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_source_embeddings_organization_id": { + "name": "IDX_source_embeddings_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_kilo_user_id": { + "name": "IDX_source_embeddings_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_project_id": { + "name": "IDX_source_embeddings_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_created_at": { + "name": "IDX_source_embeddings_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_updated_at": { + "name": "IDX_source_embeddings_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_file_path_lower": { + "name": "IDX_source_embeddings_file_path_lower", + "columns": [ + { + "expression": "LOWER(\"file_path\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_git_branch": { + "name": "IDX_source_embeddings_git_branch", + "columns": [ + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_org_project_branch": { + "name": "IDX_source_embeddings_org_project_branch", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "source_embeddings_organization_id_organizations_id_fk": { + "name": "source_embeddings_organization_id_organizations_id_fk", + "tableFrom": "source_embeddings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "source_embeddings_kilo_user_id_kilocode_users_id_fk": { + "name": "source_embeddings_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "source_embeddings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_source_embeddings_org_project_branch_file_lines": { + "name": "UQ_source_embeddings_org_project_branch_file_lines", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "project_id", + "git_branch", + "file_path", + "start_line", + "end_line" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stripe_dispute_actions": { + "name": "stripe_dispute_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_reference_id": { + "name": "result_reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_dispute_actions_case_id": { + "name": "IDX_stripe_dispute_actions_case_id", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_actions_claim_path": { + "name": "IDX_stripe_dispute_actions_claim_path", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_dispute_actions_case_id_stripe_dispute_cases_id_fk": { + "name": "stripe_dispute_actions_case_id_stripe_dispute_cases_id_fk", + "tableFrom": "stripe_dispute_actions", + "tableTo": "stripe_dispute_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_dispute_actions_case_type_target": { + "name": "UQ_stripe_dispute_actions_case_type_target", + "nullsNotDistinct": false, + "columns": [ + "case_id", + "action_type", + "target_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_dispute_actions_action_type_check": { + "name": "stripe_dispute_actions_action_type_check", + "value": "\"stripe_dispute_actions\".\"action_type\" IN ('stripe_acceptance', 'user_block', 'auto_top_up_disable', 'credit_balance_reset', 'subscription_cancellation', 'access_termination', 'kiloclaw_suspension')" + }, + "stripe_dispute_actions_status_check": { + "name": "stripe_dispute_actions_status_check", + "value": "\"stripe_dispute_actions\".\"status\" IN ('queued', 'processing', 'completed', 'failed', 'skipped')" + }, + "stripe_dispute_actions_attempt_count_non_negative_check": { + "name": "stripe_dispute_actions_attempt_count_non_negative_check", + "value": "\"stripe_dispute_actions\".\"attempt_count\" >= 0" + }, + "stripe_dispute_actions_target_key_not_empty_check": { + "name": "stripe_dispute_actions_target_key_not_empty_check", + "value": "length(\"stripe_dispute_actions\".\"target_key\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_dispute_cases": { + "name": "stripe_dispute_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_dispute_id": { + "name": "stripe_dispute_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_event_created_at": { + "name": "stripe_event_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_intent_id": { + "name": "stripe_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_minor_units": { + "name": "amount_minor_units", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dispute_reason": { + "name": "dispute_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_status": { + "name": "stripe_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_classification": { + "name": "owner_classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'needs_action'" + }, + "status_reason": { + "name": "status_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_created_at": { + "name": "stripe_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "evidence_due_by": { + "name": "evidence_due_by", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_by_kilo_user_id": { + "name": "accepted_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acceptance_started_at": { + "name": "acceptance_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enforcement_completed_at": { + "name": "enforcement_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_required_at": { + "name": "review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_dispute_cases_event_id": { + "name": "IDX_stripe_dispute_cases_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_charge_id": { + "name": "IDX_stripe_dispute_cases_charge_id", + "columns": [ + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_payment_intent_id": { + "name": "IDX_stripe_dispute_cases_payment_intent_id", + "columns": [ + { + "expression": "stripe_payment_intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_customer_id": { + "name": "IDX_stripe_dispute_cases_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_kilo_user_id": { + "name": "IDX_stripe_dispute_cases_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_organization_id": { + "name": "IDX_stripe_dispute_cases_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_status_due_by": { + "name": "IDX_stripe_dispute_cases_status_due_by", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "evidence_due_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stripe_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_dispute_cases_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_dispute_cases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_dispute_cases_organization_id_organizations_id_fk": { + "name": "stripe_dispute_cases_organization_id_organizations_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_dispute_cases_accepted_by_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_dispute_cases_accepted_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "accepted_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_dispute_cases_dispute_id": { + "name": "UQ_stripe_dispute_cases_dispute_id", + "nullsNotDistinct": false, + "columns": [ + "stripe_dispute_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_dispute_cases_owner_classification_check": { + "name": "stripe_dispute_cases_owner_classification_check", + "value": "\"stripe_dispute_cases\".\"owner_classification\" IN ('personal', 'organization', 'ambiguous', 'unmatched')" + }, + "stripe_dispute_cases_status_check": { + "name": "stripe_dispute_cases_status_check", + "value": "\"stripe_dispute_cases\".\"status\" IN ('needs_action', 'processing', 'accepted', 'acceptance_failed', 'enforcement_failed', 'review_required', 'closed')" + }, + "stripe_dispute_cases_amount_minor_units_non_negative_check": { + "name": "stripe_dispute_cases_amount_minor_units_non_negative_check", + "value": "\"stripe_dispute_cases\".\"amount_minor_units\" IS NULL OR \"stripe_dispute_cases\".\"amount_minor_units\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_early_fraud_warning_actions": { + "name": "stripe_early_fraud_warning_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_reference_id": { + "name": "result_reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_early_fraud_warning_actions_case_id": { + "name": "IDX_stripe_early_fraud_warning_actions_case_id", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_actions_claim_path": { + "name": "IDX_stripe_early_fraud_warning_actions_claim_path", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_early_fraud_warning_actions_case_id_stripe_early_fraud_warning_cases_id_fk": { + "name": "stripe_early_fraud_warning_actions_case_id_stripe_early_fraud_warning_cases_id_fk", + "tableFrom": "stripe_early_fraud_warning_actions", + "tableTo": "stripe_early_fraud_warning_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_early_fraud_warning_actions_case_type_target": { + "name": "UQ_stripe_early_fraud_warning_actions_case_type_target", + "nullsNotDistinct": false, + "columns": [ + "case_id", + "action_type", + "target_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_early_fraud_warning_actions_action_type_check": { + "name": "stripe_early_fraud_warning_actions_action_type_check", + "value": "\"stripe_early_fraud_warning_actions\".\"action_type\" IN ('containment', 'refund', 'payment_value_clawback', 'subscription_termination', 'access_termination', 'kiloclaw_suspension', 'affiliate_payout_reversal', 'referral_reward_reversal', 'user_notice')" + }, + "stripe_early_fraud_warning_actions_status_check": { + "name": "stripe_early_fraud_warning_actions_status_check", + "value": "\"stripe_early_fraud_warning_actions\".\"status\" IN ('queued', 'processing', 'completed', 'failed', 'review_required', 'dismissed')" + }, + "stripe_early_fraud_warning_actions_attempt_count_non_negative_check": { + "name": "stripe_early_fraud_warning_actions_attempt_count_non_negative_check", + "value": "\"stripe_early_fraud_warning_actions\".\"attempt_count\" >= 0" + }, + "stripe_early_fraud_warning_actions_target_key_not_empty_check": { + "name": "stripe_early_fraud_warning_actions_target_key_not_empty_check", + "value": "length(\"stripe_early_fraud_warning_actions\".\"target_key\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_early_fraud_warning_cases": { + "name": "stripe_early_fraud_warning_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_early_fraud_warning_id": { + "name": "stripe_early_fraud_warning_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_intent_id": { + "name": "stripe_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_minor_units": { + "name": "amount_minor_units", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_classification": { + "name": "owner_classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "warning_created_at": { + "name": "warning_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "contained_at": { + "name": "contained_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_required_at": { + "name": "review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "remediated_at": { + "name": "remediated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_early_fraud_warning_cases_event_id": { + "name": "IDX_stripe_early_fraud_warning_cases_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_charge_id": { + "name": "IDX_stripe_early_fraud_warning_cases_charge_id", + "columns": [ + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_payment_intent_id": { + "name": "IDX_stripe_early_fraud_warning_cases_payment_intent_id", + "columns": [ + { + "expression": "stripe_payment_intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_customer_id": { + "name": "IDX_stripe_early_fraud_warning_cases_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_kilo_user_id": { + "name": "IDX_stripe_early_fraud_warning_cases_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_organization_id": { + "name": "IDX_stripe_early_fraud_warning_cases_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_status_created_at": { + "name": "IDX_stripe_early_fraud_warning_cases_status_created_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_early_fraud_warning_cases_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_early_fraud_warning_cases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_early_fraud_warning_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_early_fraud_warning_cases_organization_id_organizations_id_fk": { + "name": "stripe_early_fraud_warning_cases_organization_id_organizations_id_fk", + "tableFrom": "stripe_early_fraud_warning_cases", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_early_fraud_warning_cases_warning_id": { + "name": "UQ_stripe_early_fraud_warning_cases_warning_id", + "nullsNotDistinct": false, + "columns": [ + "stripe_early_fraud_warning_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_early_fraud_warning_cases_owner_classification_check": { + "name": "stripe_early_fraud_warning_cases_owner_classification_check", + "value": "\"stripe_early_fraud_warning_cases\".\"owner_classification\" IN ('personal', 'organization', 'ambiguous', 'unmatched')" + }, + "stripe_early_fraud_warning_cases_status_check": { + "name": "stripe_early_fraud_warning_cases_status_check", + "value": "\"stripe_early_fraud_warning_cases\".\"status\" IN ('queued', 'contained', 'processing', 'completed', 'review_required', 'failed', 'remediated', 'dismissed')" + }, + "stripe_early_fraud_warning_cases_amount_minor_units_non_negative_check": { + "name": "stripe_early_fraud_warning_cases_amount_minor_units_non_negative_check", + "value": "\"stripe_early_fraud_warning_cases\".\"amount_minor_units\" IS NULL OR \"stripe_early_fraud_warning_cases\".\"amount_minor_units\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.stytch_fingerprints": { + "name": "stytch_fingerprints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visitor_fingerprint": { + "name": "visitor_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "browser_fingerprint": { + "name": "browser_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "browser_id": { + "name": "browser_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hardware_fingerprint": { + "name": "hardware_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "network_fingerprint": { + "name": "network_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visitor_id": { + "name": "visitor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verdict_action": { + "name": "verdict_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detected_device_type": { + "name": "detected_device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_authentic_device": { + "name": "is_authentic_device", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "reasons": { + "name": "reasons", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{\"\"}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fingerprint_data": { + "name": "fingerprint_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_free_tier_allowed": { + "name": "kilo_free_tier_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_hardware_fingerprint": { + "name": "idx_hardware_fingerprint", + "columns": [ + { + "expression": "hardware_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_kilo_user_id": { + "name": "idx_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_stytch_fingerprints_reasons_gin": { + "name": "idx_stytch_fingerprints_reasons_gin", + "columns": [ + { + "expression": "reasons", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_verdict_action": { + "name": "idx_verdict_action", + "columns": [ + { + "expression": "verdict_action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_visitor_fingerprint": { + "name": "idx_visitor_fingerprint", + "columns": [ + { + "expression": "visitor_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_prompt_prefix": { + "name": "system_prompt_prefix", + "schema": "", + "columns": { + "system_prompt_prefix_id": { + "name": "system_prompt_prefix_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "system_prompt_prefix": { + "name": "system_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_system_prompt_prefix": { + "name": "UQ_system_prompt_prefix", + "columns": [ + { + "expression": "system_prompt_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transactional_email_log": { + "name": "transactional_email_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email_type": { + "name": "email_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_transactional_email_log_type_idempotency_key": { + "name": "UQ_transactional_email_log_type_idempotency_key", + "columns": [ + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_transactional_email_log_user_id": { + "name": "IDX_transactional_email_log_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_transactional_email_log_organization_id": { + "name": "IDX_transactional_email_log_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactional_email_log_user_id_kilocode_users_id_fk": { + "name": "transactional_email_log_user_id_kilocode_users_id_fk", + "tableFrom": "transactional_email_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactional_email_log_organization_id_organizations_id_fk": { + "name": "transactional_email_log_organization_id_organizations_id_fk", + "tableFrom": "transactional_email_log", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_transactional_email_log_owner": { + "name": "CHK_transactional_email_log_owner", + "value": "\"transactional_email_log\".\"user_id\" IS NOT NULL OR \"transactional_email_log\".\"organization_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.user_admin_notes": { + "name": "user_admin_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note_content": { + "name": "note_content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admin_kilo_user_id": { + "name": "admin_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_34517df0b385234babc38fe81b": { + "name": "IDX_34517df0b385234babc38fe81b", + "columns": [ + { + "expression": "admin_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_ccbde98c4c14046daa5682ec4f": { + "name": "IDX_ccbde98c4c14046daa5682ec4f", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_d0270eb24ef6442d65a0b7853c": { + "name": "IDX_d0270eb24ef6442d65a0b7853c", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_affiliate_attributions": { + "name": "user_affiliate_attributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tracking_id": { + "name": "tracking_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_affiliate_attributions_user_id": { + "name": "IDX_user_affiliate_attributions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_affiliate_attributions_user_id_kilocode_users_id_fk": { + "name": "user_affiliate_attributions_user_id_kilocode_users_id_fk", + "tableFrom": "user_affiliate_attributions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_affiliate_attributions_user_provider": { + "name": "UQ_user_affiliate_attributions_user_provider", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "provider" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_affiliate_attributions_provider_check": { + "name": "user_affiliate_attributions_provider_check", + "value": "\"user_affiliate_attributions\".\"provider\" IN ('impact')" + } + }, + "isRLSEnabled": false + }, + "public.user_affiliate_events": { + "name": "user_affiliate_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_event_id": { + "name": "parent_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "delivery_state": { + "name": "delivery_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_action_id": { + "name": "impact_action_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_submission_uri": { + "name": "impact_submission_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_affiliate_events_claim_path": { + "name": "IDX_user_affiliate_events_claim_path", + "columns": [ + { + "expression": "delivery_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_affiliate_events_parent_event_id": { + "name": "IDX_user_affiliate_events_parent_event_id", + "columns": [ + { + "expression": "parent_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_affiliate_events_provider_event_type_charge": { + "name": "IDX_user_affiliate_events_provider_event_type_charge", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_affiliate_events_user_id_kilocode_users_id_fk": { + "name": "user_affiliate_events_user_id_kilocode_users_id_fk", + "tableFrom": "user_affiliate_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "user_affiliate_events_parent_event_id_fk": { + "name": "user_affiliate_events_parent_event_id_fk", + "tableFrom": "user_affiliate_events", + "tableTo": "user_affiliate_events", + "columnsFrom": [ + "parent_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_affiliate_events_dedupe_key": { + "name": "UQ_user_affiliate_events_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_affiliate_events_provider_check": { + "name": "user_affiliate_events_provider_check", + "value": "\"user_affiliate_events\".\"provider\" IN ('impact')" + }, + "user_affiliate_events_event_type_check": { + "name": "user_affiliate_events_event_type_check", + "value": "\"user_affiliate_events\".\"event_type\" IN ('signup', 'trial_start', 'trial_end', 'sale', 'sale_reversal')" + }, + "user_affiliate_events_delivery_state_check": { + "name": "user_affiliate_events_delivery_state_check", + "value": "\"user_affiliate_events\".\"delivery_state\" IN ('queued', 'blocked', 'sending', 'delivered', 'failed')" + }, + "user_affiliate_events_attempt_count_non_negative_check": { + "name": "user_affiliate_events_attempt_count_non_negative_check", + "value": "\"user_affiliate_events\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.user_auth_provider": { + "name": "user_auth_provider", + "schema": "", + "columns": { + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hosted_domain": { + "name": "hosted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_auth_provider_kilo_user_id": { + "name": "IDX_user_auth_provider_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_auth_provider_hosted_domain": { + "name": "IDX_user_auth_provider_hosted_domain", + "columns": [ + { + "expression": "hosted_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_auth_provider_provider_provider_account_id_pk": { + "name": "user_auth_provider_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_feedback": { + "name": "user_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feedback_for": { + "name": "feedback_for", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "feedback_batch": { + "name": "feedback_batch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "context_json": { + "name": "context_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_feedback_created_at": { + "name": "IDX_user_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_kilo_user_id": { + "name": "IDX_user_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_feedback_for": { + "name": "IDX_user_feedback_feedback_for", + "columns": [ + { + "expression": "feedback_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_feedback_batch": { + "name": "IDX_user_feedback_feedback_batch", + "columns": [ + { + "expression": "feedback_batch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_source": { + "name": "IDX_user_feedback_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "user_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_github_app_tokens": { + "name": "user_github_app_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "github_user_id": { + "name": "github_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_login": { + "name": "github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_github_app_tokens_user_app": { + "name": "UQ_user_github_app_tokens_user_app", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_github_app_tokens_github_user_app": { + "name": "UQ_user_github_app_tokens_github_user_app", + "columns": [ + { + "expression": "github_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_github_app_tokens_kilo_user_id_kilocode_users_id_fk": { + "name": "user_github_app_tokens_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_github_app_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_github_app_tokens_app_type_check": { + "name": "user_github_app_tokens_app_type_check", + "value": "\"user_github_app_tokens\".\"github_app_type\" IN ('standard', 'lite')" + } + }, + "isRLSEnabled": false + }, + "public.user_model_preferences": { + "name": "user_model_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "favorites": { + "name": "favorites", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "last_selected": { + "name": "last_selected", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_model_preferences_user_id": { + "name": "UQ_user_model_preferences_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_model_preferences_user_id_kilocode_users_id_fk": { + "name": "user_model_preferences_user_id_kilocode_users_id_fk", + "tableFrom": "user_model_preferences", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_notification_preferences": { + "name": "user_notification_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_push_enabled": { + "name": "agent_push_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "chat_messages_enabled": { + "name": "chat_messages_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "agent_attention_enabled": { + "name": "agent_attention_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "session_status_enabled": { + "name": "session_status_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kiloclaw_activity_enabled": { + "name": "kiloclaw_activity_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "balance_alerts_enabled": { + "name": "balance_alerts_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "security_findings_enabled": { + "name": "security_findings_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_notification_preferences_user_id_kilocode_users_id_fk": { + "name": "user_notification_preferences_user_id_kilocode_users_id_fk", + "tableFrom": "user_notification_preferences", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_period_cache": { + "name": "user_period_cache", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cache_type": { + "name": "cache_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_type": { + "name": "period_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_key": { + "name": "period_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "computed_at": { + "name": "computed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "shared_url_token": { + "name": "shared_url_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_at": { + "name": "shared_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_user_period_cache_kilo_user_id": { + "name": "IDX_user_period_cache_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_period_cache": { + "name": "UQ_user_period_cache", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cache_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_period_cache_lookup": { + "name": "IDX_user_period_cache_lookup", + "columns": [ + { + "expression": "cache_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_period_cache_share_token": { + "name": "UQ_user_period_cache_share_token", + "columns": [ + { + "expression": "shared_url_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_period_cache\".\"shared_url_token\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_period_cache_kilo_user_id_kilocode_users_id_fk": { + "name": "user_period_cache_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_period_cache", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_period_cache_period_type_check": { + "name": "user_period_cache_period_type_check", + "value": "\"user_period_cache\".\"period_type\" IN ('year', 'quarter', 'month', 'week', 'custom')" + } + }, + "isRLSEnabled": false + }, + "public.user_push_tokens": { + "name": "user_push_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_push_tokens_token": { + "name": "UQ_user_push_tokens_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_push_tokens_user_id": { + "name": "IDX_user_push_tokens_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_push_tokens_user_id_kilocode_users_id_fk": { + "name": "user_push_tokens_user_id_kilocode_users_id_fk", + "tableFrom": "user_push_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vercel_ip_city": { + "name": "vercel_ip_city", + "schema": "", + "columns": { + "vercel_ip_city_id": { + "name": "vercel_ip_city_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vercel_ip_city": { + "name": "vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_vercel_ip_city": { + "name": "UQ_vercel_ip_city", + "columns": [ + { + "expression": "vercel_ip_city", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vercel_ip_country": { + "name": "vercel_ip_country", + "schema": "", + "columns": { + "vercel_ip_country_id": { + "name": "vercel_ip_country_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vercel_ip_country": { + "name": "vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_vercel_ip_country": { + "name": "UQ_vercel_ip_country", + "columns": [ + { + "expression": "vercel_ip_country", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_events": { + "name": "webhook_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_action": { + "name": "event_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "processed": { + "name": "processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "handlers_triggered": { + "name": "handlers_triggered", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "event_signature": { + "name": "event_signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_webhook_events_owned_by_org_id": { + "name": "IDX_webhook_events_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_owned_by_user_id": { + "name": "IDX_webhook_events_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_platform": { + "name": "IDX_webhook_events_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_event_type": { + "name": "IDX_webhook_events_event_type", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_created_at": { + "name": "IDX_webhook_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_events_owned_by_organization_id_organizations_id_fk": { + "name": "webhook_events_owned_by_organization_id_organizations_id_fk", + "tableFrom": "webhook_events", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_events_owned_by_user_id_kilocode_users_id_fk": { + "name": "webhook_events_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "webhook_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_webhook_events_signature": { + "name": "UQ_webhook_events_signature", + "nullsNotDistinct": false, + "columns": [ + "event_signature" + ] + } + }, + "policies": {}, + "checkConstraints": { + "webhook_events_owner_check": { + "name": "webhook_events_owner_check", + "value": "(\n (\"webhook_events\".\"owned_by_user_id\" IS NOT NULL AND \"webhook_events\".\"owned_by_organization_id\" IS NULL) OR\n (\"webhook_events\".\"owned_by_user_id\" IS NULL AND \"webhook_events\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.microdollar_usage_view": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_write_tokens": { + "name": "cache_write_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_hit_tokens": { + "name": "cache_hit_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_model": { + "name": "requested_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_prompt_prefix": { + "name": "user_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt_prefix": { + "name": "system_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt_length": { + "name": "system_prompt_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_discount": { + "name": "cache_discount", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "max_tokens": { + "name": "max_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_middle_out_transform": { + "name": "has_middle_out_transform", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "abuse_classification": { + "name": "abuse_classification", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inference_provider": { + "name": "inference_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "upstream_id": { + "name": "upstream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finish_reason": { + "name": "finish_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latency": { + "name": "latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "moderation_latency": { + "name": "moderation_latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "generation_time": { + "name": "generation_time", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_byok": { + "name": "is_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_user_byok": { + "name": "is_user_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "streamed": { + "name": "streamed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancelled": { + "name": "cancelled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "editor_name": { + "name": "editor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_kind": { + "name": "api_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_tools": { + "name": "has_tools", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_model": { + "name": "auto_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "market_cost": { + "name": "market_cost", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "abuse_delay": { + "name": "abuse_delay", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "abuse_downgraded_from": { + "name": "abuse_downgraded_from", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT\n mu.id,\n mu.kilo_user_id,\n meta.message_id,\n mu.cost,\n mu.input_tokens,\n mu.output_tokens,\n mu.cache_write_tokens,\n mu.cache_hit_tokens,\n mu.created_at,\n ip.http_ip AS http_x_forwarded_for,\n city.vercel_ip_city AS http_x_vercel_ip_city,\n country.vercel_ip_country AS http_x_vercel_ip_country,\n meta.vercel_ip_latitude AS http_x_vercel_ip_latitude,\n meta.vercel_ip_longitude AS http_x_vercel_ip_longitude,\n ja4.ja4_digest AS http_x_vercel_ja4_digest,\n mu.provider,\n mu.model,\n mu.requested_model,\n meta.user_prompt_prefix,\n spp.system_prompt_prefix,\n meta.system_prompt_length,\n ua.http_user_agent,\n mu.cache_discount,\n meta.max_tokens,\n meta.has_middle_out_transform,\n mu.has_error,\n mu.abuse_classification,\n mu.organization_id,\n mu.inference_provider,\n mu.project_id,\n meta.status_code,\n meta.upstream_id,\n frfr.finish_reason,\n meta.latency,\n meta.moderation_latency,\n meta.generation_time,\n meta.is_byok,\n meta.is_user_byok,\n meta.streamed,\n meta.cancelled,\n edit.editor_name,\n ak.api_kind,\n meta.has_tools,\n meta.machine_id,\n feat.feature,\n meta.session_id,\n md.mode,\n am.auto_model,\n meta.market_cost,\n meta.is_free,\n meta.abuse_delay,\n meta.abuse_downgraded_from\n FROM \"microdollar_usage\" mu\n LEFT JOIN \"microdollar_usage_metadata\" meta ON mu.id = meta.id\n LEFT JOIN \"http_ip\" ip ON meta.http_ip_id = ip.http_ip_id\n LEFT JOIN \"vercel_ip_city\" city ON meta.vercel_ip_city_id = city.vercel_ip_city_id\n LEFT JOIN \"vercel_ip_country\" country ON meta.vercel_ip_country_id = country.vercel_ip_country_id\n LEFT JOIN \"ja4_digest\" ja4 ON meta.ja4_digest_id = ja4.ja4_digest_id\n LEFT JOIN \"system_prompt_prefix\" spp ON meta.system_prompt_prefix_id = spp.system_prompt_prefix_id\n LEFT JOIN \"http_user_agent\" ua ON meta.http_user_agent_id = ua.http_user_agent_id\n LEFT JOIN \"finish_reason\" frfr ON meta.finish_reason_id = frfr.finish_reason_id\n LEFT JOIN \"editor_name\" edit ON meta.editor_name_id = edit.editor_name_id\n LEFT JOIN \"api_kind\" ak ON meta.api_kind_id = ak.api_kind_id\n LEFT JOIN \"feature\" feat ON meta.feature_id = feat.feature_id\n LEFT JOIN \"mode\" md ON meta.mode_id = md.mode_id\n LEFT JOIN \"auto_model\" am ON meta.auto_model_id = am.auto_model_id\n", + "name": "microdollar_usage_view", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index d997ce7769..16fd2fbe56 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -1436,6 +1436,13 @@ "when": 1785918385796, "tag": "0204_drop_cost_insight_tables", "breakpoints": true + }, + { + "idx": 205, + "version": "7", + "when": 1785934676925, + "tag": "0205_device_auth_hardening", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema.test.ts b/packages/db/src/schema.test.ts index d47f81d543..dfe2d3b2e1 100644 --- a/packages/db/src/schema.test.ts +++ b/packages/db/src/schema.test.ts @@ -1,5 +1,5 @@ -import { afterAll, describe, expect, it } from '@jest/globals'; -import { eq } from 'drizzle-orm'; +import { afterAll, afterEach, beforeEach, describe, expect, it } from '@jest/globals'; +import { eq, sql } from 'drizzle-orm'; import * as fs from 'fs'; import * as path from 'path'; import { generateDrizzleJson, generateMigration } from 'drizzle-kit/api'; @@ -1310,4 +1310,196 @@ describe('database schema', () => { }); }); }); + + describe('GitHub platform integration global unique index', () => { + const uniqueIndexName = 'UQ_platform_integrations_github_platform_inst'; + const installationId = `schema-github-idx-${crypto.randomUUID()}`; + let userIdA: string; + let userIdB: string; + + beforeEach(async () => { + userIdA = `schema-github-idx-a-${crypto.randomUUID()}`; + userIdB = `schema-github-idx-b-${crypto.randomUUID()}`; + + await schemaTestDb.db.insert(schema.kilocode_users).values([ + { + id: userIdA, + google_user_email: `${userIdA}@example.com`, + google_user_name: 'GitHub Idx User A', + google_user_image_url: 'https://example.com/avatar.png', + stripe_customer_id: `cus_${crypto.randomUUID()}`, + }, + { + id: userIdB, + google_user_email: `${userIdB}@example.com`, + google_user_name: 'GitHub Idx User B', + google_user_image_url: 'https://example.com/avatar.png', + stripe_customer_id: `cus_${crypto.randomUUID()}`, + }, + ]); + }); + + afterEach(async () => { + await schemaTestDb.db + .delete(schema.platform_integrations) + .where(eq(schema.platform_integrations.platform_installation_id, installationId)); + await schemaTestDb.db + .delete(schema.kilocode_users) + .where(eq(schema.kilocode_users.id, userIdA)); + await schemaTestDb.db + .delete(schema.kilocode_users) + .where(eq(schema.kilocode_users.id, userIdB)); + }); + + it('rejects duplicate (platform, github_app_type, platform_installation_id) for GitHub', async () => { + const base = { + platform: 'github', + integration_type: 'app', + platform_installation_id: installationId, + platform_account_id: '12345', + platform_account_login: 'test-owner', + integration_status: 'active', + repository_access: 'all', + github_app_type: 'standard', + installed_at: '2026-07-01T00:00:00.000Z', + } satisfies typeof schema.platform_integrations.$inferInsert; + + // First insert succeeds. + await schemaTestDb.db.insert(schema.platform_integrations).values({ + ...base, + owned_by_user_id: userIdA, + }); + + // Second insert with same installation_id but different owner must fail. + const duplicate = schemaTestDb.db.insert(schema.platform_integrations).values({ + ...base, + owned_by_user_id: userIdB, + }); + + await expect(duplicate).rejects.toMatchObject({ + cause: { + constraint: uniqueIndexName, + }, + }); + }); + + it('allows same platform_installation_id with different github_app_type', async () => { + const base = { + platform: 'github', + integration_type: 'app', + platform_installation_id: installationId, + platform_account_id: '12345', + platform_account_login: 'test-owner', + integration_status: 'active', + repository_access: 'all', + installed_at: '2026-07-01T00:00:00.000Z', + } satisfies typeof schema.platform_integrations.$inferInsert; + + // 'standard' app type. + await schemaTestDb.db.insert(schema.platform_integrations).values({ + ...base, + github_app_type: 'standard', + owned_by_user_id: userIdA, + }); + + // 'lite' app type — uses a different indexed column value. + const lite = schemaTestDb.db.insert(schema.platform_integrations).values({ + ...base, + github_app_type: 'lite', + owned_by_user_id: userIdB, + }); + + // Should not fail — the unique index includes github_app_type. + await expect(lite).resolves.not.toThrow(); + }); + + it('runs migration 0204 against duplicates before creating its unique index', async () => { + const migrationPath = path.join(__dirname, 'migrations/0204_brainy_baron_strucker.sql'); + const fullMigration = fs.readFileSync(migrationPath, 'utf8'); + const statements = fullMigration.split('--> statement-breakpoint'); + // The dedup DO block is its own statement. The COMMIT/CONCURRENTLY/BEGIN + // markers that follow it in the migration must not run inside this test's + // transaction, so extract just the DO block and the unique-index DDL. + const migration = statements.find(stmt => stmt.includes('-- Backfill:')); + const createUniqueIndex = statements.find(stmt => + stmt.includes( + 'CREATE UNIQUE INDEX CONCURRENTLY "UQ_platform_integrations_github_platform_inst"' + ) + ); + if (migration === undefined || createUniqueIndex === undefined) { + throw new Error('migration 0204 backfill or unique index statement not found'); + } + const rollback = new Error('rollback migration test'); + + try { + await schemaTestDb.db.transaction(async tx => { + await tx.execute(sql.raw('DROP INDEX "UQ_platform_integrations_github_platform_inst"')); + + const base = { + platform: 'github', + integration_type: 'app', + platform_installation_id: installationId, + platform_account_id: `dedup-${installationId}`, + platform_account_login: 'dedup-owner', + integration_status: 'active', + repository_access: 'all', + github_app_type: 'standard', + } satisfies typeof schema.platform_integrations.$inferInsert; + const [older] = await tx + .insert(schema.platform_integrations) + .values({ + ...base, + owned_by_user_id: userIdA, + installed_at: '2026-07-01T00:00:00.000Z', + }) + .returning({ id: schema.platform_integrations.id }); + const [newer] = await tx + .insert(schema.platform_integrations) + .values({ + ...base, + owned_by_user_id: userIdB, + installed_at: '2026-07-02T00:00:00.000Z', + }) + .returning({ id: schema.platform_integrations.id }); + + await tx.execute(sql.raw(migration)); + + // The migration creates the unique index CONCURRENTLY after the + // backfill. Inside this transaction, use the plain form; the point is + // that the index now succeeds on deduped rows. + await tx.execute(sql.raw(createUniqueIndex.replace('CONCURRENTLY ', ''))); + + const rows = await tx + .select({ + id: schema.platform_integrations.id, + status: schema.platform_integrations.integration_status, + installationId: schema.platform_integrations.platform_installation_id, + metadata: schema.platform_integrations.metadata, + }) + .from(schema.platform_integrations) + .where(eq(schema.platform_integrations.platform_account_id, base.platform_account_id)); + const winner = rows.find(row => row.id === newer?.id); + const loser = rows.find(row => row.id === older?.id); + + expect(winner).toMatchObject({ status: 'active', installationId }); + expect(loser).toMatchObject({ status: 'suspended', installationId: null }); + expect(loser?.metadata).toMatchObject({ + github_dedup: { + reason: 'Duplicate installation resolved by migration 0204', + original_installation_id: installationId, + }, + }); + await expect( + tx.insert(schema.platform_integrations).values({ ...base, owned_by_user_id: userIdA }) + ).rejects.toMatchObject({ cause: { constraint: uniqueIndexName } }); + + throw rollback; + }); + } catch (error) { + if (error !== rollback) { + throw error; + } + } + }); + }); }); diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index d5bf090029..2835eb744e 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -3731,6 +3731,10 @@ export const platform_integrations = pgTable( uniqueIndex('UQ_platform_integrations_linear_platform_inst') .on(table.platform, table.platform_installation_id) .where(sql`${table.platform} = 'linear' AND ${table.platform_installation_id} IS NOT NULL`), + uniqueIndex('UQ_platform_integrations_github_platform_inst') + .on(table.platform, table.github_app_type, table.platform_installation_id) + .concurrently() + .where(sql`${table.platform} = 'github' AND ${table.platform_installation_id} IS NOT NULL`), uniqueIndex('UQ_platform_integrations_user_bitbucket') .on(table.owned_by_user_id) .where(sql`${table.platform} = 'bitbucket' AND ${table.owned_by_user_id} IS NOT NULL`), @@ -4422,11 +4426,17 @@ export const magic_link_tokens = pgTable( consumed_at: timestamp({ withTimezone: true, mode: 'string' }), created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), attempts: integer().default(0).notNull(), + reserved_until: timestamp({ withTimezone: true, mode: 'string' }), purpose: text().default('magic_link').notNull().$type<'magic_link' | 'sign_in_code'>(), + challenge_id: uuid(), }, table => [ index('idx_magic_link_tokens_email').on(table.email), index('idx_magic_link_tokens_expires_at').on(table.expires_at), + uniqueIndex('UQ_magic_link_tokens_challenge_id') + .on(table.challenge_id) + .concurrently() + .where(sql`${table.challenge_id} IS NOT NULL`), check('check_expires_at_future', sql`${table.expires_at} > ${table.created_at}`), ] ); @@ -5567,11 +5577,14 @@ export const device_auth_requests = pgTable( onDelete: 'cascade', }), status: text() - .$type<'pending' | 'approved' | 'denied' | 'expired'>() + .$type<'pending' | 'approved' | 'denied' | 'expired' | 'consumed'>() .notNull() .default('pending'), expires_at: timestamp({ withTimezone: true, mode: 'string' }).notNull(), approved_at: timestamp({ withTimezone: true, mode: 'string' }), + consumed_at: timestamp({ withTimezone: true, mode: 'string' }), + user_code: text(), + device_code_hash: text(), user_agent: text(), ip_address: text(), created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), @@ -5585,11 +5598,63 @@ export const device_auth_requests = pgTable( index('IDX_device_auth_requests_status').on(table.status), index('IDX_device_auth_requests_expires_at').on(table.expires_at), index('IDX_device_auth_requests_kilo_user_id').on(table.kilo_user_id), + uniqueIndex('UQ_device_auth_requests_device_code_hash') + .on(table.device_code_hash) + .concurrently() + .where(sql`${table.device_code_hash} IS NOT NULL`), + index('IDX_device_auth_requests_user_code') + .on(table.user_code) + .concurrently() + .where(sql`${table.user_code} IS NOT NULL`), ] ); export type DeviceAuthRequest = typeof device_auth_requests.$inferSelect; +export const device_sessions = pgTable( + 'device_sessions', + { + id: uuid() + .default(sql`pg_catalog.gen_random_uuid()`) + .primaryKey() + .notNull(), + kilo_user_id: text() + .notNull() + .references(() => kilocode_users.id, { onDelete: 'cascade' }), + device_auth_request_id: uuid(), + user_agent: text(), + created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + last_seen_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + revoked_at: timestamp({ withTimezone: true, mode: 'string' }), + revoked_reason: text(), + }, + table => [ + index('IDX_device_sessions_kilo_user_id').on(table.kilo_user_id), + index('IDX_device_sessions_revoked_at').on(table.revoked_at), + ] +); + +export type DeviceSession = typeof device_sessions.$inferSelect; + +export const device_refresh_tokens = pgTable( + 'device_refresh_tokens', + { + token_hash: text().primaryKey().notNull(), + device_session_id: uuid() + .notNull() + .references(() => device_sessions.id, { onDelete: 'cascade' }), + expires_at: timestamp({ withTimezone: true, mode: 'string' }).notNull(), + consumed_at: timestamp({ withTimezone: true, mode: 'string' }), + created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + }, + table => [ + index('IDX_device_refresh_tokens_device_session_id').on(table.device_session_id), + index('IDX_device_refresh_tokens_expires_at').on(table.expires_at), + ] +); + +export type DeviceRefreshToken = typeof device_refresh_tokens.$inferSelect; + // App Builder Projects export const app_builder_projects = pgTable( 'app_builder_projects', @@ -9888,4 +9953,63 @@ export const container_usage_segment = pgTable( ); export type ContainerUsageSegment = typeof container_usage_segment.$inferSelect; + +export const github_install_states = pgTable( + 'github_install_states', + { + token: text().primaryKey().notNull(), + kilo_user_id: text() + .notNull() + .references(() => kilocode_users.id, { onDelete: 'cascade' }), + owner_type: text().notNull(), + owner_id: text().notNull(), + github_app_type: text().notNull(), + return_to: text(), + expires_at: timestamp({ withTimezone: true, mode: 'string' }).notNull(), + consumed_at: timestamp({ withTimezone: true, mode: 'string' }), + created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + }, + table => [ + index('IDX_github_install_states_expires_at').on(table.expires_at), + check('github_install_states_owner_type_check', sql`${table.owner_type} IN ('org', 'user')`), + ] +); + +export type GitHubInstallState = typeof github_install_states.$inferSelect; + +// C14: native admission attestation +export const native_admission_challenges = pgTable( + 'native_admission_challenges', + { + challenge: text().primaryKey().notNull(), + expires_at: timestamp({ withTimezone: true, mode: 'string' }).notNull(), + consumed_at: timestamp({ withTimezone: true, mode: 'string' }), + created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + }, + table => [index('IDX_native_admission_challenges_expires_at').on(table.expires_at)] +); + +export type NativeAdmissionChallenge = typeof native_admission_challenges.$inferSelect; + +export const native_attested_keys = pgTable( + 'native_attested_keys', + { + key_id: text().primaryKey().notNull(), + kilo_user_id: text() + .notNull() + .references(() => kilocode_users.id, { onDelete: 'cascade' }), + platform: text().notNull().$type<'ios' | 'android'>(), + public_key: text().notNull(), + sign_count: integer().notNull().default(0), + last_used_at: timestamp({ withTimezone: true, mode: 'string' }), + attested_at: timestamp({ withTimezone: true, mode: 'string' }).notNull(), + created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + }, + table => [ + index('IDX_native_attested_keys_kilo_user_id').on(table.kilo_user_id), + check('native_attested_keys_platform_check', sql`${table.platform} IN ('ios', 'android')`), + ] +); + +export type NativeAttestedKey = typeof native_attested_keys.$inferSelect; export type NewContainerUsageSegment = typeof container_usage_segment.$inferInsert; diff --git a/packages/worker-utils/src/kilo-token-auth.test.ts b/packages/worker-utils/src/kilo-token-auth.test.ts index 484eb46534..a9e8b12918 100644 --- a/packages/worker-utils/src/kilo-token-auth.test.ts +++ b/packages/worker-utils/src/kilo-token-auth.test.ts @@ -1,15 +1,19 @@ import { beforeEach, describe, expect, it } from 'vitest'; +import { SignJWT } from 'jose'; import { clearSecretCacheForTest } from './cached-secret'; import { signKiloToken } from './kilo-token'; -import { verifyKiloBearerAgainstCurrentPepper } from './kilo-token-auth'; +import { verifyKiloBearerAgainstCurrentPepper, type KiloUserPepperResult } from './kilo-token-auth'; const TEST_JWT_SECRET = 'test-secret-that-is-long-enough-for-hs256'; -const currentPepperByUserId = new Map(); +const userResultByUserId = new Map(); -async function getUserPepper(_connectionString: string, userId: string) { - return currentPepperByUserId.has(userId) ? currentPepperByUserId.get(userId) : undefined; +async function getUserPepper( + _connectionString: string, + userId: string +): Promise { + return userResultByUserId.has(userId) ? userResultByUserId.get(userId)! : undefined; } async function signToken(params: { @@ -39,8 +43,8 @@ function verifyToken(token: string | null) { describe('verifyKiloBearerAgainstCurrentPepper', () => { beforeEach(() => { clearSecretCacheForTest(); - currentPepperByUserId.clear(); - currentPepperByUserId.set('user-xyz-789', 'pepper-current'); + userResultByUserId.clear(); + userResultByUserId.set('user-xyz-789', { pepper: 'pepper-current', blockedReason: null }); }); it('accepts a token with the current user pepper', async () => { @@ -56,15 +60,85 @@ describe('verifyKiloBearerAgainstCurrentPepper', () => { }); it('rejects tokens for missing users', async () => { - currentPepperByUserId.clear(); + userResultByUserId.clear(); const { token } = await signToken({ pepper: 'pepper-current', tokenSource: 'kilo-chat' }); await expect(verifyToken(token)).resolves.toBeNull(); }); + it('rejects tokens when getUserPepper returns null', async () => { + userResultByUserId.clear(); + // Simulate a custom getUserPepper that returns null instead of undefined + await expect( + verifyKiloBearerAgainstCurrentPepper({ + token: await signToken({ pepper: 'pepper-current', tokenSource: 'kilo-chat' }).then( + ({ token }) => token + ), + nextAuthSecret: { get: async () => TEST_JWT_SECRET }, + workerEnv: 'production', + connectionString: 'postgres://test', + getUserPepper: async () => null, + }) + ).resolves.toBeNull(); + }); + it('rejects tokens with stale peppers', async () => { const { token } = await signToken({ pepper: 'pepper-stale', tokenSource: 'kilo-chat' }); await expect(verifyToken(token)).resolves.toBeNull(); }); + + it('rejects tokens for blocked users (pepper matches, but blocked_reason is set)', async () => { + userResultByUserId.set('user-xyz-789', { + pepper: 'pepper-current', + blockedReason: 'manual block', + }); + const { token } = await signToken({ pepper: 'pepper-current', tokenSource: 'kilo-chat' }); + + await expect(verifyToken(token)).resolves.toBeNull(); + }); + + it('rejects tokens for blocked users even when the stored pepper is null', async () => { + userResultByUserId.set('user-xyz-789', { + pepper: null, + blockedReason: 'soft-deleted at 2026-01-01T00:00:00.000Z', + }); + const { token } = await signToken({ pepper: null, tokenSource: 'kilo-chat' }); + + await expect(verifyToken(token)).resolves.toBeNull(); + }); + + it('accepts tokens when blockedReason is null and pepper matches', async () => { + // Explicitly confirm null blockedReason + matching pepper passes. + userResultByUserId.set('user-xyz-789', { pepper: 'pepper-current', blockedReason: null }); + const { token } = await signToken({ pepper: 'pepper-current', tokenSource: 'kilo-chat' }); + + await expect(verifyToken(token)).resolves.toEqual({ userId: 'user-xyz-789' }); + }); +}); + +describe('C15 deviceSessionId compatibility', () => { + beforeEach(() => { + clearSecretCacheForTest(); + userResultByUserId.clear(); + userResultByUserId.set('user-xyz-789', { pepper: 'pepper-current', blockedReason: null }); + }); + + it('accepts a token carrying deviceSessionId claim', async () => { + const now = Math.floor(Date.now() / 1000); + const token = await new SignJWT({ + version: 3, + kiloUserId: 'user-xyz-789', + apiTokenPepper: 'pepper-current', + env: 'production', + tokenSource: 'kilo-chat', + deviceSessionId: 'session-abc-123', + }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt(now) + .setExpirationTime(now + 3600) + .sign(new TextEncoder().encode(TEST_JWT_SECRET)); + + await expect(verifyToken(token)).resolves.toEqual({ userId: 'user-xyz-789' }); + }); }); diff --git a/packages/worker-utils/src/kilo-token-auth.ts b/packages/worker-utils/src/kilo-token-auth.ts index 7b6088a2fd..6f4f8b9cd4 100644 --- a/packages/worker-utils/src/kilo-token-auth.ts +++ b/packages/worker-utils/src/kilo-token-auth.ts @@ -13,23 +13,29 @@ export type KiloSecretBinding = { get(): Promise; }; +export type KiloUserPepperResult = { pepper: string | null; blockedReason: string | null }; + export type GetKiloUserPepper = ( connectionString: string, userId: string -) => Promise; +) => Promise; export async function findKiloUserPepper( connectionString: string, userId: string -): Promise { +): Promise { const db = getWorkerDb(connectionString); const rows = await db - .select({ api_token_pepper: kilocode_users.api_token_pepper }) + .select({ + api_token_pepper: kilocode_users.api_token_pepper, + blocked_reason: kilocode_users.blocked_reason, + }) .from(kilocode_users) .where(eq(kilocode_users.id, userId)) .limit(1); const row = rows[0]; - return row ? (row.api_token_pepper ?? null) : undefined; + if (!row) return undefined; + return { pepper: row.api_token_pepper ?? null, blockedReason: row.blocked_reason }; } export async function verifyKiloBearerAgainstCurrentPepper(params: { @@ -50,11 +56,20 @@ export async function verifyKiloBearerAgainstCurrentPepper(params: { return null; } - const currentPepper = await getUserPepper(params.connectionString, payload.kiloUserId); + const result = await getUserPepper(params.connectionString, payload.kiloUserId); + if (!result) { + return null; + } + const tokenPepper = payload.apiTokenPepper ?? null; - if (currentPepper === undefined || currentPepper !== tokenPepper) { + if (result.pepper !== tokenPepper) { return null; } + + if (result.blockedReason !== null) { + return null; + } + return { userId: payload.kiloUserId }; } catch { return null; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1c046df15..1545688430 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -306,6 +306,9 @@ importers: '@expo-google-fonts/jetbrains-mono': specifier: 0.4.1 version: 0.4.1 + '@expo/app-integrity': + specifier: 57.0.1 + version: 57.0.1(expo@57.0.9)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) '@expo/react-native-action-sheet': specifier: 4.1.1 version: 4.1.1(@types/react@19.2.14)(react@19.2.3) @@ -812,6 +815,12 @@ importers: '@paper-design/shaders-react': specifier: ^0.0.76 version: 0.0.76(@types/react@19.2.14)(react@19.2.6) + '@peculiar/asn1-schema': + specifier: 2.8.0 + version: 2.8.0 + '@peculiar/asn1-x509': + specifier: 2.8.0 + version: 2.8.0 '@qdrant/js-client-rest': specifier: 1.17.0 version: 1.17.0(typescript@5.9.3) @@ -935,6 +944,12 @@ importers: archiver: specifier: 7.0.1 version: 7.0.1 + asn1js: + specifier: 3.0.6 + version: 3.0.6 + cbor2: + specifier: 2.3.0 + version: 2.3.0 chat: specifier: 4.27.0 version: 4.27.0 @@ -4550,6 +4565,10 @@ packages: resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} + '@cto.af/wtf8@0.0.5': + resolution: {integrity: sha512-LfUFi+Vv4eDzj+XAtR89e3wwjXA/NZjUSwU5NhwbBrLecxPaBYFy3exCuc1j+D4UZeOVdqlsl8G7LmOt18V0tg==} + engines: {node: '>=20'} + '@date-fns/tz@1.5.0': resolution: {integrity: sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==} @@ -4654,11 +4673,11 @@ packages: '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.hirok.io' + deprecated: 'Merged into tsx: https://tsx.is' '@esbuild-kit/esm-loader@2.6.5': resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.hirok.io' + deprecated: 'Merged into tsx: https://tsx.is' '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} @@ -4869,6 +4888,12 @@ packages: '@expo-google-fonts/material-symbols@0.4.27': resolution: {integrity: sha512-cnb3DZnWUWpezGFkJ8y4MT5f/lw6FcgDzeJzic+T+vpQHLHG1cg3SC3i1w1i8Bk4xKR4HPY3t9iIRNvtr5ml8A==} + '@expo/app-integrity@57.0.1': + resolution: {integrity: sha512-UkE1HjgXz15CEB3WoFUa3sQOsTP2INcjAgaHv9FVjPa6y/jRQ0Ey4YNLwDM8MsWbHDYLYP2ul3E9oatROznRaQ==} + peerDependencies: + expo: '*' + react-native: '*' + '@expo/cli@57.0.11': resolution: {integrity: sha512-ENXCwRyL8Q9qbabUmm0L6w9kXTmdGotW7eDEEWmUDXLPux+nEzNDqw0MzWQ5K3ZsXS51QmUJZg5yETB+6SnNRg==} hasBin: true @@ -6974,6 +6999,15 @@ packages: '@paper-design/shaders@0.0.76': resolution: {integrity: sha512-AcNDY4J66YQHUfQYFInkCP7M9VOje0od7wLpOR7LtCmc532opJy6ll+h1W9zBovz8tt9U7OADUmJ/qKEXyOX/A==} + '@peculiar/asn1-schema@2.8.0': + resolution: {integrity: sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==} + + '@peculiar/asn1-x509@2.8.0': + resolution: {integrity: sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg==} + + '@peculiar/utils@2.0.3': + resolution: {integrity: sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -10539,6 +10573,14 @@ packages: asn1.js@5.4.1: resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + asn1js@3.0.10: + resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==} + engines: {node: '>=12.0.0'} + + asn1js@3.0.6: + resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==} + engines: {node: '>=12.0.0'} + assert@2.1.0: resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} @@ -10981,6 +11023,10 @@ packages: resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} engines: {node: '>=4'} + cbor2@2.3.0: + resolution: {integrity: sha512-76WB3hq8BoaGkMkBVJ27fW5LJU+qqDLEpgRNCG/SYKhODWXpVPOTD4UcUto3IEzYLA52nsvbhb0wabhHDn3qXg==} + engines: {node: '>=20'} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -16139,6 +16185,13 @@ packages: pure-rand@8.4.0: resolution: {integrity: sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==} + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.1.5: + resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} + engines: {node: '>=16.0.0'} + qrcode@1.5.4: resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} engines: {node: '>=10.13.0'} @@ -20733,6 +20786,8 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} + '@cto.af/wtf8@0.0.5': {} + '@date-fns/tz@1.5.0': {} '@dependents/detective-less@5.0.1': @@ -21014,6 +21069,11 @@ snapshots: '@expo-google-fonts/material-symbols@0.4.27': {} + '@expo/app-integrity@57.0.1(expo@57.0.9)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))': + dependencies: + expo: 57.0.9(@babel/core@7.29.7)(@expo/metro-runtime@57.0.8)(bufferutil@4.1.0)(expo-router@57.0.9)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + react-native: 0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + '@expo/cli@57.0.11(@expo/metro-runtime@57.0.8)(bufferutil@4.1.0)(expo-constants@57.0.8)(expo-font@57.0.1)(expo-router@57.0.9)(expo@57.0.9)(react-dom@19.2.6(react@19.2.3))(react-native@0.86.2(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)': dependencies: '@expo/code-signing-certificates': 0.0.6 @@ -23392,6 +23452,23 @@ snapshots: '@paper-design/shaders@0.0.76': {} + '@peculiar/asn1-schema@2.8.0': + dependencies: + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-x509@2.8.0': + dependencies: + '@peculiar/asn1-schema': 2.8.0 + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/utils@2.0.3': + dependencies: + tslib: 2.8.1 + '@pinojs/redact@0.4.0': {} '@pkgr/core@0.2.9': {} @@ -27402,6 +27479,18 @@ snapshots: minimalistic-assert: 1.0.1 safer-buffer: 2.1.2 + asn1js@3.0.10: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.1.5 + tslib: 2.8.1 + + asn1js@3.0.6: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.1.5 + tslib: 2.8.1 + assert@2.1.0: dependencies: call-bind: 1.0.8 @@ -27956,6 +28045,10 @@ snapshots: case-sensitive-paths-webpack-plugin@2.4.0: {} + cbor2@2.3.0: + dependencies: + '@cto.af/wtf8': 0.0.5 + ccount@2.0.1: {} chai@5.3.3: @@ -34458,6 +34551,12 @@ snapshots: pure-rand@8.4.0: {} + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.1.5: {} + qrcode@1.5.4: dependencies: dijkstrajs: 1.0.3 diff --git a/services/cloud-agent-next/src/auth.test.ts b/services/cloud-agent-next/src/auth.test.ts index a7319d329a..98b17afa10 100644 --- a/services/cloud-agent-next/src/auth.test.ts +++ b/services/cloud-agent-next/src/auth.test.ts @@ -169,4 +169,17 @@ describe('validateWrapperDispatchTicket', () => { error: 'Invalid ticket signature', }); }); + + it('accepts a legacy Kilo JWT carrying deviceSessionId claim', async () => { + const legacyToken = jwt.sign( + { version: 3, kiloUserId: 'user-1', deviceSessionId: 'session-xyz-999' }, + secret, + { algorithm: 'HS256', expiresIn: '1 minute' } + ); + + await expect(validateWrapperDispatchTicket(`Bearer ${legacyToken}`, secret)).resolves.toEqual({ + success: true, + claims: { type: 'legacy_kilo_token', userId: 'user-1' }, + }); + }); }); diff --git a/services/event-service/src/__tests__/auth.test.ts b/services/event-service/src/__tests__/auth.test.ts index 4fea7e8891..f512a4cc66 100644 --- a/services/event-service/src/__tests__/auth.test.ts +++ b/services/event-service/src/__tests__/auth.test.ts @@ -1,9 +1,10 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { clearSecretCacheForTest, signKiloToken } from '@kilocode/worker-utils'; +import { type KiloUserPepperResult } from '@kilocode/worker-utils/kilo-token-auth'; import { type AuthEnv, authenticateToken } from '../auth'; const TEST_JWT_SECRET = 'test-secret-that-is-long-enough-for-hs256'; -const currentPepperByUserId = new Map(); +const currentPepperByUserId = new Map(); function makeEnv(): AuthEnv { return { @@ -13,8 +14,11 @@ function makeEnv(): AuthEnv { }; } -async function getUserPepper(_connectionString: string, userId: string) { - return currentPepperByUserId.get(userId); +async function getUserPepper( + _connectionString: string, + userId: string +): Promise { + return currentPepperByUserId.has(userId) ? currentPepperByUserId.get(userId)! : undefined; } function authenticateTestToken(token: string | null) { @@ -25,7 +29,7 @@ describe('authenticateToken', () => { beforeEach(() => { clearSecretCacheForTest(); currentPepperByUserId.clear(); - currentPepperByUserId.set('user-xyz-789', 'pepper-current'); + currentPepperByUserId.set('user-xyz-789', { pepper: 'pepper-current', blockedReason: null }); }); it('authenticates a kilo-chat token with the current pepper', async () => { @@ -81,4 +85,21 @@ describe('authenticateToken', () => { await expect(authenticateTestToken(token)).resolves.toBeNull(); }); + + it('rejects a token for a blocked user even when pepper matches', async () => { + currentPepperByUserId.set('user-xyz-789', { + pepper: 'pepper-current', + blockedReason: 'manual block', + }); + const { token } = await signKiloToken({ + userId: 'user-xyz-789', + pepper: 'pepper-current', + secret: TEST_JWT_SECRET, + expiresInSeconds: 3600, + env: 'production', + extra: { tokenSource: 'kilo-chat' }, + }); + + await expect(authenticateTestToken(token)).resolves.toBeNull(); + }); }); diff --git a/services/event-service/src/__tests__/setup.ts b/services/event-service/src/__tests__/setup.ts index f7ff3cd604..2ab00396ca 100644 --- a/services/event-service/src/__tests__/setup.ts +++ b/services/event-service/src/__tests__/setup.ts @@ -5,7 +5,7 @@ vi.mock('@kilocode/db/client', () => ({ select: () => ({ from: () => ({ where: () => ({ - limit: async () => [{ api_token_pepper: null }], + limit: async () => [{ api_token_pepper: null, blocked_reason: null }], }), }), }), diff --git a/services/gastown/src/middleware/kilo-auth.middleware.test.ts b/services/gastown/src/middleware/kilo-auth.middleware.test.ts new file mode 100644 index 0000000000..1e4991c41f --- /dev/null +++ b/services/gastown/src/middleware/kilo-auth.middleware.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from 'vitest'; +import { Hono } from 'hono'; +import { SignJWT } from 'jose'; +import { kiloAuthMiddleware } from './kilo-auth.middleware'; +import type { GastownEnv } from '../gastown.worker'; + +const TEST_SECRET = 'test-secret-that-is-long-enough-for-hs256'; + +function createApp() { + const app = new Hono(); + app.use('/api/*', kiloAuthMiddleware); + app.get('/api/whoami', c => { + return c.json({ kiloUserId: c.get('kiloUserId') }); + }); + return app; +} + +async function signToken(payload: Record) { + const now = Math.floor(Date.now() / 1000); + return new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt(now) + .setExpirationTime(now + 3600) + .sign(new TextEncoder().encode(TEST_SECRET)); +} + +describe('kiloAuthMiddleware', () => { + it('rejects when no token is provided', async () => { + const app = createApp(); + const res = await app.request('/api/whoami', {}, { + NEXTAUTH_SECRET: TEST_SECRET, + } as never); + expect(res.status).toBe(401); + }); + + it('accepts a well-formed Kilo token', async () => { + const app = createApp(); + const token = await signToken({ + version: 3, + kiloUserId: 'user-abc', + env: 'development', + }); + + const res = await app.request( + '/api/whoami', + { headers: { Authorization: `Bearer ${token}` } }, + { NEXTAUTH_SECRET: TEST_SECRET } as never + ); + expect(res.status).toBe(200); + const body = (await res.json()) as { kiloUserId: string }; + expect(body.kiloUserId).toBe('user-abc'); + }); +}); + +describe('C15 deviceSessionId compatibility', () => { + it('accepts a token carrying deviceSessionId claim', async () => { + const app = createApp(); + const token = await signToken({ + version: 3, + kiloUserId: 'user-abc', + apiTokenPepper: null, + env: 'development', + deviceSessionId: 'session-gastown-test', + }); + + const res = await app.request( + '/api/whoami', + { headers: { Authorization: `Bearer ${token}` } }, + { NEXTAUTH_SECRET: TEST_SECRET } as never + ); + expect(res.status).toBe(200); + const body = (await res.json()) as { kiloUserId: string }; + expect(body.kiloUserId).toBe('user-abc'); + }); +}); diff --git a/services/kilo-chat/src/__tests__/auth.test.ts b/services/kilo-chat/src/__tests__/auth.test.ts index 52d50337bd..aae4b2fc5f 100644 --- a/services/kilo-chat/src/__tests__/auth.test.ts +++ b/services/kilo-chat/src/__tests__/auth.test.ts @@ -18,7 +18,12 @@ vi.mock('@kilocode/db/client', () => ({ select: () => ({ from: () => ({ where: () => ({ - limit: async () => [{ api_token_pepper: currentPepperByUserId.get('user-xyz-789') }], + limit: async () => [ + { + api_token_pepper: currentPepperByUserId.get('user-xyz-789'), + blocked_reason: null, + }, + ], }), }), }), diff --git a/services/kiloclaw/src/auth/jwt.test.ts b/services/kiloclaw/src/auth/jwt.test.ts index 1153644686..17f200e18a 100644 --- a/services/kiloclaw/src/auth/jwt.test.ts +++ b/services/kiloclaw/src/auth/jwt.test.ts @@ -126,3 +126,23 @@ describe('validateKiloToken', () => { expect(result.success).toBe(false); }); }); + +describe('C15 deviceSessionId compatibility', () => { + it('accepts a token carrying deviceSessionId claim', async () => { + const token = await signToken({ + kiloUserId: 'user_123', + apiTokenPepper: 'pepper_abc', + version: KILO_TOKEN_VERSION, + env: 'development', + deviceSessionId: 'session-xyz-456', + }); + + const result = await validateKiloToken(token, TEST_SECRET, 'development'); + expect(result).toEqual({ + success: true, + userId: 'user_123', + token, + pepper: 'pepper_abc', + }); + }); +}); diff --git a/services/kiloclaw/src/auth/middleware.test.ts b/services/kiloclaw/src/auth/middleware.test.ts index e9d7ecef83..ccc23b5e31 100644 --- a/services/kiloclaw/src/auth/middleware.test.ts +++ b/services/kiloclaw/src/auth/middleware.test.ts @@ -10,6 +10,7 @@ vi.mock('../db', () => ({ findPepperByUserId: vi.fn(async (_db: unknown, userId: string) => ({ id: userId, api_token_pepper: `pepper_for_${userId}`, + blocked_reason: userId === 'blocked_user' ? 'abuse' : null, })), })); @@ -221,6 +222,56 @@ describe('authMiddleware', () => { }); }); +describe('blocked users', () => { + let app: ReturnType; + + beforeEach(() => { + app = createTestApp(); + }); + + it('rejects a matching-pepper token when blocked_reason is set', async () => { + const token = await signToken({ + kiloUserId: 'blocked_user', + apiTokenPepper: pepperFor('blocked_user'), + version: KILO_TOKEN_VERSION, + }); + + const res = await app.request( + '/protected/whoami', + { headers: { Authorization: `Bearer ${token}` } }, + ENV_WITH_HYPERDRIVE + ); + expect(res.status).toBe(401); + }); +}); + +describe('C15 deviceSessionId compatibility', () => { + let app: ReturnType; + + beforeEach(() => { + app = createTestApp(); + }); + + it('accepts a Bearer token carrying deviceSessionId claim', async () => { + const token = await signToken({ + kiloUserId: 'user_123', + apiTokenPepper: pepperFor('user_123'), + version: KILO_TOKEN_VERSION, + deviceSessionId: 'session-abc-789', + }); + + const res = await app.request( + '/protected/whoami', + { headers: { Authorization: `Bearer ${token}` } }, + ENV_WITH_HYPERDRIVE + ); + expect(res.status).toBe(200); + const body = await jsonBody(res); + expect(body.userId).toBe('user_123'); + expect(body.authToken).toBe(token); + }); +}); + describe('internalApiMiddleware', () => { let app: ReturnType; diff --git a/services/kiloclaw/src/auth/middleware.ts b/services/kiloclaw/src/auth/middleware.ts index 7f4698d1ab..bbd0ede14d 100644 --- a/services/kiloclaw/src/auth/middleware.ts +++ b/services/kiloclaw/src/auth/middleware.ts @@ -13,7 +13,8 @@ import { getWorkerDb, findPepperByUserId } from '../db'; * 2. Fallback: extract from kilo-worker-auth cookie * 3. Verify HS256 with NEXTAUTH_SECRET; check version and env * 4. Validate apiTokenPepper against DB via Hyperdrive - * 5. Set ctx.userId, ctx.authToken on context + * 5. Reject the request when the user is blocked + * 6. Set ctx.userId, ctx.authToken on context */ export async function authMiddleware(c: Context, next: Next) { const secret = c.env.NEXTAUTH_SECRET; @@ -65,6 +66,11 @@ export async function authMiddleware(c: Context, next: Next) { console.warn('[auth] Pepper mismatch for user:', result.userId); return c.json({ error: 'Token revoked' }, 401); } + // A blocked user must not keep access through an unrotated token. + if (user.blocked_reason) { + console.warn('[auth] Blocked user rejected:', result.userId); + return c.json({ error: 'Token revoked' }, 401); + } } catch (err) { console.error('[auth] Pepper validation failed:', err); return c.json({ error: 'Authentication service unavailable' }, 500); diff --git a/services/kiloclaw/src/db/index.ts b/services/kiloclaw/src/db/index.ts index 66bdd7cfff..d7da56c7ff 100644 --- a/services/kiloclaw/src/db/index.ts +++ b/services/kiloclaw/src/db/index.ts @@ -31,6 +31,7 @@ export async function findPepperByUserId(db: WorkerDb, userId: string) { .select({ id: kilocode_users.id, api_token_pepper: kilocode_users.api_token_pepper, + blocked_reason: kilocode_users.blocked_reason, }) .from(kilocode_users) .where(eq(kilocode_users.id, userId)) diff --git a/services/kiloclaw/src/durable-objects/kiloclaw-instance/config.ts b/services/kiloclaw/src/durable-objects/kiloclaw-instance/config.ts index 3eaa488c72..3b4e2afacc 100644 --- a/services/kiloclaw/src/durable-objects/kiloclaw-instance/config.ts +++ b/services/kiloclaw/src/durable-objects/kiloclaw-instance/config.ts @@ -109,6 +109,12 @@ export async function mintFreshApiKey( return null; } + // A blocked account must not receive a fresh API key. + if (user.blocked_reason) { + console.warn('[DO] mintFreshApiKey: user is blocked'); + return null; + } + return signKiloToken({ userId: user.id, pepper: user.api_token_pepper, diff --git a/services/kiloclaw/src/routes/access-gateway.ts b/services/kiloclaw/src/routes/access-gateway.ts index 71d952e882..2fdd06d425 100644 --- a/services/kiloclaw/src/routes/access-gateway.ts +++ b/services/kiloclaw/src/routes/access-gateway.ts @@ -353,6 +353,11 @@ async function redeemCodeAndSetCookie( return { error: 'User not found.', status: 401 }; } + // A blocked account must not receive a fresh gateway credential. + if (user.blocked_reason) { + return { error: 'Access denied', status: 401 }; + } + const token = await signKiloToken({ userId: redeemedUserId, pepper: user.api_token_pepper, diff --git a/services/notifications/src/__tests__/auth.test.ts b/services/notifications/src/__tests__/auth.test.ts index 6b31b780ed..c8e847526f 100644 --- a/services/notifications/src/__tests__/auth.test.ts +++ b/services/notifications/src/__tests__/auth.test.ts @@ -18,7 +18,12 @@ vi.mock('@kilocode/db/client', () => ({ select: () => ({ from: () => ({ where: () => ({ - limit: async () => [{ api_token_pepper: currentPepperByUserId.get('user-xyz-789') }], + limit: async () => [ + { + api_token_pepper: currentPepperByUserId.get('user-xyz-789'), + blocked_reason: null, + }, + ], }), }), }), diff --git a/services/notifications/src/__tests__/setup.ts b/services/notifications/src/__tests__/setup.ts index 064706efc8..284bc5cdd9 100644 --- a/services/notifications/src/__tests__/setup.ts +++ b/services/notifications/src/__tests__/setup.ts @@ -5,7 +5,7 @@ vi.mock('@kilocode/db/client', () => ({ select: () => ({ from: (table: { _: { name: string } }) => ({ where: () => ({ - limit: async () => [{ api_token_pepper: null }], + limit: async () => [{ api_token_pepper: null, blocked_reason: null }], then: (resolve: (rows: unknown[]) => unknown) => { if (table._.name === 'user_push_tokens') return resolve([]); return resolve([]);