From 233d14f56e05bb13c5101694b959e1363a36014b Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Wed, 24 Jun 2026 17:34:27 +0200 Subject: [PATCH 1/6] feat(bitbucket): add Cloud Agent and WAT integrations Includes Bitbucket OAuth setup, repository selection support, token-service integration, worker secret handling, and organization Workspace Access Token authorization. --- .env.local.example | 7 + apps/web/.env.development.local.example | 15 + apps/web/.env.test | 4 + .../gastown/onboarding/OnboardingStepRepo.tsx | 1 + .../integrations/components/PlatformCard.tsx | 2 +- .../bitbucket/callback/route.test.ts | 210 + .../bitbucket/connect/route.test.ts | 72 + .../app/collab/_components/setup-status.ts | 5 +- .../web/src/components/auth/BitbucketLogo.tsx | 12 + .../cloud-agent-next/CloudAgentProvider.tsx | 27 +- .../cloud-agent-next/NewSessionPanel.tsx | 198 +- .../cloud-agent-next/utils/git-utils.test.ts | 42 +- .../cloud-agent-next/utils/git-utils.ts | 31 +- .../cloud-agent/CloudSessionsPage.tsx | 2 +- .../components/gastown/CreateRigDialog.tsx | 2 +- .../integrations/BitbucketConnectSetup.tsx | 325 + .../BitbucketConnectedManagement.tsx | 350 + .../BitbucketIntegrationControls.tsx | 388 + .../BitbucketIntegrationDetails.test.ts | 89 + .../BitbucketIntegrationDetails.tsx | 130 + .../BitbucketRepositoryCacheSection.tsx | 222 + .../integrations/IntegrationDetailPage.tsx | 13 + .../integrations/IntegrationsHub.tsx | 9 +- .../components/shared/RepositoryCombobox.tsx | 21 +- apps/web/src/hooks/useRefreshRepositories.ts | 5 +- .../cloud-agent-next/cloud-agent-client.ts | 8 +- .../bitbucket-integration-helpers.ts | 54 + .../cloud-agent/github-integration-helpers.ts | 15 +- .../cloud-agent/gitlab-integration-helpers.ts | 15 +- apps/web/src/lib/config.server.ts | 8 + .../src/lib/integrations/core/constants.ts | 1 + apps/web/src/lib/integrations/core/types.ts | 16 + .../lib/integrations/github-apps-service.ts | 25 +- .../src/lib/integrations/gitlab-service.ts | 7 +- apps/web/src/lib/integrations/oauth/common.ts | 4 +- .../src/lib/integrations/oauth/paths.test.ts | 9 + apps/web/src/lib/integrations/oauth/paths.ts | 1 + .../oauth/platforms/bitbucket-callback.ts | 165 + .../oauth/platforms/bitbucket-connect.ts | 59 + apps/web/src/lib/integrations/oauth/routes.ts | 10 + .../integrations/platform-definitions.test.ts | 23 + .../lib/integrations/platform-definitions.ts | 45 +- .../platform-integration-setup-status.ts | 2 + .../platforms/bitbucket/adapter.test.ts | 596 + .../platforms/bitbucket/adapter.ts | 315 + .../platforms/bitbucket/credentials.test.ts | 394 + .../platforms/bitbucket/credentials.ts | 227 + .../platforms/bitbucket/metadata.test.ts | 133 + .../platforms/bitbucket/metadata.ts | 44 + .../platforms/bitbucket/oauth-integration.ts | 278 + .../bitbucket/repository-cache.test.ts | 391 + .../platforms/bitbucket/repository-cache.ts | 237 + .../bitbucket/token-service-client.test.ts | 11 + .../bitbucket/token-service-client.ts | 75 + .../workspace-access-token-adapter.test.ts | 483 + .../workspace-access-token-adapter.ts | 562 + ...workspace-access-token-credentials.test.ts | 1134 + .../workspace-access-token-credentials.ts | 544 + ...access-token-organization-authorization.ts | 84 + ...pace-access-token-repository-cache.test.ts | 764 + ...workspace-access-token-repository-cache.ts | 517 + .../installation-repositories-handler.ts | 7 +- .../integrations/validate-return-path.test.ts | 8 + .../lib/integrations/validate-return-path.ts | 19 +- .../security-agent/router/shared-handlers.ts | 11 +- .../slack-bot/github-repository-context.ts | 8 +- .../slack-bot/gitlab-repository-context.ts | 8 +- apps/web/src/lib/token.test.ts | 11 + apps/web/src/lib/tokens.ts | 22 +- apps/web/src/lib/user/index.test.ts | 200 + apps/web/src/lib/user/index.ts | 18 + apps/web/src/routers/bitbucket-router.test.ts | 196 + apps/web/src/routers/bitbucket-router.ts | 95 + .../routers/cloud-agent-next-router.test.ts | 30 +- .../src/routers/cloud-agent-next-router.ts | 4 +- .../routers/cloud-agent-next-schemas.test.ts | 18 + .../src/routers/cloud-agent-next-schemas.ts | 29 +- apps/web/src/routers/github-apps-router.ts | 3 +- apps/web/src/routers/gitlab-router.ts | 3 +- .../organization-bitbucket-router.test.ts | 554 + .../organization-bitbucket-router.ts | 227 + ...ganization-cloud-agent-next-router.test.ts | 99 +- .../organization-cloud-agent-next-router.ts | 37 +- .../organizations/organization-router.ts | 2 + .../platform-integrations-router.test.ts | 45 +- .../routers/platform-integrations-router.ts | 11 +- apps/web/src/routers/root-router.test.ts | 12 + .../migrations/0173_wealthy_johnny_blaze.sql | 58 + .../db/src/migrations/meta/0173_snapshot.json | 32152 ++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 7 + packages/db/src/schema-types.ts | 5 +- packages/db/src/schema.test.ts | 252 + packages/db/src/schema.ts | 116 +- packages/worker-utils/package.json | 2 + .../bitbucket-workspace-access-token.test.ts | 126 + .../src/bitbucket-workspace-access-token.ts | 85 + packages/worker-utils/src/index.ts | 1 + .../src/internal-service-token-audiences.ts | 1 + packages/worker-utils/src/kilo-token.test.ts | 17 + packages/worker-utils/src/kilo-token.ts | 18 +- .../src/persistence/CloudAgentSession.ts | 33 +- .../src/persistence/session-metadata.test.ts | 28 + .../src/persistence/session-metadata.ts | 11 + .../src/router/handlers/session-management.ts | 24 +- .../src/router/handlers/session-prepare.ts | 46 +- .../src/router/handlers/session-start.ts | 36 +- .../src/router/schemas.test.ts | 33 + .../cloud-agent-next/src/router/schemas.ts | 36 +- .../services/git-token-service-client.test.ts | 79 + .../src/services/git-token-service-client.ts | 43 +- .../src/session-service.test.ts | 84 +- .../cloud-agent-next/src/session-service.ts | 74 +- .../src/session/session-requests.ts | 7 + .../validate-repository-access.test.ts | 116 + .../src/session/validate-repository-access.ts | 33 + .../src/shared/wrapper-bootstrap.ts | 3 +- services/cloud-agent-next/src/types.ts | 27 +- .../cloud-agent-next/src/workspace.test.ts | 38 + services/cloud-agent-next/src/workspace.ts | 25 +- .../wrapper/src/session-bootstrap.test.ts | 36 + .../wrapper/src/session-bootstrap.ts | 5 +- services/git-token-service/.dev.vars.example | 16 +- .../src/bitbucket-api.test.ts | 710 + .../git-token-service/src/bitbucket-api.ts | 343 + .../bitbucket-authorization-service.test.ts | 372 + .../src/bitbucket-authorization-service.ts | 492 + .../bitbucket-runtime-token-resolver.test.ts | 284 + .../src/bitbucket-runtime-token-resolver.ts | 280 + .../src/bitbucket-url.test.ts | 78 + .../git-token-service/src/bitbucket-url.ts | 50 + ...access-token-authorization-service.test.ts | 429 + ...pace-access-token-authorization-service.ts | 561 + ...thub-user-authorization-entrypoint.test.ts | 23 +- services/git-token-service/src/index.test.ts | 130 +- services/git-token-service/src/index.ts | 63 +- .../worker-configuration.d.ts | 1008 +- services/git-token-service/wrangler.jsonc | 30 + services/security-sync/src/sync.ts | 6 +- 138 files changed, 49396 insertions(+), 251 deletions(-) create mode 100644 apps/web/src/app/api/integrations/bitbucket/callback/route.test.ts create mode 100644 apps/web/src/app/api/integrations/bitbucket/connect/route.test.ts create mode 100644 apps/web/src/components/auth/BitbucketLogo.tsx create mode 100644 apps/web/src/components/integrations/BitbucketConnectSetup.tsx create mode 100644 apps/web/src/components/integrations/BitbucketConnectedManagement.tsx create mode 100644 apps/web/src/components/integrations/BitbucketIntegrationControls.tsx create mode 100644 apps/web/src/components/integrations/BitbucketIntegrationDetails.test.ts create mode 100644 apps/web/src/components/integrations/BitbucketIntegrationDetails.tsx create mode 100644 apps/web/src/components/integrations/BitbucketRepositoryCacheSection.tsx create mode 100644 apps/web/src/lib/cloud-agent/bitbucket-integration-helpers.ts create mode 100644 apps/web/src/lib/integrations/oauth/paths.test.ts create mode 100644 apps/web/src/lib/integrations/oauth/platforms/bitbucket-callback.ts create mode 100644 apps/web/src/lib/integrations/oauth/platforms/bitbucket-connect.ts create mode 100644 apps/web/src/lib/integrations/platform-definitions.test.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/adapter.test.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/adapter.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/credentials.test.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/credentials.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/metadata.test.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/metadata.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/oauth-integration.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/repository-cache.test.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/repository-cache.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/token-service-client.test.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/token-service-client.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-adapter.test.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-adapter.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-credentials.test.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-credentials.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-organization-authorization.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-repository-cache.test.ts create mode 100644 apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-repository-cache.ts create mode 100644 apps/web/src/routers/bitbucket-router.test.ts create mode 100644 apps/web/src/routers/bitbucket-router.ts create mode 100644 apps/web/src/routers/organizations/organization-bitbucket-router.test.ts create mode 100644 apps/web/src/routers/organizations/organization-bitbucket-router.ts create mode 100644 packages/db/src/migrations/0173_wealthy_johnny_blaze.sql create mode 100644 packages/db/src/migrations/meta/0173_snapshot.json create mode 100644 packages/worker-utils/src/bitbucket-workspace-access-token.test.ts create mode 100644 packages/worker-utils/src/bitbucket-workspace-access-token.ts create mode 100644 packages/worker-utils/src/internal-service-token-audiences.ts create mode 100644 services/cloud-agent-next/src/session/validate-repository-access.test.ts create mode 100644 services/cloud-agent-next/src/session/validate-repository-access.ts create mode 100644 services/git-token-service/src/bitbucket-api.test.ts create mode 100644 services/git-token-service/src/bitbucket-api.ts create mode 100644 services/git-token-service/src/bitbucket-authorization-service.test.ts create mode 100644 services/git-token-service/src/bitbucket-authorization-service.ts create mode 100644 services/git-token-service/src/bitbucket-runtime-token-resolver.test.ts create mode 100644 services/git-token-service/src/bitbucket-runtime-token-resolver.ts create mode 100644 services/git-token-service/src/bitbucket-url.test.ts create mode 100644 services/git-token-service/src/bitbucket-url.ts create mode 100644 services/git-token-service/src/bitbucket-workspace-access-token-authorization-service.test.ts create mode 100644 services/git-token-service/src/bitbucket-workspace-access-token-authorization-service.ts diff --git a/.env.local.example b/.env.local.example index 1e7ab79c21..165cebb48a 100644 --- a/.env.local.example +++ b/.env.local.example @@ -50,6 +50,9 @@ GOOGLE_CLIENT_SECRET=your-google-client-secret # GitLab OAuth - create at https://gitlab.com/-/profile/applications GITLAB_CLIENT_ID=your-gitlab-client-id GITLAB_CLIENT_SECRET=your-gitlab-client-secret +# Bitbucket Cloud OAuth consumer - create in Bitbucket workspace settings +BITBUCKET_CLIENT_ID= +BITBUCKET_CLIENT_SECRET= # LinkedIn OAuth - create at https://www.linkedin.com/developers/apps LINKEDIN_CLIENT_ID=your-linkedin-client-id LINKEDIN_CLIENT_SECRET=your-linkedin-client-secret @@ -141,6 +144,10 @@ CREDIT_CATEGORIES_ENCRYPTION_KEY= USER_GITHUB_APP_TOKEN_ACTIVE_KEY_ID= # Base64-encoded PEM public key; keep the matching private key only in git-token-service USER_GITHUB_APP_TOKEN_ACTIVE_PUBLIC_KEY= +# Bitbucket OAuth credential envelope encryption (dedicated RSA public key only in Web) +BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID= +# Base64-encoded PEM public key; keep the matching private key only in git-token-service +BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY= # Agent environment vars encryption (RSA public key, base64 encoded) AGENT_ENV_VARS_PUBLIC_KEY= # User deployments diff --git a/apps/web/.env.development.local.example b/apps/web/.env.development.local.example index d7ef403376..64a4a688e4 100644 --- a/apps/web/.env.development.local.example +++ b/apps/web/.env.development.local.example @@ -37,6 +37,21 @@ APP_BUILDER_URL=http://localhost:8790 # @url kiloclaw KILOCLAW_API_URL=http://localhost:8795 +# @url cloudflare-git-token-service +GIT_TOKEN_SERVICE_API_URL=http://localhost:8802 + +# @from BITBUCKET_CLIENT_ID +BITBUCKET_CLIENT_ID= + +# @from BITBUCKET_CLIENT_SECRET +BITBUCKET_CLIENT_SECRET= + +# @from BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID +BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID= + +# @from BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY +BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY= + # @url cloudflare-session-ingest SESSION_INGEST_WORKER_URL=http://localhost:8800 diff --git a/apps/web/.env.test b/apps/web/.env.test index e24e702e51..ecb9e25d76 100644 --- a/apps/web/.env.test +++ b/apps/web/.env.test @@ -21,6 +21,10 @@ IS_IN_AUTOMATED_TEST=1 GITHUB_CLIENT_SECRET=dummy-test-github-client-secret GITLAB_CLIENT_ID= GITLAB_CLIENT_SECRET= +BITBUCKET_CLIENT_ID= +BITBUCKET_CLIENT_SECRET= +BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID= +BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY= GOOGLE_CLIENT_SECRET= GOOGLE_CLIENT_ID= GITHUB_CLIENT_ID=dummy-test-github-client-id diff --git a/apps/web/src/app/(app)/gastown/onboarding/OnboardingStepRepo.tsx b/apps/web/src/app/(app)/gastown/onboarding/OnboardingStepRepo.tsx index ce92d0866a..b396541b45 100644 --- a/apps/web/src/app/(app)/gastown/onboarding/OnboardingStepRepo.tsx +++ b/apps/web/src/app/(app)/gastown/onboarding/OnboardingStepRepo.tsx @@ -89,6 +89,7 @@ export function OnboardingStepRepo() { if (!repo) return; const platform = repo.platform ?? 'github'; + if (platform === 'bitbucket') return; const gitlabInstanceUrl = (gitlabReposQuery.data as { instanceUrl?: string } | undefined) ?.instanceUrl; const gitUrl = resolveGitUrlFromRepo(platform, fullName, gitlabInstanceUrl); diff --git a/apps/web/src/app/(app)/organizations/[id]/integrations/components/PlatformCard.tsx b/apps/web/src/app/(app)/organizations/[id]/integrations/components/PlatformCard.tsx index 7f2947cd2f..441306f929 100644 --- a/apps/web/src/app/(app)/organizations/[id]/integrations/components/PlatformCard.tsx +++ b/apps/web/src/app/(app)/organizations/[id]/integrations/components/PlatformCard.tsx @@ -81,7 +81,7 @@ export function PlatformCard({ platform, githubIdentityStatus, onNavigate }: Pla return ( ({ + ensureOrganizationAccess: jest.fn(), +})); +jest.mock('@/lib/integrations/platforms/bitbucket/adapter', () => ({ + exchangeBitbucketOAuthCode: jest.fn(), + fetchBitbucketUser: jest.fn(), + fetchBitbucketWorkspaces: jest.fn(), +})); +jest.mock('@/lib/integrations/platforms/bitbucket/credentials', () => ({ + BitbucketIntegrationAuthorizationError: class BitbucketIntegrationAuthorizationError extends Error {}, + BitbucketIntegrationConnectionConflictError: class BitbucketIntegrationConnectionConflictError extends Error {}, + storeBitbucketIntegration: jest.fn(), +})); +jest.mock('@/lib/integrations/platforms/bitbucket/repository-cache', () => ({ + scheduleBitbucketRepositoryCachePrime: jest.fn(), +})); +jest.mock('@sentry/nextjs', () => ({ + captureException: jest.fn(), + captureMessage: jest.fn(), +})); + +const mockedCaptureException = jest.mocked(captureException); +const mockedGetUserFromAuth = jest.mocked(getUserFromAuth); +const mockedExchangeBitbucketOAuthCode = jest.mocked(exchangeBitbucketOAuthCode); +const mockedFetchBitbucketUser = jest.mocked(fetchBitbucketUser); +const mockedFetchBitbucketWorkspaces = jest.mocked(fetchBitbucketWorkspaces); +const mockedStoreBitbucketIntegration = jest.mocked(storeBitbucketIntegration); +const mockedScheduleBitbucketRepositoryCachePrime = jest.mocked( + scheduleBitbucketRepositoryCachePrime +); + +const USER_ID = '034489e8-19e0-4479-9d69-2edad719e847'; +const ORGANIZATION_ID = '7e3011af-e99d-444f-8171-54c2225b87dc'; +const BITBUCKET_TOKENS = { + accessToken: 'access-token', + refreshToken: 'refresh-token', + tokenType: 'bearer', + expiresIn: 3600, + scopes: ['account', 'email', 'pullrequest', 'repository', 'repository:write', 'webhook'], +} satisfies BitbucketOAuthTokens; +const BITBUCKET_USER = { + uuid: '{bitbucket-user}', + nickname: 'bucket-user', + displayName: 'Bucket User', +}; +const WORKSPACE = { + uuid: '{workspace-one}', + slug: 'workspace-one', + name: 'Workspace One', +}; + +function makeRequest(state: string) { + return new NextRequest( + `http://localhost:3000/api/integrations/bitbucket/callback?code=authorization-code&state=${encodeURIComponent(state)}` + ); +} + +function expectRedirectLocation(response: Response, expectedPathWithQuery: string) { + const location = response.headers.get('location'); + expect(location).toBeTruthy(); + const url = new URL(location ?? ''); + expect(`${url.pathname}${url.search}`).toBe(expectedPathWithQuery); +} + +async function callBitbucketCallbackImplementation(request: NextRequest) { + const { handleBitbucketOAuthCallback } = + await import('@/lib/integrations/oauth/platforms/bitbucket-callback'); + return handleBitbucketOAuthCallback(request); +} + +async function callPublicBitbucketCallback(request: NextRequest) { + const { GET } = await import('../../[platform]/callback/route'); + return GET(request, { params: Promise.resolve({ platform: 'bitbucket' }) }); +} + +describe('GET /api/integrations/bitbucket/callback', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockedGetUserFromAuth.mockResolvedValue({ + user: { id: USER_ID }, + authFailedResponse: null, + } as never); + mockedExchangeBitbucketOAuthCode.mockResolvedValue(BITBUCKET_TOKENS); + mockedFetchBitbucketUser.mockResolvedValue(BITBUCKET_USER); + }); + + test('dispatches the public callback through the Bitbucket OAuth implementation', async () => { + mockedFetchBitbucketWorkspaces.mockResolvedValue([WORKSPACE]); + mockedStoreBitbucketIntegration.mockResolvedValue({ + status: 'connected', + integrationId: 'integration-id', + }); + const state = createOAuthState(`user_${USER_ID}`, USER_ID); + + const response = await callPublicBitbucketCallback(makeRequest(state)); + + expectRedirectLocation(response, '/integrations/bitbucket?success=connected'); + expect(mockedStoreBitbucketIntegration).toHaveBeenCalledWith( + expect.objectContaining({ availableWorkspaces: [WORKSPACE] }) + ); + expect(mockedScheduleBitbucketRepositoryCachePrime).toHaveBeenCalledWith({ + owner: { type: 'user', id: USER_ID }, + kiloUserId: USER_ID, + integrationId: 'integration-id', + }); + }); + + test('keeps the implementation directly testable for personal support', async () => { + mockedFetchBitbucketWorkspaces.mockResolvedValue([WORKSPACE]); + mockedStoreBitbucketIntegration.mockResolvedValue({ + status: 'connected', + integrationId: 'integration-id', + }); + const state = createOAuthState(`user_${USER_ID}`, USER_ID); + + const response = await callBitbucketCallbackImplementation(makeRequest(state)); + + expectRedirectLocation(response, '/integrations/bitbucket?success=connected'); + expect(mockedStoreBitbucketIntegration).toHaveBeenCalledWith( + expect.objectContaining({ availableWorkspaces: [WORKSPACE] }) + ); + expect(mockedScheduleBitbucketRepositoryCachePrime).toHaveBeenCalledWith({ + owner: { type: 'user', id: USER_ID }, + kiloUserId: USER_ID, + integrationId: 'integration-id', + }); + }); + + test('redirects multiple workspaces to explicit selection', async () => { + const secondWorkspace = { + uuid: '{workspace-two}', + slug: 'workspace-two', + name: 'Workspace Two', + }; + mockedFetchBitbucketWorkspaces.mockResolvedValue([WORKSPACE, secondWorkspace]); + mockedStoreBitbucketIntegration.mockResolvedValue({ + status: 'workspace_selection_required', + integrationId: 'integration-id', + }); + const state = createOAuthState(`user_${USER_ID}`, USER_ID); + + const response = await callBitbucketCallbackImplementation(makeRequest(state)); + + expectRedirectLocation( + response, + '/integrations/bitbucket?success=workspace_selection_required' + ); + }); + + test('does not replace an integration when no workspaces are available', async () => { + mockedFetchBitbucketWorkspaces.mockResolvedValue([]); + const state = createOAuthState(`user_${USER_ID}`, USER_ID); + + const response = await callBitbucketCallbackImplementation(makeRequest(state)); + + expectRedirectLocation(response, '/integrations/bitbucket?error=no_workspaces'); + expect(mockedStoreBitbucketIntegration).not.toHaveBeenCalled(); + }); + + test('reports authorization revoked during storage as unauthorized', async () => { + mockedFetchBitbucketWorkspaces.mockResolvedValue([WORKSPACE]); + mockedStoreBitbucketIntegration.mockRejectedValue( + new BitbucketIntegrationAuthorizationError('authorization revoked') + ); + const state = createOAuthState(`org_${ORGANIZATION_ID}`, USER_ID); + + const response = await callBitbucketCallbackImplementation(makeRequest(state)); + + expectRedirectLocation( + response, + `/organizations/${ORGANIZATION_ID}/integrations/bitbucket?error=unauthorized` + ); + expect(mockedCaptureException).not.toHaveBeenCalled(); + }); + + test('reports an existing Bitbucket connection without replacing it', async () => { + mockedFetchBitbucketWorkspaces.mockResolvedValue([WORKSPACE]); + mockedStoreBitbucketIntegration.mockRejectedValue( + new BitbucketIntegrationConnectionConflictError() + ); + const state = createOAuthState(`org_${ORGANIZATION_ID}`, USER_ID); + + const response = await callPublicBitbucketCallback(makeRequest(state)); + + expectRedirectLocation( + response, + `/organizations/${ORGANIZATION_ID}/integrations/bitbucket?error=connection_exists` + ); + expect(mockedCaptureException).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/app/api/integrations/bitbucket/connect/route.test.ts b/apps/web/src/app/api/integrations/bitbucket/connect/route.test.ts new file mode 100644 index 0000000000..dd9722b148 --- /dev/null +++ b/apps/web/src/app/api/integrations/bitbucket/connect/route.test.ts @@ -0,0 +1,72 @@ +import { beforeEach, describe, expect, test } from '@jest/globals'; +import { NextRequest } from 'next/server'; +import { getUserFromAuth } from '@/lib/user/server'; +import { verifyOAuthState } from '@/lib/integrations/oauth-state'; +import { ensureOrganizationAccess } from '@/routers/organizations/utils'; + +jest.mock('@/lib/config.server', () => ({ + BITBUCKET_CLIENT_ID: 'bitbucket-client-id', + NEXTAUTH_SECRET: 'test-nextauth-secret', +})); +jest.mock('@/lib/user/server'); +jest.mock('@/routers/organizations/utils', () => ({ + ensureOrganizationAccess: jest.fn(), +})); +jest.mock('@sentry/nextjs', () => ({ + captureException: jest.fn(), +})); + +const mockedGetUserFromAuth = jest.mocked(getUserFromAuth); +const mockedEnsureOrganizationAccess = jest.mocked(ensureOrganizationAccess); +const USER_ID = '034489e8-19e0-4479-9d69-2edad719e847'; +const ORGANIZATION_ID = '7e3011af-e99d-444f-8171-54c2225b87dc'; + +async function callPublicBitbucketConnect() { + const { GET } = await import('../../[platform]/connect/route'); + return GET( + new NextRequest( + `http://localhost:3000/api/integrations/bitbucket/connect?organizationId=${ORGANIZATION_ID}&returnTo=%2Forganizations%2F${ORGANIZATION_ID}%2Fintegrations%2Fbitbucket` + ), + { + params: Promise.resolve({ platform: 'bitbucket' }), + } + ); +} + +describe('GET /api/integrations/bitbucket/connect', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockedGetUserFromAuth.mockResolvedValue({ + user: { id: USER_ID }, + authFailedResponse: null, + } as never); + mockedEnsureOrganizationAccess.mockResolvedValue('owner'); + }); + + test('dispatches the public OAuth route with organization ownership and required scopes', async () => { + const response = await callPublicBitbucketConnect(); + + expect(response.status).toBe(307); + const location = response.headers.get('location'); + expect(location).toBeTruthy(); + const url = new URL(location ?? ''); + expect(`${url.origin}${url.pathname}`).toBe('https://bitbucket.org/site/oauth2/authorize'); + expect(Object.fromEntries(url.searchParams)).toEqual( + expect.objectContaining({ + client_id: 'bitbucket-client-id', + response_type: 'code', + scope: 'account repository:write pullrequest webhook', + }) + ); + expect(verifyOAuthState(url.searchParams.get('state'))).toEqual({ + owner: `org_${ORGANIZATION_ID}`, + userId: USER_ID, + returnTo: `/organizations/${ORGANIZATION_ID}/integrations/bitbucket`, + }); + expect(mockedEnsureOrganizationAccess).toHaveBeenCalledWith( + { user: expect.objectContaining({ id: USER_ID }) }, + ORGANIZATION_ID, + ['owner', 'billing_manager'] + ); + }); +}); diff --git a/apps/web/src/app/collab/_components/setup-status.ts b/apps/web/src/app/collab/_components/setup-status.ts index 91b89bdc3a..6fa73c4b47 100644 --- a/apps/web/src/app/collab/_components/setup-status.ts +++ b/apps/web/src/app/collab/_components/setup-status.ts @@ -1,6 +1,9 @@ import type { PlatformId } from './platforms'; -type SetupStatusPlatformId = Exclude | 'dolthub'; +type SetupStatusPlatformId = + | Exclude + | 'bitbucket' + | 'dolthub'; export type PlatformInstallation = { platform: SetupStatusPlatformId; diff --git a/apps/web/src/components/auth/BitbucketLogo.tsx b/apps/web/src/components/auth/BitbucketLogo.tsx new file mode 100644 index 0000000000..e8651cca7b --- /dev/null +++ b/apps/web/src/components/auth/BitbucketLogo.tsx @@ -0,0 +1,12 @@ +import type { SVGProps } from 'react'; + +export function BitbucketLogo(props: SVGProps) { + return ( + + ); +} diff --git a/apps/web/src/components/cloud-agent-next/CloudAgentProvider.tsx b/apps/web/src/components/cloud-agent-next/CloudAgentProvider.tsx index ebb99502f1..95d1c8654e 100644 --- a/apps/web/src/components/cloud-agent-next/CloudAgentProvider.tsx +++ b/apps/web/src/components/cloud-agent-next/CloudAgentProvider.tsx @@ -227,18 +227,21 @@ export function CloudAgentProvider({ children, organizationId }: CloudAgentProvi }, respondToPermission: async payload => { - const trpc = organizationId - ? trpcClient.organizations.cloudAgentNext - : trpcClient.cloudAgentNext; - await trpc.answerPermission.mutate( - { - ...(organizationId ? { organizationId } : {}), - sessionId: payload.sessionId, - permissionId: payload.requestId, - response: payload.response, - }, - { context: { skipBatch: true } } - ); + const input = { + sessionId: payload.sessionId, + permissionId: payload.requestId, + response: payload.response, + }; + if (organizationId) { + await trpcClient.organizations.cloudAgentNext.answerPermission.mutate( + { ...input, organizationId }, + { context: { skipBatch: true } } + ); + return; + } + await trpcClient.cloudAgentNext.answerPermission.mutate(input, { + context: { skipBatch: true }, + }); }, }, diff --git a/apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx b/apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx index 274b01fe1b..3c53559c9c 100644 --- a/apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx +++ b/apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx @@ -99,10 +99,11 @@ import { } from '@/components/cloud-agent-next/github-identity-hint'; type Repository = { - id: number; + id: string | number; name: string; fullName: string; private: boolean; + workspaceUuid?: string; }; type NewSessionPanelProps = { @@ -394,7 +395,7 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New const displayVariants = hasAgentModelOverride ? [] : availableVariants; // --------------------------------------------------------------------------- - // Repositories (GitHub + GitLab) + // Repositories (GitHub + GitLab + Bitbucket) // --------------------------------------------------------------------------- const { data: githubRepoData, @@ -426,6 +427,17 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New }) ); + const { + data: bitbucketRepoData, + isLoading: isLoadingBitbucketRepos, + error: bitbucketRepoError, + } = useQuery({ + ...trpc.organizations.cloudAgentNext.listBitbucketRepositories.queryOptions({ + organizationId: organizationId ?? '', + }), + enabled: Boolean(organizationId), + }); + const repoUpdatedSince = useMemo(() => startOfDay(subDays(new Date(), 5)).toISOString(), []); const { data: recentRepoData } = useQuery( trpc.cliSessionsV2.recentRepositories.queryOptions({ @@ -434,10 +446,15 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New }) ); - const isLoadingRepos = isLoadingGitHubRepos && isLoadingGitLabRepos; + const isLoadingRepos = + isLoadingGitHubRepos && isLoadingGitLabRepos && (!organizationId || isLoadingBitbucketRepos); const githubRepositories = (githubRepoData?.repositories || []) as Repository[]; const gitlabRepositories = (gitlabRepoData?.repositories || []) as Repository[]; + const bitbucketRepositories = + organizationId && bitbucketRepoData?.status === 'available' + ? bitbucketRepoData.repositories + : []; const unifiedRepositories = useMemo(() => { const github = githubRepositories.map(repo => ({ @@ -452,8 +469,15 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New private: repo.private, platform: 'gitlab' as const, })); - return [...github, ...gitlab]; - }, [githubRepositories, gitlabRepositories]); + const bitbucket = bitbucketRepositories.map(repo => ({ + id: repo.id, + fullName: repo.fullName, + private: repo.private, + platform: 'bitbucket' as const, + workspaceUuid: repo.workspaceUuid, + })); + return [...github, ...gitlab, ...bitbucket]; + }, [githubRepositories, gitlabRepositories, bitbucketRepositories]); const recentRepos = useMemo(() => { const recentList = recentRepoData?.repositories; @@ -474,7 +498,10 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New return result; }, [recentRepoData?.repositories, unifiedRepositories]); - const hasMultiplePlatforms = githubRepositories.length > 0 && gitlabRepositories.length > 0; + const hasMultiplePlatforms = + [githubRepositories, gitlabRepositories, bitbucketRepositories].filter( + repositories => repositories.length > 0 + ).length > 1; const handleRepoSelect = useCallback( (repo: RepositoryOption, userInitiated = true) => { @@ -499,8 +526,10 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New const onlyAvailableRepo = !isLoadingGitHubRepos && !isLoadingGitLabRepos && + (!organizationId || !isLoadingBitbucketRepos) && !githubRepoError && !gitlabRepoError && + (!organizationId || !bitbucketRepoError) && unifiedRepositories.length === 1 ? unifiedRepositories[0] : undefined; @@ -524,8 +553,10 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New organizationId, isLoadingGitHubRepos, isLoadingGitLabRepos, + isLoadingBitbucketRepos, githubRepoError, gitlabRepoError, + bitbucketRepoError, ]); // --------------------------------------------------------------------------- @@ -553,7 +584,8 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New } }, [prompt, isRepoUserSelected, unifiedRepositories]); - const repoError = githubRepoError || gitlabRepoError; + const repoError = + githubRepoError || gitlabRepoError || (organizationId ? bitbucketRepoError : null); const { refresh: refreshGitHubRepositories, isRefreshing: isRefreshingGitHubRepos } = useRefreshRepositories({ @@ -616,7 +648,7 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New const refreshRepositories = useCallback(async () => { try { await Promise.all([refreshGitHubRepositories(), refreshGitLabRepositories()]); - toast.success('Repositories refreshed'); + toast.success('GitHub and GitLab repositories refreshed'); } catch (error) { toast.error('Failed to refresh repositories', { description: error instanceof Error ? error.message : 'Unknown error', @@ -633,7 +665,13 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New !isLoadingGitHubRepos && githubRepoData?.integrationInstalled === false; const gitlabIntegrationMissing = !isLoadingGitLabRepos && gitlabRepoData?.integrationInstalled === false; - const isIntegrationMissing = githubIntegrationMissing && gitlabIntegrationMissing; + const bitbucketIntegrationMissing = + !organizationId || (!isLoadingBitbucketRepos && bitbucketRepoData?.status !== 'available'); + const isIntegrationMissing = + githubIntegrationMissing && gitlabIntegrationMissing && bitbucketIntegrationMissing; + const bitbucketIntegrationHref = organizationId + ? `/organizations/${organizationId}/integrations/bitbucket` + : null; // --------------------------------------------------------------------------- // Repo popover state (must be declared before early returns to satisfy Rules of Hooks) @@ -647,6 +685,9 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New const gitlabRepos = unifiedRepositories.filter( r => r.platform === 'gitlab' && !recentFullNames.has(r.fullName) ); + const bitbucketRepos = unifiedRepositories.filter( + r => r.platform === 'bitbucket' && !recentFullNames.has(r.fullName) + ); const otherRepos = unifiedRepositories.filter( r => !r.platform && !recentFullNames.has(r.fullName) ); @@ -730,6 +771,31 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New setShowRepositoryRequiredMessage(true); return; } + const selectedRepository = unifiedRepositories.find( + repository => repository.fullName === selectedRepo && repository.platform === selectedPlatform + ); + if ( + selectedPlatform === 'bitbucket' && + (!organizationId || + !selectedRepository || + typeof selectedRepository.id !== 'string' || + !selectedRepository.workspaceUuid) + ) { + toast.error('Select the Bitbucket repository again.'); + return; + } + const bitbucketRepo = + organizationId && + selectedPlatform === 'bitbucket' && + selectedRepository && + typeof selectedRepository.id === 'string' && + selectedRepository.workspaceUuid + ? { + fullName: selectedRepository.fullName, + workspaceUuid: selectedRepository.workspaceUuid, + repositoryUuid: selectedRepository.id, + } + : undefined; setIsPreparing(true); @@ -784,25 +850,29 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New gitlabProject: selectedRepo, organizationId, }); - } else { + } else if (selectedPlatform === 'bitbucket' && bitbucketRepo) { result = await trpcClient.organizations.cloudAgentNext.prepareSession.mutate({ ...baseInput, - githubRepo: selectedRepo, + bitbucketRepo, organizationId, }); - } - } else { - if (selectedPlatform === 'gitlab') { - result = await trpcClient.cloudAgentNext.prepareSession.mutate({ - ...baseInput, - gitlabProject: selectedRepo, - }); } else { - result = await trpcClient.cloudAgentNext.prepareSession.mutate({ + result = await trpcClient.organizations.cloudAgentNext.prepareSession.mutate({ ...baseInput, githubRepo: selectedRepo, + organizationId, }); } + } else if (selectedPlatform === 'gitlab') { + result = await trpcClient.cloudAgentNext.prepareSession.mutate({ + ...baseInput, + gitlabProject: selectedRepo, + }); + } else { + result = await trpcClient.cloudAgentNext.prepareSession.mutate({ + ...baseInput, + githubRepo: selectedRepo, + }); } if (!hasAgentModelOverride) { @@ -858,6 +928,7 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New slashCommands, trpc.cliSessionsV2.list, trpcClient, + unifiedRepositories, ]); // --------------------------------------------------------------------------- @@ -934,7 +1005,7 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New const integrationMessage = githubRepoData?.errorMessage || gitlabRepoData?.errorMessage || - 'Connect a GitHub or GitLab integration to select a repository for the cloud agent.'; + 'Connect a source control integration to select a repository for Cloud Agent.'; return (
@@ -945,7 +1016,7 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New
-

Connect GitHub or GitLab to start a session

+

Connect source control to start a session

{integrationMessage}

@@ -1258,8 +1329,40 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New Failed to load repositories
) : unifiedRepositories.length === 0 ? ( -
- No repositories found +
+

No repositories found

+ {organizationId && bitbucketRepoData?.status === 'temporarily_unavailable' && ( +

The Bitbucket repository cache is temporarily unavailable.

+ )} + {organizationId && + bitbucketIntegrationHref && + bitbucketRepoData?.status && + bitbucketRepoData.status !== 'available' && + bitbucketRepoData.status !== 'temporarily_unavailable' && ( + + {bitbucketRepoData.status === 'not_connected' + ? 'Connect a Bitbucket workspace' + : bitbucketRepoData.status === 'reconnect_required' + ? 'Replace the Bitbucket token' + : 'Review the Bitbucket integration'} + + )} + void refreshRepositories()} + disabled={isRefreshingRepos} + className="mx-auto" + > + + {isRefreshingRepos + ? 'Refreshing GitHub and GitLab...' + : 'Refresh GitHub and GitLab'} +
) : ( @@ -1270,13 +1373,43 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New onClick={() => void refreshRepositories()} disabled={isRefreshingRepos} className="text-muted-foreground hover:text-foreground shrink-0 rounded-sm p-1 disabled:opacity-50" - title="Refresh repositories" + aria-label="Refresh GitHub and GitLab repositories" + title="Refresh GitHub and GitLab repositories" >
+ {organizationId && + bitbucketIntegrationHref && + bitbucketRepoData?.status && + bitbucketRepoData.status !== 'available' && ( +
+ {bitbucketRepoData.status === 'temporarily_unavailable' ? ( + + The Bitbucket repository cache is temporarily unavailable.{' '} + + Review the Bitbucket integration + + + ) : ( + + {bitbucketRepoData.status === 'not_connected' + ? 'Connect a Bitbucket workspace to list repositories' + : bitbucketRepoData.status === 'reconnect_required' + ? 'Replace the Bitbucket token to list repositories' + : 'Review Bitbucket repository permissions'} + + )} +
+ )} No repositories match your search {recentRepos.length > 0 && ( @@ -1325,6 +1458,21 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New ))} )} + {bitbucketRepos.length > 0 && ( + + {bitbucketRepos.map(repo => ( + + ))} + + )} {otherRepos.length > 0 && ( {otherRepos.map(repo => ( @@ -1372,7 +1520,7 @@ export function NewSessionPanel({ organizationId, isDevcontainerAvailable }: New selectedOverrideProfileId={selectedProfileId} onOverrideProfileSelect={setSelectedProfileId} repoFullName={selectedRepo || undefined} - platform={selectedPlatform} + platform={selectedPlatform === 'bitbucket' ? undefined : selectedPlatform} devcontainerToggle={ isDevcontainerAvailable ? { diff --git a/apps/web/src/components/cloud-agent-next/utils/git-utils.test.ts b/apps/web/src/components/cloud-agent-next/utils/git-utils.test.ts index ef6bb17610..2a1d759420 100644 --- a/apps/web/src/components/cloud-agent-next/utils/git-utils.test.ts +++ b/apps/web/src/components/cloud-agent-next/utils/git-utils.test.ts @@ -124,8 +124,18 @@ describe('detectGitPlatform', () => { expect(detectGitPlatform('ssh://git@gitlab.com/group/project.git')).toBe('gitlab'); }); - test('Bitbucket returns undefined', () => { - expect(detectGitPlatform('https://bitbucket.org/owner/repo.git')).toBeUndefined(); + test('Bitbucket HTTPS', () => { + expect(detectGitPlatform('https://bitbucket.org/workspace/repo.git')).toBe('bitbucket'); + }); + + test('Bitbucket SSH', () => { + expect(detectGitPlatform('git@bitbucket.org:workspace/repo.git')).toBe('bitbucket'); + }); + + test('Bitbucket browse URL', () => { + expect(detectGitPlatform('https://bitbucket.org/workspace/repo/pull-requests/1')).toBe( + 'bitbucket' + ); }); test('null returns undefined', () => { @@ -186,6 +196,22 @@ describe('extractRepoFromGitUrl', () => { ); }); + test('Bitbucket HTTPS extracts workspace/repo', () => { + expect(extractRepoFromGitUrl('https://bitbucket.org/workspace/repo.git')).toBe( + 'workspace/repo' + ); + }); + + test('Bitbucket SSH extracts workspace/repo', () => { + expect(extractRepoFromGitUrl('git@bitbucket.org:workspace/repo.git')).toBe('workspace/repo'); + }); + + test('Bitbucket browse URL extracts workspace/repo', () => { + expect(extractRepoFromGitUrl('https://bitbucket.org/workspace/repo/pull-requests/1')).toBe( + 'workspace/repo' + ); + }); + test('null returns undefined', () => { expect(extractRepoFromGitUrl(null)).toBeUndefined(); }); @@ -204,6 +230,12 @@ describe('buildPrepareSessionRepoParams', () => { }); }); + test('bitbucket platform requires structured repository identity', () => { + expect( + buildPrepareSessionRepoParams({ repo: 'workspace/repo', platform: 'bitbucket' }) + ).toBeNull(); + }); + test('null repo returns null', () => { expect(buildPrepareSessionRepoParams({ repo: null, platform: 'github' })).toBeNull(); }); @@ -226,6 +258,12 @@ describe('findAllGitPlatformUrls', () => { ).toEqual(['https://gitlab.com/group/project/-/merge_requests/1']); }); + test('extracts Bitbucket URL', () => { + expect( + findAllGitPlatformUrls('See https://bitbucket.org/workspace/repo/pull-requests/1') + ).toEqual(['https://bitbucket.org/workspace/repo/pull-requests/1']); + }); + test('returns all URLs in order when multiple are present', () => { expect(findAllGitPlatformUrls('https://github.com/a/b and https://gitlab.com/c/d')).toEqual([ 'https://github.com/a/b', diff --git a/apps/web/src/components/cloud-agent-next/utils/git-utils.ts b/apps/web/src/components/cloud-agent-next/utils/git-utils.ts index b33b1afce2..7c163d38dc 100644 --- a/apps/web/src/components/cloud-agent-next/utils/git-utils.ts +++ b/apps/web/src/components/cloud-agent-next/utils/git-utils.ts @@ -8,7 +8,7 @@ import { PLATFORM } from '@/lib/integrations/core/constants'; /** - * Extract owner/repo or group/project from a git URL + * Extract owner/repo, group/project, or workspace/repo from a git URL * * Supports formats: * - https://github.com/owner/repo @@ -19,9 +19,11 @@ import { PLATFORM } from '@/lib/integrations/core/constants'; * - https://gitlab.com/group/subgroup/project.git (nested GitLab groups) * - git@gitlab.com:group/project.git * - git@gitlab.com:group/subgroup/project.git (nested GitLab groups) + * - https://bitbucket.org/workspace/repo.git + * - Bitbucket SSH shorthand * * @param gitUrl - The git URL to parse - * @returns owner/repo or group/project format string, or undefined if parsing fails + * @returns Repository path in provider format, or undefined if parsing fails */ export function extractRepoFromGitUrl(gitUrl: string | null | undefined): string | undefined { if (!gitUrl) return undefined; @@ -51,7 +53,7 @@ export function extractRepoFromGitUrl(gitUrl: string | null | undefined): string if (url.hostname === 'gitlab.com') { return fullPath; } - // Fallback for GitHub-style URLs: owner/repo (first two segments) + // GitHub and Bitbucket repository paths use the first two segments. return `${pathParts[0]}/${pathParts[1]}`; } } catch { @@ -61,7 +63,7 @@ export function extractRepoFromGitUrl(gitUrl: string | null | undefined): string return undefined; } -export type GitPlatform = 'github' | 'gitlab'; +export type GitPlatform = 'github' | 'gitlab' | 'bitbucket'; export function buildPrepareSessionRepoParams(options: { repo?: string | null; @@ -70,11 +72,17 @@ export function buildPrepareSessionRepoParams(options: { const repo = options.repo?.trim(); if (!repo) return null; - if (options.platform === PLATFORM.GITLAB) { - return { gitlabProject: repo }; + switch (options.platform) { + case PLATFORM.GITHUB: + return { githubRepo: repo }; + case PLATFORM.GITLAB: + return { gitlabProject: repo }; + case 'bitbucket': + return null; + default: + options.platform satisfies never; + return null; } - - return { githubRepo: repo }; } export function buildRepoBrowseUrl(gitUrl: string | null | undefined): string | undefined { @@ -121,17 +129,20 @@ export function detectGitPlatform(gitUrl: string | null | undefined): GitPlatfor if (hostname === 'github.com') return 'github'; if (hostname === 'gitlab.com') return 'gitlab'; + if (hostname === 'bitbucket.org') return 'bitbucket'; return undefined; } /** - * Find all GitHub or GitLab URLs in free-form text, in order of appearance. + * Find all GitHub, GitLab, or Bitbucket URLs in free-form text, in order of appearance. * * Useful for detecting when a user pastes links to issues, PRs, etc. * Returns all matches so the caller can iterate and pick the first one * that corresponds to a connected repository. */ export function findAllGitPlatformUrls(text: string): string[] { - const matches = text.matchAll(/https?:\/\/(?:github\.com|gitlab\.com)\/[^\s)>\]]+/g); + const matches = text.matchAll( + /https?:\/\/(?:github\.com|gitlab\.com|bitbucket\.org)\/[^\s)>\]]+/g + ); return Array.from(matches, m => m[0].replace(/[.,;:!?]+$/, '')); } diff --git a/apps/web/src/components/cloud-agent/CloudSessionsPage.tsx b/apps/web/src/components/cloud-agent/CloudSessionsPage.tsx index 8f8251b5cb..91dcd8f173 100644 --- a/apps/web/src/components/cloud-agent/CloudSessionsPage.tsx +++ b/apps/web/src/components/cloud-agent/CloudSessionsPage.tsx @@ -618,7 +618,7 @@ export function CloudSessionsPage({ organizationId }: CloudSessionsPageProps) { selectedOverrideProfileId={selectedProfileId} onOverrideProfileSelect={setSelectedProfileId} repoFullName={selectedRepo || undefined} - platform={selectedPlatform} + platform={selectedPlatform === 'bitbucket' ? undefined : selectedPlatform} />
diff --git a/apps/web/src/components/gastown/CreateRigDialog.tsx b/apps/web/src/components/gastown/CreateRigDialog.tsx index 4e3c2c0823..6dda4316f9 100644 --- a/apps/web/src/components/gastown/CreateRigDialog.tsx +++ b/apps/web/src/components/gastown/CreateRigDialog.tsx @@ -106,7 +106,7 @@ export function CreateRigDialog({ townId, isOpen, onClose, organizationId }: Cre setSelectedRepo(fullName); // Determine platform from the selection const repo = unifiedRepositories.find(r => r.fullName === fullName); - if (repo?.platform) { + if (repo?.platform && repo.platform !== 'bitbucket') { setSelectedPlatform(repo.platform); } // Auto-fill name from repo name diff --git a/apps/web/src/components/integrations/BitbucketConnectSetup.tsx b/apps/web/src/components/integrations/BitbucketConnectSetup.tsx new file mode 100644 index 0000000000..147044d747 --- /dev/null +++ b/apps/web/src/components/integrations/BitbucketConnectSetup.tsx @@ -0,0 +1,325 @@ +'use client'; + +import { useRef, useState, type FormEvent } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { inferRouterOutputs } from '@trpc/server'; +import { + AlertCircle, + CheckCircle2, + ExternalLink, + GitBranch, + Key, + ShieldCheck, + XCircle, +} from 'lucide-react'; +import { toast } from 'sonner'; +import { BitbucketLogo } from '@/components/auth/BitbucketLogo'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { SecretTokenInput } from '@/components/ui/secret-token-input'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { useTRPC } from '@/lib/trpc/utils'; +import type { RootRouter } from '@/routers/root-router'; + +const CREATE_WORKSPACE_TOKEN_URL = + 'https://support.atlassian.com/bitbucket-cloud/docs/create-a-workspace-access-token/'; +const BITBUCKET_CONNECT_RETURN_PATH = (organizationId: string) => + `/organizations/${organizationId}/integrations/bitbucket`; +const REQUIRED_PERMISSIONS = [ + 'Account Read', + 'Repository Read', + 'Repository Write', + 'Pull request Read', + 'Webhooks Read and Write', +]; + +type BitbucketConnectSetupProps = { + organizationId: string; + canManage: boolean; + statusRefetchFailed: boolean; +}; + +type RouterOutputs = inferRouterOutputs; +type BitbucketStatus = RouterOutputs['organizations']['bitbucket']['getStatus']; +type BitbucketConnectResult = RouterOutputs['organizations']['bitbucket']['connect']; + +export function buildConnectedWorkspaceAccessTokenStatus( + result: BitbucketConnectResult, + canManage: boolean +): BitbucketStatus { + return { + status: 'connected', + recoveryAction: null, + method: 'workspace_access_token', + integrationId: result.integrationId, + integrationStatus: 'active', + workspace: result.workspace, + invalidatedAt: null, + invalidationReason: null, + lastValidatedAt: result.validatedAt, + repositoryCache: { + status: 'uninitialized', + repositories: [], + syncedAt: null, + }, + canManage, + }; +} + +function CardHeaderContent() { + return ( + +
+
+ + + Bitbucket Cloud + + + Repository access for cloud agents and pull request workflows. + +
+ + + Not connected + +
+
+ ); +} + +export function BitbucketConnectSetup({ + organizationId, + canManage, + statusRefetchFailed, +}: BitbucketConnectSetupProps) { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const [accessToken, setAccessToken] = useState(''); + const [connectError, setConnectError] = useState(null); + const mutationResetRef = useRef<() => void>(() => undefined); + const statusInput = { organizationId }; + const statusQueryKey = trpc.organizations.bitbucket.getStatus.queryKey(statusInput); + const repositoryQueryKey = + trpc.organizations.cloudAgentNext.listBitbucketRepositories.queryKey(statusInput); + const oauthConnectHref = `/api/integrations/bitbucket/connect?organizationId=${encodeURIComponent( + organizationId + )}&returnTo=${encodeURIComponent(BITBUCKET_CONNECT_RETURN_PATH(organizationId))}`; + + const connectMutation = useMutation( + trpc.organizations.bitbucket.connect.mutationOptions({ + gcTime: 0, + onSuccess: result => { + setConnectError(null); + queryClient.setQueryData( + statusQueryKey, + buildConnectedWorkspaceAccessTokenStatus(result, canManage) + ); + void Promise.all([ + queryClient.invalidateQueries({ queryKey: statusQueryKey }), + queryClient.invalidateQueries({ queryKey: repositoryQueryKey }), + ]); + toast.success('Bitbucket workspace connected'); + }, + onError: error => { + setConnectError(error.message); + toast.error("Couldn't connect the Bitbucket workspace", { description: error.message }); + }, + onSettled: () => { + setAccessToken(''); + queueMicrotask(() => mutationResetRef.current()); + }, + }) + ); + mutationResetRef.current = connectMutation.reset; + + const handleConnect = (event: FormEvent) => { + event.preventDefault(); + if (!accessToken) return; + + setConnectError(null); + connectMutation.mutate({ + organizationId, + accessToken, + }); + setAccessToken(''); + }; + + const clearError = () => setConnectError(null); + + return ( + + + + {statusRefetchFailed && ( + + + Bitbucket status could not be refreshed + + Showing the last loaded integration status. Try again in a minute. + + + )} + + {!canManage ? ( + + + Bitbucket is not connected + + An organization owner or billing manager can connect a Bitbucket Premium workspace + with a Workspace Access Token, or connect Bitbucket with OAuth. + + + ) : ( + <> +
+

What happens when you connect:

+
    +
  • +
  • +
  • +
  • +
  • +
  • +
+
+ + + + + + Workspace Access Token + + + + OAuth + + + + +
+
+

+ Bitbucket Premium is required. Create a token for the workspace you want this + Kilo organization to use. Kilo detects the workspace from the token. +

+ + Create a workspace access token in Bitbucket + + +
+
+

Required permissions

+
    + {REQUIRED_PERMISSIONS.map(permission => ( +
  • {permission}
  • + ))} +
+
+
+ +
+
+ + { + setAccessToken(event.target.value); + clearError(); + }} + required + maxLength={8192} + className="h-control-touch sm:h-9" + aria-describedby="bitbucket-workspace-token-help" + /> +

+ Kilo encrypts this token and detects the connected Bitbucket workspace. The + token is never shown again after submission. +

+
+ + {connectError && ( +

+ {connectError} +

+ )} + +
+
+ + +
+ + + +

Prefer Workspace Access Token. Use OAuth only when it is not available.

+
    +
  • + Authorize with a dedicated Bitbucket service bot account, not a regular + user account. +
  • +
  • This connection is shared with everyone in the organization.
  • +
+

+ After authorizing Bitbucket, choose the workspace this organization uses. +

+
+
+ +
+
+
+ + )} +
+
+ ); +} diff --git a/apps/web/src/components/integrations/BitbucketConnectedManagement.tsx b/apps/web/src/components/integrations/BitbucketConnectedManagement.tsx new file mode 100644 index 0000000000..e9c83bfbb3 --- /dev/null +++ b/apps/web/src/components/integrations/BitbucketConnectedManagement.tsx @@ -0,0 +1,350 @@ +'use client'; + +import { useState } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { inferRouterOutputs } from '@trpc/server'; +import { AlertCircle, CheckCircle2, ShieldCheck } from 'lucide-react'; +import { toast } from 'sonner'; +import type { RootRouter } from '@/routers/root-router'; +import { BitbucketLogo } from '@/components/auth/BitbucketLogo'; +import { BitbucketIntegrationControls } from '@/components/integrations/BitbucketIntegrationControls'; +import { BitbucketRepositoryCacheSection } from '@/components/integrations/BitbucketRepositoryCacheSection'; +import { TimeAgo } from '@/components/shared/TimeAgo'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { useTRPC } from '@/lib/trpc/utils'; + +type RouterOutputs = inferRouterOutputs; +type BitbucketStatus = RouterOutputs['organizations']['bitbucket']['getStatus']; +type RecoveryAction = 'replace_token' | 'disconnect_and_connect' | null; + +type BitbucketConnectedManagementProps = { + organizationId: string; + status: BitbucketStatus; + statusRefetchFailed: boolean; +}; + +export function getRecoveryGuidance( + method: BitbucketStatus['method'], + recoveryAction: RecoveryAction, + invalidationReason: string | null, + canManage: boolean +): string { + if (method === 'oauth') { + const nextStep = canManage + ? 'Disconnect Bitbucket from Kilo, then connect it again with OAuth.' + : 'Ask an organization owner or billing manager to disconnect Bitbucket and connect it again with OAuth.'; + return `The Bitbucket OAuth connection cannot use its current credential. ${nextStep}`; + } + + const problem = + invalidationReason === 'expired' + ? 'The token has expired.' + : invalidationReason === 'provider_rejected' + ? 'Bitbucket rejected the token.' + : invalidationReason === 'workspace_mismatch' + ? 'The token no longer matches this workspace.' + : invalidationReason === 'encryption_unreadable' + ? 'Kilo can no longer read the encrypted token.' + : 'The Bitbucket integration cannot use its current credential.'; + + if (recoveryAction === 'disconnect_and_connect') { + const nextStep = canManage + ? 'Disconnect Bitbucket from Kilo, then connect the workspace again with a new Workspace Access Token.' + : 'Ask an organization owner or billing manager to disconnect Bitbucket and connect the workspace again.'; + return `${problem} ${nextStep}`; + } + if (recoveryAction === 'replace_token') { + const nextStep = canManage + ? 'Replace the token to restore provider access.' + : 'Ask an organization owner or billing manager to replace the token.'; + return `${problem} ${nextStep}`; + } + return problem; +} + +function StatusBadge({ status }: { status: BitbucketStatus['status'] }) { + if (status === 'connected') { + return ( + + + Connected + + ); + } + return ( + + + Action required + + ); +} + +function TimestampValue({ timestamp }: { timestamp: string | null }) { + if (!timestamp) return Not recorded; + return ( + + + + ); +} + +function MethodLabel({ method }: { method: BitbucketStatus['method'] }) { + return method === 'oauth' ? 'OAuth' : 'Workspace Access Token'; +} + +function WorkspaceStatus({ status }: { status: BitbucketStatus }) { + return ( +
+

+ Workspace status +

+
+
+
Method
+
+ +
+
+ {status.method === 'oauth' && 'authorizingNickname' in status && ( +
+
Authorized account
+
+ {status.authorizingNickname ?? 'Not available'} +
+
+ )} +
+
Display name
+
{status.workspace?.displayName ?? 'Not available'}
+
+
+
Canonical slug
+
+ {status.workspace?.slug ?? 'Not available'} +
+
+
+
Workspace UUID
+
+ {status.workspace?.uuid ?? 'Not available'} +
+
+ {status.method === 'workspace_access_token' && ( +
+
Last validation
+
+ +
+
+ )} +
+
+ ); +} + +function workspaceKey(workspace: { uuid: string; slug: string }) { + return `${workspace.uuid}:${workspace.slug}`; +} + +function BitbucketOAuthWorkspaceSelection({ + organizationId, + status, +}: { + organizationId: string; + status: Extract; +}) { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const availableWorkspaces = 'availableWorkspaces' in status ? status.availableWorkspaces : []; + const [selectedWorkspaceKey, setSelectedWorkspaceKey] = useState( + availableWorkspaces[0] ? workspaceKey(availableWorkspaces[0]) : '' + ); + const selectedWorkspace = availableWorkspaces.find( + workspace => workspaceKey(workspace) === selectedWorkspaceKey + ); + const statusInput = { organizationId }; + const statusQueryKey = trpc.organizations.bitbucket.getStatus.queryKey(statusInput); + const repositoryQueryKey = + trpc.organizations.cloudAgentNext.listBitbucketRepositories.queryKey(statusInput); + const mutation = useMutation( + trpc.organizations.bitbucket.selectWorkspace.mutationOptions({ + onSuccess: () => { + void Promise.all([ + queryClient.invalidateQueries({ queryKey: statusQueryKey }), + queryClient.invalidateQueries({ queryKey: repositoryQueryKey }), + ]); + toast.success('Bitbucket workspace selected'); + }, + onError: error => { + toast.error("Couldn't select the Bitbucket workspace", { description: error.message }); + }, + }) + ); + + if (!status.canManage) { + return ( + + + Workspace selection required + + An organization owner or billing manager must choose the Bitbucket workspace before + repository access is available. + + + ); + } + + if (availableWorkspaces.length === 0) { + return ( + + + No Bitbucket workspaces available + + Disconnect Bitbucket, then connect OAuth again with an account that can access the + workspace. + + + ); + } + + const selectWorkspace = () => { + if (!selectedWorkspace) return; + mutation.mutate({ + organizationId, + workspaceUuid: selectedWorkspace.uuid, + workspaceSlug: selectedWorkspace.slug, + }); + }; + + return ( +
+
+

+ Choose workspace +

+

+ Select the Bitbucket workspace this Kilo organization should use. +

+
+
+ Bitbucket workspace +
+ {availableWorkspaces.map(workspace => { + const key = workspaceKey(workspace); + return ( + + ); + })} +
+
+ +
+ ); +} + +export function BitbucketConnectedManagement({ + organizationId, + status, + statusRefetchFailed, +}: BitbucketConnectedManagementProps) { + return ( + + +
+
+ + + Bitbucket Cloud + + + Organization repository access through{' '} + {status.method === 'oauth' ? 'Bitbucket OAuth' : 'a Bitbucket Workspace Access Token'} + . + +
+ +
+
+ + {statusRefetchFailed && ( + + + Bitbucket status could not be refreshed + + Showing the last loaded workspace and repository cache. Try again in a minute. + + + )} + {status.status === 'reconnect_required' && ( + + + Bitbucket access needs attention + + {getRecoveryGuidance( + status.method, + status.recoveryAction, + status.invalidationReason, + status.canManage + )} + + + )} + + {status.status === 'workspace_selection_required' ? ( + + ) : ( + <> + + + + )} + {status.canManage ? ( + + ) : ( + + + Read-only organization integration + + {status.method === 'oauth' + ? status.status === 'workspace_selection_required' + ? 'You can view the Bitbucket connection. An organization owner or billing manager must choose a workspace before repository access is available.' + : 'You can view the workspace and cached repositories. An organization owner or billing manager can refresh repositories or disconnect Bitbucket.' + : status.recoveryAction === 'disconnect_and_connect' + ? 'You can view the workspace and cached repositories. An organization owner or billing manager must disconnect Bitbucket, then connect the workspace again.' + : 'You can view the workspace and cached repositories. An organization owner or billing manager can replace the token, refresh repositories, or disconnect Bitbucket.'} + + + )} + +
+ ); +} diff --git a/apps/web/src/components/integrations/BitbucketIntegrationControls.tsx b/apps/web/src/components/integrations/BitbucketIntegrationControls.tsx new file mode 100644 index 0000000000..569c4879bc --- /dev/null +++ b/apps/web/src/components/integrations/BitbucketIntegrationControls.tsx @@ -0,0 +1,388 @@ +'use client'; + +import { useEffect, useRef, useState, type FormEvent } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { inferRouterOutputs } from '@trpc/server'; +import { ExternalLink } from 'lucide-react'; +import { toast } from 'sonner'; +import type { RootRouter } from '@/routers/root-router'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { SecretTokenInput } from '@/components/ui/secret-token-input'; +import { useTRPC } from '@/lib/trpc/utils'; +import { buildConnectedWorkspaceAccessTokenStatus } from './BitbucketConnectSetup'; + +const REVOKE_WORKSPACE_TOKEN_URL = + 'https://support.atlassian.com/bitbucket-cloud/docs/revoke-a-workspace-access-token/'; + +type RouterOutputs = inferRouterOutputs; +type BitbucketStatus = RouterOutputs['organizations']['bitbucket']['getStatus']; +type RecoveryAction = 'replace_token' | 'disconnect_and_connect' | null; + +type BitbucketIntegrationControlsProps = { + organizationId: string; + status: BitbucketStatus; +}; + +export function getBitbucketIntegrationControlsDescription( + method: BitbucketStatus['method'], + recoveryAction: RecoveryAction +): string | null { + if (method === 'oauth') return null; + if (recoveryAction === 'disconnect_and_connect') { + return 'Disconnect this integration, then connect the workspace again with a new Workspace Access Token.'; + } + return 'Replace the credential without changing the connected workspace, or disconnect the integration from Kilo.'; +} + +function ReplaceTokenDialog({ + organizationId, + integrationId, + workspaceSlug, + available, +}: { + organizationId: string; + integrationId: string; + workspaceSlug: string | null; + available: boolean; +}) { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + const [token, setToken] = useState(''); + const [error, setError] = useState(null); + const mutationResetRef = useRef<() => void>(() => undefined); + const statusInput = { organizationId }; + const statusQueryKey = trpc.organizations.bitbucket.getStatus.queryKey(statusInput); + const repositoryQueryKey = + trpc.organizations.cloudAgentNext.listBitbucketRepositories.queryKey(statusInput); + const mutation = useMutation( + trpc.organizations.bitbucket.replaceToken.mutationOptions({ + gcTime: 0, + onSuccess: result => { + setToken(''); + setError(null); + setOpen(false); + queryClient.setQueryData(statusQueryKey, current => + current ? buildConnectedWorkspaceAccessTokenStatus(result, current.canManage) : current + ); + void Promise.all([ + queryClient.invalidateQueries({ queryKey: statusQueryKey }), + queryClient.invalidateQueries({ queryKey: repositoryQueryKey }), + ]); + toast.success('Bitbucket token replaced'); + }, + onError: mutationError => { + setError(mutationError.message); + toast.error("Couldn't replace the Bitbucket token", { + description: mutationError.message, + }); + }, + onSettled: () => { + setToken(''); + queueMicrotask(() => mutationResetRef.current()); + }, + }) + ); + mutationResetRef.current = mutation.reset; + + useEffect(() => { + if (available || !open || mutation.isPending) return; + setOpen(false); + setToken(''); + setError(null); + mutation.reset(); + }, [available, mutation.isPending, mutation.reset, open]); + + const handleOpenChange = (nextOpen: boolean) => { + if (mutation.isPending) return; + setOpen(nextOpen); + setToken(''); + setError(null); + mutation.reset(); + }; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + if (!token) return; + + setError(null); + mutation.mutate({ organizationId, integrationId, accessToken: token }); + setToken(''); + }; + + return ( + + {available && ( + + + + )} + mutation.isPending && event.preventDefault()} + onPointerDownOutside={event => mutation.isPending && event.preventDefault()} + onInteractOutside={event => mutation.isPending && event.preventDefault()} + > +
+ + Replace Workspace Access Token + + The workspace stays fixed. Kilo detects the workspace from the new token, validates + that it matches, then encrypts the replacement. + + + +
+ + +

+ Disconnect Bitbucket to use a different workspace. +

+
+ +
+ + { + setToken(event.target.value); + setError(null); + }} + required + maxLength={8192} + className="h-control-touch sm:h-9" + aria-describedby="bitbucket-replacement-token-help" + /> +

+ Kilo encrypts this token. It is never shown again after submission. +

+
+ + {error && ( +

+ {error} +

+ )} + + + + + + +
+
+
+ ); +} + +function DisconnectDialog({ + organizationId, + integrationId, + method, +}: { + organizationId: string; + integrationId: string; + method: BitbucketStatus['method']; +}) { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + const [error, setError] = useState(null); + const statusInput = { organizationId }; + const statusQueryKey = trpc.organizations.bitbucket.getStatus.queryKey(statusInput); + const repositoryQueryKey = + trpc.organizations.cloudAgentNext.listBitbucketRepositories.queryKey(statusInput); + const mutation = useMutation(trpc.organizations.bitbucket.disconnect.mutationOptions()); + + const handleOpenChange = (nextOpen: boolean) => { + if (mutation.isPending) return; + setOpen(nextOpen); + setError(null); + mutation.reset(); + }; + + const handleDisconnect = () => { + setError(null); + mutation.mutate( + { organizationId, integrationId }, + { + onSuccess: () => { + queryClient.setQueryData(repositoryQueryKey, { status: 'not_connected' }); + setOpen(false); + setError(null); + void Promise.all([ + queryClient.invalidateQueries({ queryKey: statusQueryKey }), + queryClient.invalidateQueries({ queryKey: repositoryQueryKey }), + ]); + toast.success('Bitbucket disconnected from Kilo'); + }, + onError: mutationError => { + setError(mutationError.message); + toast.error("Couldn't disconnect Bitbucket", { description: mutationError.message }); + }, + onSettled: () => queueMicrotask(() => mutation.reset()), + } + ); + }; + + return ( + + + + + mutation.isPending && event.preventDefault()} + onPointerDownOutside={event => mutation.isPending && event.preventDefault()} + onInteractOutside={event => mutation.isPending && event.preventDefault()} + > + + Disconnect Bitbucket? + + + {method === 'oauth' + ? "Disconnecting deletes Kilo's local OAuth credential and repository cache. It does not revoke Kilo's OAuth access in Bitbucket." + : "Disconnecting deletes Kilo's local encrypted credential and repository cache. It does not revoke the Workspace Access Token in Bitbucket."} + + {method === 'workspace_access_token' && ( + + Revoke the workspace token in Bitbucket + + + )} + + + {error && ( +

+ {error} +

+ )} + + + Keep Bitbucket connected + + { + event.preventDefault(); + handleDisconnect(); + }} + > + {mutation.isPending ? 'Disconnecting Bitbucket...' : 'Disconnect Bitbucket'} + + +
+
+ ); +} + +export function BitbucketIntegrationControls({ + organizationId, + status, +}: BitbucketIntegrationControlsProps) { + if (!status.integrationId || !status.canManage) return null; + const replacementAvailable = + status.method === 'workspace_access_token' && + (status.status === 'connected' || status.recoveryAction === 'replace_token'); + const controlsDescription = getBitbucketIntegrationControlsDescription( + status.method, + status.recoveryAction + ); + + return ( +
+
+

+ Integration controls +

+ {controlsDescription && ( +

{controlsDescription}

+ )} +
+
+ {status.method === 'workspace_access_token' && ( + + )} + +
+
+ ); +} diff --git a/apps/web/src/components/integrations/BitbucketIntegrationDetails.test.ts b/apps/web/src/components/integrations/BitbucketIntegrationDetails.test.ts new file mode 100644 index 0000000000..182c1164c7 --- /dev/null +++ b/apps/web/src/components/integrations/BitbucketIntegrationDetails.test.ts @@ -0,0 +1,89 @@ +import { createElement } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { buildConnectedWorkspaceAccessTokenStatus } from '@/components/integrations/BitbucketConnectSetup'; +import { getRecoveryGuidance } from '@/components/integrations/BitbucketConnectedManagement'; +import { + BitbucketConnectionRedirectNotice, + getBitbucketConnectionErrorMessage, +} from '@/components/integrations/BitbucketIntegrationDetails'; +import { getBitbucketIntegrationControlsDescription } from '@/components/integrations/BitbucketIntegrationControls'; + +describe('Bitbucket integration UI state', () => { + it('builds connected status from a successful Workspace Access Token mutation', () => { + expect( + buildConnectedWorkspaceAccessTokenStatus( + { + integrationId: '33333333-3333-4333-8333-333333333333', + workspace: { + uuid: '11111111-1111-4111-8111-111111111111', + slug: 'acme', + displayName: 'Acme Workspace', + }, + credentialVersion: 1, + repositoryCount: 1, + validatedAt: '2026-06-24T08:00:00.000Z', + }, + true + ) + ).toEqual({ + status: 'connected', + recoveryAction: null, + method: 'workspace_access_token', + integrationId: '33333333-3333-4333-8333-333333333333', + integrationStatus: 'active', + workspace: { + uuid: '11111111-1111-4111-8111-111111111111', + slug: 'acme', + displayName: 'Acme Workspace', + }, + invalidatedAt: null, + invalidationReason: null, + lastValidatedAt: '2026-06-24T08:00:00.000Z', + repositoryCache: { + status: 'uninitialized', + repositories: [], + syncedAt: null, + }, + canManage: true, + }); + }); + + it('omits redundant integration controls guidance for OAuth connections', () => { + expect(getBitbucketIntegrationControlsDescription('oauth', null)).toBeNull(); + }); + + it('instructs token replacement only when recovery permits rotation', () => { + expect( + getRecoveryGuidance('workspace_access_token', 'replace_token', 'provider_rejected', true) + ).toContain('Replace the token'); + expect( + getRecoveryGuidance( + 'workspace_access_token', + 'disconnect_and_connect', + 'provider_rejected', + true + ) + ).toContain('Disconnect Bitbucket from Kilo, then connect the workspace again'); + expect( + getRecoveryGuidance( + 'workspace_access_token', + 'disconnect_and_connect', + 'provider_rejected', + true + ) + ).not.toContain('Replace the token'); + }); + + it('shows a visible message when Bitbucket OAuth authorization is cancelled', () => { + expect(getBitbucketConnectionErrorMessage('authorization_cancelled')).toBe( + 'Bitbucket authorization was cancelled. No changes were made. Start OAuth again when you are ready.' + ); + + const html = renderToStaticMarkup( + createElement(BitbucketConnectionRedirectNotice, { error: 'authorization_cancelled' }) + ); + + expect(html).toContain('Bitbucket OAuth was cancelled'); + expect(html).toContain('No changes were made'); + }); +}); diff --git a/apps/web/src/components/integrations/BitbucketIntegrationDetails.tsx b/apps/web/src/components/integrations/BitbucketIntegrationDetails.tsx new file mode 100644 index 0000000000..22af14d23a --- /dev/null +++ b/apps/web/src/components/integrations/BitbucketIntegrationDetails.tsx @@ -0,0 +1,130 @@ +'use client'; + +import * as React from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { AlertCircle } from 'lucide-react'; +import { BitbucketLogo } from '@/components/auth/BitbucketLogo'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useTRPC } from '@/lib/trpc/utils'; +import { BitbucketConnectSetup } from './BitbucketConnectSetup'; +import { BitbucketConnectedManagement } from './BitbucketConnectedManagement'; + +type BitbucketIntegrationDetailsProps = { + organizationId: string; + error?: string; +}; + +const bitbucketConnectionErrorMessages: Record = { + authorization_cancelled: + 'Bitbucket authorization was cancelled. No changes were made. Start OAuth again when you are ready.', + invalid_state: 'Bitbucket authorization expired or could not be verified. Start OAuth again.', + unauthorized: + "You don't have access to connect Bitbucket for this organization. Ask an owner or billing manager to connect it.", + missing_code: 'Bitbucket did not return an authorization code. Start OAuth again.', + no_workspaces: + 'The authorized Bitbucket account has no available workspaces. Use a service bot account with access to the workspace, then try again.', + connection_exists: + 'Bitbucket is already connected. Disconnect the current connection before using OAuth.', + connection_failed: 'Bitbucket could not be connected. Try OAuth again in a minute.', +}; + +export function getBitbucketConnectionErrorMessage(error: string): string { + return ( + bitbucketConnectionErrorMessages[error] ?? 'Bitbucket could not be connected. Try OAuth again.' + ); +} + +export function BitbucketConnectionRedirectNotice({ error }: { error?: string }) { + if (!error) return null; + + const isAuthorizationCancelled = error === 'authorization_cancelled'; + + return ( + + + + {isAuthorizationCancelled ? 'Bitbucket OAuth was cancelled' : 'Could not connect Bitbucket'} + + {getBitbucketConnectionErrorMessage(error)} + + ); +} + +function LoadingState() { + return ( + + + + + + + + + + + ); +} + +function ErrorState() { + return ( + + + + + Bitbucket Cloud + + + + + + Bitbucket status is unavailable + + Refresh the page to try again. No integration settings were changed. + + + + + ); +} + +export function BitbucketIntegrationDetails({ + organizationId, + error, +}: BitbucketIntegrationDetailsProps) { + const trpc = useTRPC(); + const statusQuery = useQuery( + trpc.organizations.bitbucket.getStatus.queryOptions({ organizationId }) + ); + + let details; + if (statusQuery.isLoading) { + details = ; + } else if (!statusQuery.data) { + details = ; + } else if (statusQuery.data.integrationId === null) { + details = ( + + ); + } else { + details = ( + + ); + } + + return ( +
+ + {details} +
+ ); +} diff --git a/apps/web/src/components/integrations/BitbucketRepositoryCacheSection.tsx b/apps/web/src/components/integrations/BitbucketRepositoryCacheSection.tsx new file mode 100644 index 0000000000..a5986e6b8e --- /dev/null +++ b/apps/web/src/components/integrations/BitbucketRepositoryCacheSection.tsx @@ -0,0 +1,222 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { inferRouterOutputs } from '@trpc/server'; +import { AlertCircle, RefreshCw } from 'lucide-react'; +import { toast } from 'sonner'; +import type { RootRouter } from '@/routers/root-router'; +import { TimeAgo } from '@/components/shared/TimeAgo'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { useTRPC } from '@/lib/trpc/utils'; + +type RouterOutputs = inferRouterOutputs; +type BitbucketStatus = RouterOutputs['organizations']['bitbucket']['getStatus']; +type RepositoryActionState = + | 'not_connected' + | 'reconnect_required' + | 'workspace_selection_required' + | 'insufficient_permissions' + | 'temporarily_unavailable' + | 'invalid_request' + | null; + +type BitbucketRepositoryCacheSectionProps = { + organizationId: string; + status: BitbucketStatus; +}; + +function TimestampValue({ timestamp }: { timestamp: string }) { + return ( + + + + ); +} + +function RefreshFeedback({ + state, + method, +}: { + state: RepositoryActionState; + method: BitbucketStatus['method']; +}) { + if (!state) return null; + if (state === 'temporarily_unavailable') { + return ( + + + Bitbucket is temporarily unavailable + + The repository cache was not changed. Wait a minute, then refresh repositories again. + + + ); + } + + const content = { + insufficient_permissions: { + title: 'Repository refresh needs more permissions', + description: `The ${ + method === 'oauth' ? 'OAuth connection' : 'Workspace Access Token' + } must include Account Read, Repository Read, Repository Write, and Webhooks Read and Write. Repository selection is disabled until access is restored. The last successful cache remains in the workspace status.`, + }, + not_connected: { + title: 'Bitbucket is not connected', + description: + 'Repository selection is disabled. Connect the workspace again to restore access.', + }, + workspace_selection_required: { + title: 'Workspace selection required', + description: + 'Repository selection is disabled until an organization owner or billing manager chooses a Bitbucket workspace.', + }, + reconnect_required: { + title: 'Bitbucket access needs attention', + description: + 'Repository selection is disabled. Follow the recovery step shown for this integration. The last successful cache remains in the workspace status.', + }, + invalid_request: { + title: 'Repository refresh used stale integration details', + description: + 'Repository selection is disabled. Reload the integration status before trying again.', + }, + }[state]; + + return ( + + + {content.title} + {content.description} + + ); +} + +export function BitbucketRepositoryCacheSection({ + organizationId, + status, +}: BitbucketRepositoryCacheSectionProps) { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const [refreshFeedback, setRefreshFeedback] = useState(null); + const previousValidation = useRef(status.lastValidatedAt); + const statusInput = { organizationId }; + const statusQueryKey = trpc.organizations.bitbucket.getStatus.queryKey(statusInput); + const repositoryQueryKey = + trpc.organizations.cloudAgentNext.listBitbucketRepositories.queryKey(statusInput); + const cache = status.repositoryCache; + + useEffect(() => { + if (previousValidation.current === status.lastValidatedAt) return; + previousValidation.current = status.lastValidatedAt; + setRefreshFeedback(null); + }, [status.lastValidatedAt]); + + const refreshMutation = useMutation( + trpc.organizations.bitbucket.refreshRepositories.mutationOptions({ + onSuccess: result => { + switch (result.status) { + case 'available': + setRefreshFeedback(null); + queryClient.setQueryData(repositoryQueryKey, result); + toast.success('Bitbucket repositories refreshed'); + break; + case 'temporarily_unavailable': + setRefreshFeedback(result.status); + break; + case 'not_connected': + case 'workspace_selection_required': + case 'reconnect_required': + case 'invalid_request': + case 'insufficient_permissions': + queryClient.setQueryData(repositoryQueryKey, result); + setRefreshFeedback(result.status); + break; + } + void queryClient.invalidateQueries({ queryKey: statusQueryKey }); + }, + onError: error => { + toast.error("Couldn't refresh Bitbucket repositories", { description: error.message }); + }, + }) + ); + + const refreshRepositories = () => { + if (!status.integrationId) return; + setRefreshFeedback(null); + refreshMutation.mutate({ organizationId, integrationId: status.integrationId }); + }; + + return ( + <> + +
+
+
+

+ Repository cache{cache.status === 'available' && ` (${cache.repositories.length})`} +

+

+ Last successful sync:{' '} + {cache.syncedAt ? : 'Not yet synced'} +

+
+ {status.canManage && status.integrationId && ( + + )} +
+ + {cache.status === 'uninitialized' ? ( +

+ The repository cache has not been initialized. + {status.canManage + ? ' Refresh repositories to try again.' + : ' An organization owner or billing manager can initialize it.'} +

+ ) : cache.repositories.length === 0 ? ( +

+ The repository cache is initialized and contains no repositories. +

+ ) : ( +
    + {cache.repositories.map(repository => ( +
  • + {repository.fullName} + + {repository.private ? 'Private' : 'Public'} + +
  • + ))} +
+ )} +
+ + ); +} diff --git a/apps/web/src/components/integrations/IntegrationDetailPage.tsx b/apps/web/src/components/integrations/IntegrationDetailPage.tsx index 912ff4ffc0..8aa136590b 100644 --- a/apps/web/src/components/integrations/IntegrationDetailPage.tsx +++ b/apps/web/src/components/integrations/IntegrationDetailPage.tsx @@ -69,6 +69,18 @@ const integrationDetailRegistry = { ); }, }, + [PLATFORM.BITBUCKET]: { + title: 'Bitbucket Integration', + userSubtitle: 'Bitbucket is available for Kilo organizations', + organizationSubtitle: organizationName => + `Manage the Bitbucket Cloud workspace for ${organizationName}`, + render: async ({ organizationId, search }) => { + if (!organizationId) notFound(); + const { BitbucketIntegrationDetails } = + await import('@/components/integrations/BitbucketIntegrationDetails'); + return ; + }, + }, [PLATFORM.SLACK]: { title: 'Slack Integration', userSubtitle: 'Connect your Slack workspace to receive notifications', @@ -209,6 +221,7 @@ export async function UserIntegrationDetailPage({ searchParams: IntegrationDetailSearchParams; }) { const detailPlatform = getIntegrationDetailPlatform(platform); + if (detailPlatform === PLATFORM.BITBUCKET) notFound(); const entry = getIntegrationDetailEntry(detailPlatform); await getUserFromAuthOrRedirect('/users/sign_in'); const search = await searchParams; diff --git a/apps/web/src/components/integrations/IntegrationsHub.tsx b/apps/web/src/components/integrations/IntegrationsHub.tsx index af3ef66487..c063de5a1a 100644 --- a/apps/web/src/components/integrations/IntegrationsHub.tsx +++ b/apps/web/src/components/integrations/IntegrationsHub.tsx @@ -5,7 +5,10 @@ import { PlatformCard, type GitHubIdentityStatus, } from '@/app/(app)/organizations/[id]/integrations/components/PlatformCard'; -import { buildPlatforms, PLATFORM_DEFINITIONS } from '@/lib/integrations/platform-definitions'; +import { + buildPlatforms, + getPlatformDefinitionCountForOwner, +} from '@/lib/integrations/platform-definitions'; import { Card, CardContent } from '@/components/ui/card'; import { useQuery } from '@tanstack/react-query'; import { useTRPC } from '@/lib/trpc/utils'; @@ -32,8 +35,8 @@ export function IntegrationsHub({ organizationId }: IntegrationsHubProps) { if (isLoading) { return (
- {PLATFORM_DEFINITIONS.map((_, i) => ( - + {Array.from({ length: getPlatformDefinitionCountForOwner(organizationId) }, (_, index) => ( +
diff --git a/apps/web/src/components/shared/RepositoryCombobox.tsx b/apps/web/src/components/shared/RepositoryCombobox.tsx index 131fc4d982..1ddbbe3e45 100644 --- a/apps/web/src/components/shared/RepositoryCombobox.tsx +++ b/apps/web/src/components/shared/RepositoryCombobox.tsx @@ -16,8 +16,9 @@ import { import { ChevronsUpDown, Check, Lock, Unlock } from 'lucide-react'; import { cn } from '@/lib/utils'; import { GitLabLogo } from '@/components/auth/GitLabLogo'; +import { BitbucketLogo } from '@/components/auth/BitbucketLogo'; -export type RepositoryPlatform = 'github' | 'gitlab'; +export type RepositoryPlatform = 'github' | 'gitlab' | 'bitbucket'; export type RepositoryOption = { id: string | number; @@ -25,6 +26,7 @@ export type RepositoryOption = { private?: boolean; description?: string; platform?: RepositoryPlatform; + workspaceUuid?: string; }; export type RepositoryComboboxProps = { @@ -71,6 +73,9 @@ function PlatformIcon({ if (platform === 'gitlab') { return ; } + if (platform === 'bitbucket') { + return ; + } return null; } @@ -186,6 +191,7 @@ export function RepositoryCombobox({ // Group repositories by platform when groupByPlatform is enabled const githubRepos = repositories.filter(r => r.platform === 'github'); const gitlabRepos = repositories.filter(r => r.platform === 'gitlab'); + const bitbucketRepos = repositories.filter(r => r.platform === 'bitbucket'); const otherRepos = repositories.filter(r => !r.platform); const renderGroupedList = () => ( @@ -216,6 +222,19 @@ export function RepositoryCombobox({ ))} )} + {bitbucketRepos.length > 0 && ( + + {bitbucketRepos.map(repo => ( + + ))} + + )} {otherRepos.length > 0 && ( {otherRepos.map(repo => ( diff --git a/apps/web/src/hooks/useRefreshRepositories.ts b/apps/web/src/hooks/useRefreshRepositories.ts index 8370ac309e..2d726b858d 100644 --- a/apps/web/src/hooks/useRefreshRepositories.ts +++ b/apps/web/src/hooks/useRefreshRepositories.ts @@ -39,7 +39,10 @@ export function useRefreshRepositories({ const refresh = useCallback(async () => { setIsRefreshing(true); try { - const freshData = await queryClient.fetchQuery(getRefreshQueryOptions()); + const freshData = await queryClient.fetchQuery({ + ...getRefreshQueryOptions(), + staleTime: 0, + }); queryClient.setQueryData(getCacheQueryKey(), freshData); if (!silent) toast.success('Repositories refreshed'); } catch (error) { diff --git a/apps/web/src/lib/cloud-agent-next/cloud-agent-client.ts b/apps/web/src/lib/cloud-agent-next/cloud-agent-client.ts index 4dbebd6095..7a3644601c 100644 --- a/apps/web/src/lib/cloud-agent-next/cloud-agent-client.ts +++ b/apps/web/src/lib/cloud-agent-next/cloud-agent-client.ts @@ -105,7 +105,9 @@ export type PrepareSessionInput = { gitUrl?: string; gitToken?: string; /** Explicit platform type for correct env var setup (avoids URL-based detection) */ - platform?: 'github' | 'gitlab'; + platform?: 'github' | 'gitlab' | 'bitbucket'; + bitbucketWorkspaceUuid?: string; + bitbucketRepositoryUuid?: string; // Common params kilocodeOrganizationId?: string; /** Profile ID forwarded to cloud-agent-next for server-side merge. */ @@ -260,7 +262,9 @@ export type GetSessionOutput = { // Repository info (no tokens) githubRepo?: string; gitUrl?: string; - platform?: 'github' | 'gitlab'; + platform?: 'github' | 'gitlab' | 'bitbucket'; + bitbucketWorkspaceUuid?: string; + bitbucketRepositoryUuid?: string; // Execution params prompt?: string; diff --git a/apps/web/src/lib/cloud-agent/bitbucket-integration-helpers.ts b/apps/web/src/lib/cloud-agent/bitbucket-integration-helpers.ts new file mode 100644 index 0000000000..13383f80ba --- /dev/null +++ b/apps/web/src/lib/cloud-agent/bitbucket-integration-helpers.ts @@ -0,0 +1,54 @@ +import 'server-only'; + +import { and, eq, isNull } from 'drizzle-orm'; +import { z } from 'zod'; +import { db } from '@/lib/drizzle'; +import { PLATFORM } from '@/lib/integrations/core/constants'; +import { + BitbucketOrganizationRepositoryListResultSchema, + type BitbucketOrganizationRepositoryListResult, +} from '@/lib/integrations/platforms/bitbucket/oauth-integration'; +import { listBitbucketRepositories } from '@/lib/integrations/platforms/bitbucket/repository-cache'; +import { readCachedBitbucketWorkspaceAccessTokenRepositories } from '@/lib/integrations/platforms/bitbucket/workspace-access-token-repository-cache'; +import { platform_integrations } from '@kilocode/db/schema'; + +async function findBitbucketIntegrationType(organizationId: string) { + const [integration] = await db + .select({ integrationType: platform_integrations.integration_type }) + .from(platform_integrations) + .where( + and( + eq(platform_integrations.owned_by_organization_id, organizationId), + isNull(platform_integrations.owned_by_user_id), + eq(platform_integrations.platform, PLATFORM.BITBUCKET) + ) + ) + .limit(1); + return integration?.integrationType ?? null; +} + +export async function fetchBitbucketRepositoriesForOrganization( + organizationId: string, + kiloUserId: string +): Promise { + const canonicalOrganizationId = z.uuid().safeParse(organizationId); + if (!canonicalOrganizationId.success) return { status: 'invalid_request' }; + + const integrationType = await findBitbucketIntegrationType(canonicalOrganizationId.data); + if (integrationType === 'workspace_access_token') { + return readCachedBitbucketWorkspaceAccessTokenRepositories({ + organizationId: canonicalOrganizationId.data, + }); + } + if (integrationType === 'oauth') { + return listBitbucketRepositories({ + owner: { type: 'org', id: canonicalOrganizationId.data }, + kiloUserId, + }); + } + if (integrationType) return { status: 'reconnect_required' }; + return { status: 'not_connected' }; +} + +export { BitbucketOrganizationRepositoryListResultSchema }; +export type { BitbucketOrganizationRepositoryListResult }; diff --git a/apps/web/src/lib/cloud-agent/github-integration-helpers.ts b/apps/web/src/lib/cloud-agent/github-integration-helpers.ts index f6ae2077ab..8e95ebb6d6 100644 --- a/apps/web/src/lib/cloud-agent/github-integration-helpers.ts +++ b/apps/web/src/lib/cloud-agent/github-integration-helpers.ts @@ -11,7 +11,10 @@ import { } from '@/lib/integrations/platforms/github/adapter'; import { DEMO_SOURCE_OWNER, DEMO_SOURCE_REPO_NAME } from '@/components/cloud-agent/demo-config'; import { PLATFORM } from '@/lib/integrations/core/constants'; -import type { PlatformRepository } from '@/lib/integrations/core/types'; +import { + requireNumericPlatformRepositories, + type PlatformRepository, +} from '@/lib/integrations/core/types'; type GitHubRepositoriesResult = { integrationInstalled: boolean; @@ -130,8 +133,9 @@ export async function fetchGitHubRepositoriesForOrganization( } try { + const cachedRepositories = requireNumericPlatformRepositories(integration.repositories); // If forceRefresh or no cached repos, fetch from GitHub and update cache - if (forceRefresh || !integration.repositories?.length) { + if (forceRefresh || !cachedRepositories?.length) { const appType = integration.github_app_type || 'standard'; const repositories = await fetchGitHubRepositories( integration.platform_installation_id, @@ -148,7 +152,7 @@ export async function fetchGitHubRepositoriesForOrganization( // Return cached repos return { integrationInstalled: true, - repositories: mapRepositories(integration.repositories), + repositories: mapRepositories(cachedRepositories), syncedAt: integration.repositories_synced_at, }; } catch (_error) { @@ -174,8 +178,9 @@ export async function fetchGitHubRepositoriesForUser( } try { + const cachedRepositories = requireNumericPlatformRepositories(integration.repositories); // If forceRefresh or no cached repos, fetch from GitHub and update cache - if (forceRefresh || !integration.repositories?.length) { + if (forceRefresh || !cachedRepositories?.length) { const appType = integration.github_app_type || 'standard'; const repositories = await fetchGitHubRepositories( integration.platform_installation_id, @@ -192,7 +197,7 @@ export async function fetchGitHubRepositoriesForUser( // Return cached repos return { integrationInstalled: true, - repositories: mapRepositories(integration.repositories), + repositories: mapRepositories(cachedRepositories), syncedAt: integration.repositories_synced_at, }; } catch (_error) { diff --git a/apps/web/src/lib/cloud-agent/gitlab-integration-helpers.ts b/apps/web/src/lib/cloud-agent/gitlab-integration-helpers.ts index 8ddd021f45..3c9810ff75 100644 --- a/apps/web/src/lib/cloud-agent/gitlab-integration-helpers.ts +++ b/apps/web/src/lib/cloud-agent/gitlab-integration-helpers.ts @@ -7,7 +7,10 @@ import { import { getGitLabIntegration, getValidGitLabToken } from '@/lib/integrations/gitlab-service'; import { fetchGitLabProjects } from '@/lib/integrations/platforms/gitlab/adapter'; import { PLATFORM } from '@/lib/integrations/core/constants'; -import type { PlatformRepository } from '@/lib/integrations/core/types'; +import { + requireNumericPlatformRepositories, + type PlatformRepository, +} from '@/lib/integrations/core/types'; const DEFAULT_GITLAB_URL = 'https://gitlab.com'; @@ -115,8 +118,9 @@ export async function fetchGitLabRepositoriesForOrganization( const instanceUrl = metadata?.gitlab_instance_url || DEFAULT_GITLAB_URL; try { + const cachedRepositories = requireNumericPlatformRepositories(integration.repositories); // If forceRefresh or no cached repos, fetch from GitLab and update cache - if (forceRefresh || !integration.repositories?.length) { + if (forceRefresh || !cachedRepositories?.length) { const accessToken = await getValidGitLabToken(integration); const repositories = await fetchGitLabProjects(accessToken, instanceUrl); await updateRepositoriesForIntegration(integration.id, repositories); @@ -131,7 +135,7 @@ export async function fetchGitLabRepositoriesForOrganization( // Return cached repos return { integrationInstalled: true, - repositories: mapRepositories(integration.repositories), + repositories: mapRepositories(cachedRepositories), syncedAt: integration.repositories_synced_at, instanceUrl, }; @@ -161,8 +165,9 @@ export async function fetchGitLabRepositoriesForUser( const instanceUrl = metadata?.gitlab_instance_url || DEFAULT_GITLAB_URL; try { + const cachedRepositories = requireNumericPlatformRepositories(integration.repositories); // If forceRefresh or no cached repos, fetch from GitLab and update cache - if (forceRefresh || !integration.repositories?.length) { + if (forceRefresh || !cachedRepositories?.length) { const accessToken = await getValidGitLabToken(integration); const repositories = await fetchGitLabProjects(accessToken, instanceUrl); await updateRepositoriesForIntegration(integration.id, repositories); @@ -177,7 +182,7 @@ export async function fetchGitLabRepositoriesForUser( // Return cached repos return { integrationInstalled: true, - repositories: mapRepositories(integration.repositories), + repositories: mapRepositories(cachedRepositories), syncedAt: integration.repositories_synced_at, instanceUrl, }; diff --git a/apps/web/src/lib/config.server.ts b/apps/web/src/lib/config.server.ts index a0812e0c36..50b945feb1 100644 --- a/apps/web/src/lib/config.server.ts +++ b/apps/web/src/lib/config.server.ts @@ -36,6 +36,14 @@ export const CONTRIBUTOR_CHAMPION_TEAM_EMAILS = getEnvVariable('CONTRIBUTOR_CHAMPION_TEAM_EMAILS') || ''; export const GITLAB_CLIENT_ID = getEnvVariable('GITLAB_CLIENT_ID'); export const GITLAB_CLIENT_SECRET = getEnvVariable('GITLAB_CLIENT_SECRET'); +export const BITBUCKET_CLIENT_ID = getEnvVariable('BITBUCKET_CLIENT_ID'); +export const BITBUCKET_CLIENT_SECRET = getEnvVariable('BITBUCKET_CLIENT_SECRET'); +export const BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID = getEnvVariable( + 'BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID' +); +export const BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY = getEnvVariable( + 'BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY' +); export const LINKEDIN_CLIENT_ID = getEnvVariable('LINKEDIN_CLIENT_ID'); export const LINKEDIN_CLIENT_SECRET = getEnvVariable('LINKEDIN_CLIENT_SECRET'); export const TURNSTILE_SECRET_KEY = getEnvVariable('TURNSTILE_SECRET_KEY'); diff --git a/apps/web/src/lib/integrations/core/constants.ts b/apps/web/src/lib/integrations/core/constants.ts index 0a27ce2ee4..d0a5857fa6 100644 --- a/apps/web/src/lib/integrations/core/constants.ts +++ b/apps/web/src/lib/integrations/core/constants.ts @@ -149,6 +149,7 @@ export const GITLAB_ACTION = { export const PLATFORM = { GITHUB: 'github', GITLAB: 'gitlab', + BITBUCKET: 'bitbucket', SLACK: 'slack', DISCORD: 'discord', LINEAR: 'linear', diff --git a/apps/web/src/lib/integrations/core/types.ts b/apps/web/src/lib/integrations/core/types.ts index 187409ab3d..80aa4d37dd 100644 --- a/apps/web/src/lib/integrations/core/types.ts +++ b/apps/web/src/lib/integrations/core/types.ts @@ -1,6 +1,22 @@ // Core types for the integrations system +import type { PlatformRepository } from '@kilocode/db/schema-types'; + export type { IntegrationPermissions, PlatformRepository } from '@kilocode/db/schema-types'; +export function requireNumericPlatformRepositories( + repositories: PlatformRepository[] | null +): PlatformRepository[] | null { + if (!repositories) return null; + if ( + !repositories.every( + (repository): repository is PlatformRepository => typeof repository.id === 'number' + ) + ) { + throw new Error('Expected numeric platform repository IDs'); + } + return repositories; +} + /** * Represents ownership of an integration * Can be either a user or an organization diff --git a/apps/web/src/lib/integrations/github-apps-service.ts b/apps/web/src/lib/integrations/github-apps-service.ts index 8eb6af50e8..65f9499437 100644 --- a/apps/web/src/lib/integrations/github-apps-service.ts +++ b/apps/web/src/lib/integrations/github-apps-service.ts @@ -4,8 +4,8 @@ import type { PlatformIntegration } from '@kilocode/db/schema'; import { platform_integrations } from '@kilocode/db/schema'; import { eq, and, isNull } from 'drizzle-orm'; import { TRPCError } from '@trpc/server'; -import type { Owner } from '@/lib/integrations/core/types'; -import { INTEGRATION_STATUS } from '@/lib/integrations/core/constants'; +import { requireNumericPlatformRepositories, type Owner } from '@/lib/integrations/core/types'; +import { INTEGRATION_STATUS, PLATFORM } from '@/lib/integrations/core/constants'; import { deleteIntegration, findPendingInstallationByKiloUserId, @@ -176,7 +176,13 @@ export async function listRepositories( const [integration] = await db .select() .from(platform_integrations) - .where(and(eq(platform_integrations.id, integrationId), ownershipCondition)) + .where( + and( + eq(platform_integrations.id, integrationId), + ownershipCondition, + eq(platform_integrations.platform, PLATFORM.GITHUB) + ) + ) .limit(1); if (!integration) { @@ -193,8 +199,9 @@ export async function listRepositories( }); } + const cachedRepositories = requireNumericPlatformRepositories(integration.repositories); // If forceRefresh, no cached repos, or never synced before, fetch from GitHub and update cache - if (forceRefresh || !integration.repositories?.length || !integration.repositories_synced_at) { + if (forceRefresh || !cachedRepositories?.length || !integration.repositories_synced_at) { const appType = integration.github_app_type || 'standard'; const repos = await fetchGitHubRepositories(integration.platform_installation_id, appType); await updateRepositoriesForIntegration(integrationId, repos); @@ -206,7 +213,7 @@ export async function listRepositories( // Return cached repos return { - repositories: integration.repositories, + repositories: cachedRepositories, syncedAt: integration.repositories_synced_at, }; } @@ -266,7 +273,13 @@ export async function listBranches( const [integration] = await db .select() .from(platform_integrations) - .where(and(eq(platform_integrations.id, integrationId), ownershipCondition)) + .where( + and( + eq(platform_integrations.id, integrationId), + ownershipCondition, + eq(platform_integrations.platform, PLATFORM.GITHUB) + ) + ) .limit(1); if (!integration) { diff --git a/apps/web/src/lib/integrations/gitlab-service.ts b/apps/web/src/lib/integrations/gitlab-service.ts index 67a31b9824..793f0d3538 100644 --- a/apps/web/src/lib/integrations/gitlab-service.ts +++ b/apps/web/src/lib/integrations/gitlab-service.ts @@ -4,7 +4,7 @@ import type { PlatformIntegration } from '@kilocode/db/schema'; import { platform_integrations } from '@kilocode/db/schema'; import { eq, and } from 'drizzle-orm'; import { TRPCError } from '@trpc/server'; -import type { Owner } from '@/lib/integrations/core/types'; +import { requireNumericPlatformRepositories, type Owner } from '@/lib/integrations/core/types'; import { INTEGRATION_STATUS, PLATFORM } from '@/lib/integrations/core/constants'; import { updateRepositoriesForIntegration } from '@/lib/integrations/db/platform-integrations'; import { resetCodeReviewConfigForOwner } from '@/lib/agent-config/db/agent-configs'; @@ -180,8 +180,9 @@ export async function listGitLabRepositories( }); } + const cachedRepositories = requireNumericPlatformRepositories(integration.repositories); // If forceRefresh, no cached repos, or never synced before, fetch from GitLab and update cache - if (forceRefresh || !integration.repositories?.length || !integration.repositories_synced_at) { + if (forceRefresh || !cachedRepositories?.length || !integration.repositories_synced_at) { const accessToken = await getValidGitLabToken(integration); const metadata = integration.metadata as { gitlab_instance_url?: string } | null; const instanceUrl = normalizeInstanceUrl(metadata?.gitlab_instance_url); @@ -197,7 +198,7 @@ export async function listGitLabRepositories( // Return cached repos return { - repositories: integration.repositories, + repositories: cachedRepositories, syncedAt: integration.repositories_synced_at, }; } diff --git a/apps/web/src/lib/integrations/oauth/common.ts b/apps/web/src/lib/integrations/oauth/common.ts index 219258ac63..003ed94dd8 100644 --- a/apps/web/src/lib/integrations/oauth/common.ts +++ b/apps/web/src/lib/integrations/oauth/common.ts @@ -9,7 +9,7 @@ import { requireActiveSubscriptionOrTrial } from '@/lib/organizations/trial-midd import { createOAuthState, verifyOAuthState } from '@/lib/integrations/oauth-state'; import { validateReturnPath } from '@/lib/integrations/validate-return-path'; import type { Owner } from '@/lib/integrations/core/types'; -import type { StandardOAuthPlatform } from '@/lib/integrations/oauth/paths'; +import type { RetainedOAuthPlatform, StandardOAuthPlatform } from '@/lib/integrations/oauth/paths'; type AuthenticatedOAuthUser = Parameters[0]['user']; @@ -99,7 +99,7 @@ export function buildIntegrationOAuthRedirectPathFromState( } export function buildIntegrationOAuthConnectErrorPath( - platform: StandardOAuthPlatform, + platform: RetainedOAuthPlatform, organizationId: string | null | undefined, errorCode: string ): string { diff --git a/apps/web/src/lib/integrations/oauth/paths.test.ts b/apps/web/src/lib/integrations/oauth/paths.test.ts new file mode 100644 index 0000000000..5875073e89 --- /dev/null +++ b/apps/web/src/lib/integrations/oauth/paths.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from '@jest/globals'; +import { PLATFORM } from '@/lib/integrations/core/constants'; +import { STANDARD_OAUTH_PLATFORMS } from './paths'; + +describe('STANDARD_OAUTH_PLATFORMS', () => { + it('does not compose Bitbucket OAuth in V1', () => { + expect(STANDARD_OAUTH_PLATFORMS).not.toContain(PLATFORM.BITBUCKET); + }); +}); diff --git a/apps/web/src/lib/integrations/oauth/paths.ts b/apps/web/src/lib/integrations/oauth/paths.ts index 3dd8075215..a0446d2122 100644 --- a/apps/web/src/lib/integrations/oauth/paths.ts +++ b/apps/web/src/lib/integrations/oauth/paths.ts @@ -9,6 +9,7 @@ export const STANDARD_OAUTH_PLATFORMS = [ ] as const; export type StandardOAuthPlatform = (typeof STANDARD_OAUTH_PLATFORMS)[number]; +export type RetainedOAuthPlatform = StandardOAuthPlatform | typeof PLATFORM.BITBUCKET; export function getPlatformOAuthConnectPath( platform: StandardOAuthPlatform, diff --git a/apps/web/src/lib/integrations/oauth/platforms/bitbucket-callback.ts b/apps/web/src/lib/integrations/oauth/platforms/bitbucket-callback.ts new file mode 100644 index 0000000000..4d16edb4d0 --- /dev/null +++ b/apps/web/src/lib/integrations/oauth/platforms/bitbucket-callback.ts @@ -0,0 +1,165 @@ +import type { NextRequest } from 'next/server'; +import { createHash } from 'node:crypto'; +import { NextResponse } from 'next/server'; +import { captureException, captureMessage } from '@sentry/nextjs'; +import { APP_URL } from '@/lib/constants'; +import type { Owner } from '@/lib/integrations/core/types'; +import { + appendIntegrationOAuthRedirectQuery, + parseOAuthStateOwner, +} from '@/lib/integrations/oauth/common'; +import { verifyOAuthState } from '@/lib/integrations/oauth-state'; +import { + exchangeBitbucketOAuthCode, + fetchBitbucketUser, + fetchBitbucketWorkspaces, +} from '@/lib/integrations/platforms/bitbucket/adapter'; +import { + BitbucketIntegrationAuthorizationError, + BitbucketIntegrationConnectionConflictError, + storeBitbucketIntegration, +} from '@/lib/integrations/platforms/bitbucket/credentials'; +import { scheduleBitbucketRepositoryCachePrime } from '@/lib/integrations/platforms/bitbucket/repository-cache'; +import { getUserFromAuth } from '@/lib/user/server'; +import { ensureOrganizationAccess } from '@/routers/organizations/utils'; + +type CallbackState = { owner: string; returnTo?: string }; +type CallbackPhase = + | 'authenticate' + | 'authorize_owner' + | 'token_exchange' + | 'provider_profile' + | 'store_integration'; +type AuthenticatedOAuthUser = Parameters[0]['user']; + +function redirectWithStatus( + state: CallbackState | null, + key: 'success' | 'error', + value: string +): NextResponse { + const owner = state ? parseOAuthStateOwner(state.owner) : null; + const defaultPath = + owner?.type === 'org' + ? `/organizations/${owner.id}/integrations/bitbucket` + : '/integrations/bitbucket'; + const path = state?.returnTo + ? appendIntegrationOAuthRedirectQuery(state.returnTo, `${key}=${encodeURIComponent(value)}`) + : `${defaultPath}?${key}=${encodeURIComponent(value)}`; + return NextResponse.redirect(new URL(path, APP_URL)); +} + +function safeCallbackContext(searchParams: URLSearchParams) { + const state = searchParams.get('state'); + return { + hasCode: Boolean(searchParams.get('code')), + hasState: Boolean(state), + stateHash: state ? createHash('sha256').update(state).digest('hex').slice(0, 8) : null, + hasProviderError: Boolean(searchParams.get('error')), + }; +} + +function validOAuthCode(code: string | null): string | null { + if (!code || code.length > 2048 || !/^[A-Za-z0-9._~+/-]+$/.test(code)) return null; + return code; +} + +async function authorizeOwner(owner: Owner, user: AuthenticatedOAuthUser): Promise { + if (owner.type === 'user') { + if (owner.id !== user.id) throw new Error('OAuth owner mismatch'); + return; + } + await ensureOrganizationAccess({ user }, owner.id, ['owner', 'billing_manager']); +} + +export async function handleBitbucketOAuthCallback(request: NextRequest): Promise { + const searchParams = request.nextUrl.searchParams; + const verifiedState = verifyOAuthState(searchParams.get('state')); + let callbackPhase: CallbackPhase = 'authenticate'; + + try { + const { user, authFailedResponse } = await getUserFromAuth({ adminOnly: false }); + if (authFailedResponse) { + return NextResponse.redirect(new URL('/users/sign_in', APP_URL)); + } + + if (!verifiedState) { + captureMessage('Bitbucket OAuth callback invalid state', { + level: 'warning', + tags: { endpoint: 'bitbucket/callback', source: 'bitbucket_oauth' }, + extra: safeCallbackContext(searchParams), + }); + return redirectWithStatus(null, 'error', 'invalid_state'); + } + + const owner = parseOAuthStateOwner(verifiedState.owner); + if (verifiedState.userId !== user.id || !owner) { + return redirectWithStatus(verifiedState, 'error', 'unauthorized'); + } + callbackPhase = 'authorize_owner'; + try { + await authorizeOwner(owner, user); + } catch { + return redirectWithStatus(verifiedState, 'error', 'unauthorized'); + } + + if (searchParams.get('error')) { + return redirectWithStatus(verifiedState, 'error', 'authorization_cancelled'); + } + + const code = validOAuthCode(searchParams.get('code')); + if (!code) { + return redirectWithStatus(verifiedState, 'error', 'missing_code'); + } + + callbackPhase = 'token_exchange'; + const tokens = await exchangeBitbucketOAuthCode(code); + callbackPhase = 'provider_profile'; + const [bitbucketUser, availableWorkspaces] = await Promise.all([ + fetchBitbucketUser(tokens.accessToken), + fetchBitbucketWorkspaces(tokens.accessToken), + ]); + if (availableWorkspaces.length === 0) { + return redirectWithStatus(verifiedState, 'error', 'no_workspaces'); + } + + callbackPhase = 'store_integration'; + const storedIntegration = await storeBitbucketIntegration({ + owner, + authorizedByUserId: user.id, + bitbucketUser, + tokens, + availableWorkspaces, + }); + if (storedIntegration.status === 'connected') { + scheduleBitbucketRepositoryCachePrime({ + owner, + kiloUserId: user.id, + integrationId: storedIntegration.integrationId, + }); + } + + return redirectWithStatus(verifiedState, 'success', storedIntegration.status); + } catch (error) { + if (error instanceof BitbucketIntegrationAuthorizationError) { + return redirectWithStatus(verifiedState, 'error', 'unauthorized'); + } + if (error instanceof BitbucketIntegrationConnectionConflictError) { + return redirectWithStatus(verifiedState, 'error', 'connection_exists'); + } + + const callbackContext = safeCallbackContext(searchParams); + if (process.env.NODE_ENV === 'development') { + console.error('Bitbucket OAuth callback failed', { + phase: callbackPhase, + errorName: error instanceof Error ? error.name : 'UnknownError', + errorMessage: error instanceof Error ? error.message : 'Unknown error', + ...callbackContext, + }); + } + captureException(error, { + tags: { endpoint: 'bitbucket/callback', source: 'bitbucket_oauth' }, + extra: { phase: callbackPhase, ...callbackContext }, + }); + return redirectWithStatus(verifiedState, 'error', 'connection_failed'); + } +} diff --git a/apps/web/src/lib/integrations/oauth/platforms/bitbucket-connect.ts b/apps/web/src/lib/integrations/oauth/platforms/bitbucket-connect.ts new file mode 100644 index 0000000000..13ed91f066 --- /dev/null +++ b/apps/web/src/lib/integrations/oauth/platforms/bitbucket-connect.ts @@ -0,0 +1,59 @@ +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; +import { captureException } from '@sentry/nextjs'; +import { APP_URL } from '@/lib/constants'; +import { PLATFORM } from '@/lib/integrations/core/constants'; +import type { Owner } from '@/lib/integrations/core/types'; +import { buildBitbucketOAuthUrl } from '@/lib/integrations/platforms/bitbucket/adapter'; +import { createOAuthState } from '@/lib/integrations/oauth-state'; +import { + buildIntegrationOAuthConnectErrorPath, + redirectToSignInForOAuthConnect, +} from '@/lib/integrations/oauth/common'; +import { validateReturnPath } from '@/lib/integrations/validate-return-path'; +import { getUserFromAuth } from '@/lib/user/server'; +import { ensureOrganizationAccess } from '@/routers/organizations/utils'; + +function detailPath(organizationId: string | null): string { + return organizationId + ? `/organizations/${organizationId}/integrations/bitbucket` + : '/integrations/bitbucket'; +} + +export async function handleBitbucketOAuthConnect(request: NextRequest): Promise { + const organizationId = request.nextUrl.searchParams.get('organizationId'); + + try { + const { user, authFailedResponse } = await getUserFromAuth({ adminOnly: false }); + if (authFailedResponse) { + return redirectToSignInForOAuthConnect(request, detailPath(organizationId)); + } + + const owner: Owner = organizationId + ? { type: 'org', id: organizationId } + : { type: 'user', id: user.id }; + if (owner.type === 'org') { + await ensureOrganizationAccess({ user }, owner.id, ['owner', 'billing_manager']); + } + + const returnToParam = request.nextUrl.searchParams.get('returnTo'); + const returnTo = returnToParam ? validateReturnPath(returnToParam) : null; + const state = createOAuthState(`${owner.type}_${owner.id}`, user.id, returnTo ?? undefined); + return NextResponse.redirect(buildBitbucketOAuthUrl(state)); + } catch (error) { + captureException(error, { + tags: { endpoint: 'bitbucket/connect', source: 'bitbucket_oauth' }, + extra: { hasOrganizationId: Boolean(organizationId) }, + }); + return NextResponse.redirect( + new URL( + buildIntegrationOAuthConnectErrorPath( + PLATFORM.BITBUCKET, + organizationId, + 'oauth_init_failed' + ), + APP_URL + ) + ); + } +} diff --git a/apps/web/src/lib/integrations/oauth/routes.ts b/apps/web/src/lib/integrations/oauth/routes.ts index 78915539f0..1a8983fe34 100644 --- a/apps/web/src/lib/integrations/oauth/routes.ts +++ b/apps/web/src/lib/integrations/oauth/routes.ts @@ -71,6 +71,12 @@ export async function handlePlatformOAuthConnect( request: NextRequest, platform: string ): Promise { + if (platform === PLATFORM.BITBUCKET) { + return ( + await import('@/lib/integrations/oauth/platforms/bitbucket-connect') + ).handleBitbucketOAuthConnect(request); + } + if (platform === PLATFORM.GITLAB) { return ( await import('@/lib/integrations/oauth/platforms/gitlab-connect') @@ -114,6 +120,10 @@ export async function handlePlatformOAuthCallback( platform: string ): Promise { switch (platform) { + case PLATFORM.BITBUCKET: + return ( + await import('@/lib/integrations/oauth/platforms/bitbucket-callback') + ).handleBitbucketOAuthCallback(request); case PLATFORM.DISCORD: return ( await import('@/lib/integrations/oauth/platforms/discord-callback') diff --git a/apps/web/src/lib/integrations/platform-definitions.test.ts b/apps/web/src/lib/integrations/platform-definitions.test.ts new file mode 100644 index 0000000000..c5ccee0f61 --- /dev/null +++ b/apps/web/src/lib/integrations/platform-definitions.test.ts @@ -0,0 +1,23 @@ +import { PLATFORM } from '@/lib/integrations/core/constants'; +import { + buildPlatforms, + getPlatformDefinitionCountForOwner, +} from '@/lib/integrations/platform-definitions'; + +describe('integration platform definitions', () => { + it('omits Bitbucket from personal integrations', () => { + const platforms = buildPlatforms([]); + + expect(platforms.map(platform => platform.id)).not.toContain(PLATFORM.BITBUCKET); + }); + + it('includes Bitbucket for organization integrations and owner-scoped skeletons', () => { + const organizationId = '123e4567-e89b-12d3-a456-426614174000'; + const platforms = buildPlatforms([], organizationId); + + expect(platforms.map(platform => platform.id)).toContain(PLATFORM.BITBUCKET); + expect(getPlatformDefinitionCountForOwner(organizationId)).toBe( + getPlatformDefinitionCountForOwner() + 1 + ); + }); +}); diff --git a/apps/web/src/lib/integrations/platform-definitions.ts b/apps/web/src/lib/integrations/platform-definitions.ts index e97993f9c8..562e8ef648 100644 --- a/apps/web/src/lib/integrations/platform-definitions.ts +++ b/apps/web/src/lib/integrations/platform-definitions.ts @@ -86,13 +86,24 @@ export const PLATFORM_DEFINITIONS: PlatformDefinition[] = [ orgRoute: organizationId => `/organizations/${organizationId}/integrations/dolthub`, }, { - id: 'bitbucket', + id: PLATFORM.BITBUCKET, name: 'Bitbucket', - description: 'Integrate Bitbucket repositories for intelligent code analysis and automation', - enabled: false, + description: 'Connect your Bitbucket Cloud repositories to Kilo Code', + enabled: true, + orgRoute: organizationId => `/organizations/${organizationId}/integrations/bitbucket`, }, ]; +function getPlatformDefinitionsForOwner(organizationId?: string): PlatformDefinition[] { + return PLATFORM_DEFINITIONS.filter(definition => + organizationId ? Boolean(definition.orgRoute) : Boolean(definition.personalRoute) + ); +} + +export function getPlatformDefinitionCountForOwner(organizationId?: string): number { + return getPlatformDefinitionsForOwner(organizationId).length; +} + type InstallationStatus = Partial>; function buildInstallationStatusMap( @@ -115,17 +126,19 @@ export function buildPlatforms( ): Platform[] { const installations = buildInstallationStatusMap(installationStatuses); - return PLATFORM_DEFINITIONS.filter(def => { - if (def.hiddenUnlessInstalled) { - return installations[def.id as keyof InstallationStatus]?.installed === true; - } - return true; - }).map(def => ({ - id: def.id, - name: def.name, - description: def.description, - status: getStatus(def.id, installations), - enabled: def.enabled, - route: organizationId ? def.orgRoute?.(organizationId) : def.personalRoute, - })); + return getPlatformDefinitionsForOwner(organizationId) + .filter(def => { + if (def.hiddenUnlessInstalled) { + return installations[def.id as keyof InstallationStatus]?.installed === true; + } + return true; + }) + .map(def => ({ + id: def.id, + name: def.name, + description: def.description, + status: getStatus(def.id, installations), + enabled: def.enabled, + route: organizationId ? def.orgRoute?.(organizationId) : def.personalRoute, + })); } diff --git a/apps/web/src/lib/integrations/platform-integration-setup-status.ts b/apps/web/src/lib/integrations/platform-integration-setup-status.ts index a471b17fec..75264f686c 100644 --- a/apps/web/src/lib/integrations/platform-integration-setup-status.ts +++ b/apps/web/src/lib/integrations/platform-integration-setup-status.ts @@ -6,6 +6,7 @@ export const SETUP_STATUS_PLATFORMS = [ PLATFORM.DISCORD, PLATFORM.GITHUB, PLATFORM.GITLAB, + PLATFORM.BITBUCKET, PLATFORM.LINEAR, PLATFORM.DOLTHUB, ] as const; @@ -42,6 +43,7 @@ function buildInstallationSummary( if ( integration.platform === PLATFORM.GITHUB || integration.platform === PLATFORM.GITLAB || + integration.platform === PLATFORM.BITBUCKET || integration.platform === PLATFORM.DOLTHUB ) { return { accountLogin: integration.platform_account_login }; diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/adapter.test.ts b/apps/web/src/lib/integrations/platforms/bitbucket/adapter.test.ts new file mode 100644 index 0000000000..945552c49a --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/adapter.test.ts @@ -0,0 +1,596 @@ +jest.mock('@/lib/config.server', () => ({ + BITBUCKET_CLIENT_ID: 'bitbucket-client-id', + BITBUCKET_CLIENT_SECRET: 'bitbucket-client-secret', +})); + +import { + buildBitbucketOAuthUrl, + exchangeBitbucketOAuthCode, + fetchBitbucketUser, + fetchBitbucketWorkspaces, +} from './adapter'; + +function validTokenResponse(overrides: Record = {}): Response { + return Response.json({ + access_token: 'access-token', + refresh_token: 'refresh-token', + token_type: 'bearer', + expires_in: 3600, + scope: 'repository:write account pullrequest webhook', + ...overrides, + }); +} + +describe('Bitbucket OAuth adapter', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + it('builds the canonical authorization URL with only the required scopes', () => { + const url = new URL(buildBitbucketOAuthUrl('signed-state')); + + expect(`${url.origin}${url.pathname}`).toBe('https://bitbucket.org/site/oauth2/authorize'); + expect(Object.fromEntries(url.searchParams)).toEqual({ + client_id: 'bitbucket-client-id', + response_type: 'code', + scope: 'account repository:write pullrequest webhook', + state: 'signed-state', + }); + expect(url.toString()).not.toContain('bitbucket-client-secret'); + }); + + it('exchanges an authorization code with Basic auth and a form body', async () => { + const authorizationCode = 'authorization code+&='; + const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValueOnce(validTokenResponse()); + + await expect(exchangeBitbucketOAuthCode(authorizationCode)).resolves.toEqual({ + accessToken: 'access-token', + refreshToken: 'refresh-token', + tokenType: 'bearer', + expiresIn: 3600, + scopes: ['account', 'email', 'pullrequest', 'repository', 'repository:write', 'webhook'], + }); + + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, init] = fetchMock.mock.calls[0] ?? []; + expect(url).toBe('https://bitbucket.org/site/oauth2/access_token'); + expect(init).toEqual( + expect.objectContaining({ + method: 'POST', + redirect: 'manual', + headers: { + Accept: 'application/json', + Authorization: `Basic ${Buffer.from( + 'bitbucket-client-id:bitbucket-client-secret' + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + ); + expect(new URLSearchParams(init?.body as string)).toEqual( + new URLSearchParams({ + grant_type: 'authorization_code', + code: authorizationCode, + }) + ); + }); + + it.each([ + { access_token: '' }, + { access_token: ' ' }, + { access_token: ' access-token' }, + { access_token: 'access-token ' }, + { refresh_token: '' }, + { refresh_token: ' ' }, + { refresh_token: ' refresh-token' }, + { refresh_token: 'refresh-token ' }, + ])('rejects invalid rotating OAuth tokens', async invalidToken => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce(validTokenResponse(invalidToken)); + + await expect(exchangeBitbucketOAuthCode('authorization-code')).rejects.toThrow( + 'Bitbucket OAuth token exchange returned invalid credentials' + ); + }); + + it('rejects token responses that do not use bearer authentication', async () => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce(validTokenResponse({ token_type: 'mac' })); + + await expect(exchangeBitbucketOAuthCode('authorization-code')).rejects.toThrow( + 'Bitbucket OAuth token exchange returned invalid credentials' + ); + }); + + it.each([0, -1, 1.5, 86_401])('rejects invalid or unbounded token expiry', async expiresIn => { + jest + .spyOn(global, 'fetch') + .mockResolvedValueOnce(validTokenResponse({ expires_in: expiresIn })); + + await expect(exchangeBitbucketOAuthCode('authorization-code')).rejects.toThrow( + 'Bitbucket OAuth token exchange returned invalid credentials' + ); + }); + + it('accepts the transitional plural scopes field alongside canonical scope', async () => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + validTokenResponse({ + scopes: 'repository:write repository account pullrequest webhook', + }) + ); + + await expect(exchangeBitbucketOAuthCode('authorization-code')).resolves.toEqual( + expect.objectContaining({ + scopes: ['account', 'email', 'pullrequest', 'repository', 'repository:write', 'webhook'], + }) + ); + }); + + it('accepts Atlassian legacy scope aliases returned by Bitbucket OAuth', async () => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + validTokenResponse({ + scope: [ + 'read:pullrequest:bitbucket-legacy', + 'pullrequest', + 'offline_access', + 'write:repository:bitbucket-legacy', + 'read:account:bitbucket-legacy', + 'admin:webhook:bitbucket-legacy', + 'read:email:bitbucket-legacy', + 'read:repository:bitbucket-legacy', + ].join(' '), + }) + ); + + await expect(exchangeBitbucketOAuthCode('authorization-code')).resolves.toEqual( + expect.objectContaining({ + scopes: ['account', 'email', 'pullrequest', 'repository', 'repository:write', 'webhook'], + }) + ); + }); + + it('rejects the retired plural scopes response field without canonical scope', async () => { + jest + .spyOn(global, 'fetch') + .mockResolvedValueOnce( + validTokenResponse({ scope: undefined, scopes: 'account repository:write webhook' }) + ); + + await expect(exchangeBitbucketOAuthCode('authorization-code')).rejects.toThrow( + 'Bitbucket OAuth token exchange returned invalid credentials' + ); + }); + + it.each([ + 'account repository webhook', + 'repository:write repository webhook', + 'account repository:write webhook', + 'account repository:write', + ])('rejects token responses missing a required OAuth scope', async scope => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce(validTokenResponse({ scope })); + + await expect(exchangeBitbucketOAuthCode('authorization-code')).rejects.toThrow( + 'Bitbucket OAuth token exchange returned invalid credentials' + ); + }); + + it('ignores token response scopes beyond the required OAuth grant', async () => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + validTokenResponse({ + scope: 'account repository repository:write pullrequest webhook snippet', + }) + ); + + await expect(exchangeBitbucketOAuthCode('authorization-code')).resolves.toEqual( + expect.objectContaining({ + scopes: ['account', 'email', 'pullrequest', 'repository', 'repository:write', 'webhook'], + }) + ); + }); + + it('accepts the email permission implied by the required account scope', async () => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + validTokenResponse({ + scope: 'repository:write repository account email pullrequest webhook', + }) + ); + + await expect(exchangeBitbucketOAuthCode('authorization-code')).resolves.toEqual( + expect.objectContaining({ + scopes: ['account', 'email', 'pullrequest', 'repository', 'repository:write', 'webhook'], + }) + ); + }); + + it('normalizes duplicate and implied OAuth scopes', async () => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + validTokenResponse({ + scope: ' repository:write account repository:write pullrequest webhook webhook ', + }) + ); + + await expect(exchangeBitbucketOAuthCode('authorization-code')).resolves.toEqual( + expect.objectContaining({ + scopes: ['account', 'email', 'pullrequest', 'repository', 'repository:write', 'webhook'], + }) + ); + }); + + it('does not expose malformed token response bodies', async () => { + const providerBody = 'provider-access-token-is-not-json'; + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + new Response(providerBody, { + headers: { 'Content-Type': 'application/json' }, + }) + ); + + const exchange = exchangeBitbucketOAuthCode('authorization-code'); + await expect(exchange).rejects.toThrow( + 'Bitbucket OAuth token exchange returned invalid credentials' + ); + await expect(exchange).rejects.not.toThrow(providerBody); + }); + + it('does not expose failed token response bodies', async () => { + const providerBody = 'provider-error-containing-a-token'; + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + new Response(providerBody, { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }) + ); + + const exchange = exchangeBitbucketOAuthCode('authorization-code'); + await expect(exchange).rejects.toThrow('Bitbucket OAuth token exchange failed (400)'); + await expect(exchange).rejects.not.toThrow(providerBody); + }); + + it('fetches only the safe current-user identity fields', async () => { + const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValueOnce( + Response.json({ + uuid: '{user-uuid}', + nickname: 'octobucket', + display_name: 'Octo Bucket', + email: 'must-not-leak@example.com', + links: { self: { href: 'https://api.bitbucket.org/2.0/users/user-uuid' } }, + }) + ); + + await expect(fetchBitbucketUser('access-token')).resolves.toEqual({ + uuid: '{user-uuid}', + nickname: 'octobucket', + displayName: 'Octo Bucket', + }); + expect(fetchMock).toHaveBeenCalledWith('https://api.bitbucket.org/2.0/user', { + redirect: 'manual', + headers: { + Accept: 'application/json', + Authorization: 'Bearer access-token', + }, + }); + }); + + it.each(['uuid', 'nickname', 'display_name'])( + 'rejects a current-user response with a blank %s', + async field => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + Response.json({ + uuid: '{user-uuid}', + nickname: 'octobucket', + display_name: 'Octo Bucket', + [field]: ' ', + }) + ); + + await expect(fetchBitbucketUser('access-token')).rejects.toThrow( + 'Bitbucket current-user request returned an invalid identity' + ); + } + ); + + it.each([ + ['uuid', ' {user-uuid}'], + ['nickname', 'octobucket '], + ['display_name', ' Octo Bucket'], + ])('rejects a current-user response with whitespace-padded %s', async (field, value) => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + Response.json({ + uuid: '{user-uuid}', + nickname: 'octobucket', + display_name: 'Octo Bucket', + [field]: value, + }) + ); + + await expect(fetchBitbucketUser('access-token')).rejects.toThrow( + 'Bitbucket current-user request returned an invalid identity' + ); + }); + + it('does not expose malformed current-user response bodies', async () => { + const providerBody = 'provider-user-body-containing-a-token'; + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + new Response(providerBody, { + headers: { 'Content-Type': 'application/json' }, + }) + ); + + const request = fetchBitbucketUser('access-token'); + await expect(request).rejects.toThrow( + 'Bitbucket current-user request returned an invalid identity' + ); + await expect(request).rejects.not.toThrow(providerBody); + }); + + it('does not expose failed current-user response bodies or access tokens', async () => { + const accessToken = 'current-user-access-token'; + const providerBody = 'provider-current-user-error-body'; + jest.spyOn(global, 'fetch').mockResolvedValueOnce(new Response(providerBody, { status: 401 })); + + const request = fetchBitbucketUser(accessToken); + await expect(request).rejects.toThrow('Bitbucket current-user request failed (401)'); + await expect(request).rejects.not.toThrow(providerBody); + await expect(request).rejects.not.toThrow(accessToken); + }); + + it('fetches safe workspace metadata with manual redirects', async () => { + const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValueOnce( + Response.json({ + values: [ + { + administrator: true, + workspace: { + uuid: '{workspace-uuid}', + slug: 'kilo-workspace', + name: 'Kilo Workspace', + links: { self: { href: 'https://api.bitbucket.org/2.0/workspaces/kilo-workspace' } }, + }, + }, + ], + }) + ); + + await expect(fetchBitbucketWorkspaces('access-token')).resolves.toEqual([ + { + uuid: '{workspace-uuid}', + slug: 'kilo-workspace', + name: 'Kilo Workspace', + }, + ]); + expect(fetchMock).toHaveBeenCalledWith('https://api.bitbucket.org/2.0/user/workspaces', { + redirect: 'manual', + headers: { + Accept: 'application/json', + Authorization: 'Bearer access-token', + }, + }); + }); + + it.each(['uuid', 'slug', 'name'])('rejects a workspace response with a blank %s', async field => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + Response.json({ + values: [ + { + workspace: { + uuid: '{workspace-uuid}', + slug: 'kilo-workspace', + name: 'Kilo Workspace', + [field]: ' ', + }, + }, + ], + }) + ); + + await expect(fetchBitbucketWorkspaces('access-token')).rejects.toThrow( + 'Bitbucket workspace request returned an invalid response' + ); + }); + + it.each([ + ['uuid', ' {workspace-uuid}'], + ['slug', 'kilo-workspace '], + ['name', ' Kilo Workspace'], + ])('rejects a workspace response with whitespace-padded %s', async (field, value) => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + Response.json({ + values: [ + { + workspace: { + uuid: '{workspace-uuid}', + slug: 'kilo-workspace', + name: 'Kilo Workspace', + [field]: value, + }, + }, + ], + }) + ); + + await expect(fetchBitbucketWorkspaces('access-token')).rejects.toThrow( + 'Bitbucket workspace request returned an invalid response' + ); + }); + + it('does not expose malformed workspace response bodies', async () => { + const providerBody = 'provider-workspace-body-containing-a-token'; + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + new Response(providerBody, { + headers: { 'Content-Type': 'application/json' }, + }) + ); + + const request = fetchBitbucketWorkspaces('access-token'); + await expect(request).rejects.toThrow( + 'Bitbucket workspace request returned an invalid response' + ); + await expect(request).rejects.not.toThrow(providerBody); + }); + + it('does not expose failed workspace response bodies or access tokens', async () => { + const accessToken = 'workspace-access-token'; + const providerBody = 'provider-workspace-error-body'; + jest.spyOn(global, 'fetch').mockResolvedValueOnce(new Response(providerBody, { status: 403 })); + + const request = fetchBitbucketWorkspaces(accessToken); + await expect(request).rejects.toThrow('Bitbucket workspace request failed (403)'); + await expect(request).rejects.not.toThrow(providerBody); + await expect(request).rejects.not.toThrow(accessToken); + }); + + it('follows opaque workspace pagination links', async () => { + const nextUrl = 'https://api.bitbucket.org/2.0/user/workspaces?cursor=opaque%3Dvalue'; + const fetchMock = jest + .spyOn(global, 'fetch') + .mockResolvedValueOnce( + Response.json({ + values: [ + { + workspace: { + uuid: '{workspace-1}', + slug: 'workspace-1', + name: 'Workspace One', + }, + }, + ], + next: nextUrl, + }) + ) + .mockResolvedValueOnce( + Response.json({ + values: [ + { + workspace: { + uuid: '{workspace-2}', + slug: 'workspace-2', + name: 'Workspace Two', + }, + }, + ], + }) + ); + + await expect(fetchBitbucketWorkspaces('access-token')).resolves.toEqual([ + { uuid: '{workspace-1}', slug: 'workspace-1', name: 'Workspace One' }, + { uuid: '{workspace-2}', slug: 'workspace-2', name: 'Workspace Two' }, + ]); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + nextUrl, + expect.objectContaining({ redirect: 'manual' }) + ); + }); + + it('rejects workspace API redirects without following them', async () => { + const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValueOnce( + new Response(null, { + status: 302, + headers: { Location: 'https://api.bitbucket.org/2.0/user/workspaces?cursor=next' }, + }) + ); + + await expect(fetchBitbucketWorkspaces('access-token')).rejects.toThrow( + 'Bitbucket workspace request failed (302)' + ); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it.each([ + 'https://attacker.example/2.0/user/workspaces?cursor=next', + 'http://api.bitbucket.org/2.0/user/workspaces?cursor=next', + 'HTTPS://api.bitbucket.org/2.0/user/workspaces?cursor=next', + 'https://API.bitbucket.org/2.0/user/workspaces?cursor=next', + 'https://api.bitbucket.org./2.0/user/workspaces?cursor=next', + 'https://user:password@api.bitbucket.org/2.0/user/workspaces?cursor=next', + 'https://api.bitbucket.org:443/2.0/user/workspaces?cursor=next', + 'https://api.bitbucket.org:8443/2.0/user/workspaces?cursor=next', + 'https://api.bitbucket.org/2.0/user/workspaces#', + 'https://api.bitbucket.org/2.0/user/workspaces?cursor=next#', + 'https://api.bitbucket.org/2.0/user/workspaces?cursor=next#fragment', + 'https://api.bitbucket.org/2.0/user/workspaces/?cursor=next', + 'https://api.bitbucket.org/2.0/repositories?cursor=next', + 'https://api.bitbucket.org/2.0/user/segment/../workspaces?cursor=next', + 'https://api.bitbucket.org/2.0/user/%2e%2e/user/workspaces?cursor=next', + ])('rejects unsafe workspace pagination URLs before fetching them', async next => { + const fetchMock = jest + .spyOn(global, 'fetch') + .mockResolvedValueOnce(Response.json({ values: [], next })); + + await expect(fetchBitbucketWorkspaces('access-token')).rejects.toThrow( + 'Bitbucket refused unsafe workspace pagination URL' + ); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it('rejects workspace pagination cycles before refetching a page', async () => { + const firstUrl = 'https://api.bitbucket.org/2.0/user/workspaces'; + const secondUrl = `${firstUrl}?cursor=second`; + const fetchMock = jest + .spyOn(global, 'fetch') + .mockResolvedValueOnce(Response.json({ values: [], next: secondUrl })) + .mockResolvedValueOnce(Response.json({ values: [], next: firstUrl })) + .mockRejectedValueOnce(new Error('must not fetch a visited page')); + + await expect(fetchBitbucketWorkspaces('access-token')).rejects.toThrow( + 'Bitbucket workspace pagination cycle detected' + ); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + it('rejects workspace pagination that exceeds the page cap', async () => { + let page = 0; + const fetchMock = jest.spyOn(global, 'fetch').mockImplementation(async () => { + page += 1; + if (page > 21) { + throw new Error('must stop at the workspace page cap'); + } + return Response.json({ + values: [], + next: `https://api.bitbucket.org/2.0/user/workspaces?cursor=${page + 1}`, + }); + }); + + await expect(fetchBitbucketWorkspaces('access-token')).rejects.toThrow( + 'Bitbucket workspace pagination exceeded page limit' + ); + expect(fetchMock).toHaveBeenCalledTimes(20); + }); + + it('rejects workspace pagination that exceeds the item cap', async () => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + Response.json({ + values: Array.from({ length: 501 }, (_, index) => ({ + workspace: { + uuid: `{workspace-${index}}`, + slug: `workspace-${index}`, + name: `Workspace ${index}`, + }, + })), + }) + ); + + await expect(fetchBitbucketWorkspaces('access-token')).rejects.toThrow( + 'Bitbucket workspace pagination exceeded item limit' + ); + }); + + it('does not fetch another workspace page after reaching the item cap', async () => { + const fetchMock = jest + .spyOn(global, 'fetch') + .mockResolvedValueOnce( + Response.json({ + values: Array.from({ length: 500 }, (_, index) => ({ + workspace: { + uuid: `{workspace-${index}}`, + slug: `workspace-${index}`, + name: `Workspace ${index}`, + }, + })), + next: 'https://api.bitbucket.org/2.0/user/workspaces?cursor=overflow', + }) + ) + .mockRejectedValueOnce(new Error('must not fetch past the workspace item cap')); + + await expect(fetchBitbucketWorkspaces('access-token')).rejects.toThrow( + 'Bitbucket workspace pagination exceeded item limit' + ); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/adapter.ts b/apps/web/src/lib/integrations/platforms/bitbucket/adapter.ts new file mode 100644 index 0000000000..f72c4decae --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/adapter.ts @@ -0,0 +1,315 @@ +import 'server-only'; + +import { BITBUCKET_CLIENT_ID, BITBUCKET_CLIENT_SECRET } from '@/lib/config.server'; +import { MAX_BITBUCKET_WORKSPACES, type BitbucketWorkspace } from './metadata'; +import { z } from 'zod'; + +const BITBUCKET_AUTHORIZE_URL = 'https://bitbucket.org/site/oauth2/authorize'; +const BITBUCKET_TOKEN_URL = 'https://bitbucket.org/site/oauth2/access_token'; +const BITBUCKET_CURRENT_USER_URL = 'https://api.bitbucket.org/2.0/user'; +const BITBUCKET_WORKSPACES_URL = 'https://api.bitbucket.org/2.0/user/workspaces'; +const MAX_BITBUCKET_TOKEN_EXPIRY_SECONDS = 24 * 60 * 60; +const MAX_BITBUCKET_WORKSPACE_PAGES = 20; + +const NonEmptyOAuthTokenSchema = z + .string() + .min(1) + .refine(value => value.trim() === value); +const NonEmptyProviderStringSchema = z + .string() + .min(1) + .refine(value => value.trim() === value); + +const BitbucketOAuthTokenPayloadSchema = z + .object({ + access_token: NonEmptyOAuthTokenSchema, + refresh_token: NonEmptyOAuthTokenSchema, + token_type: z + .string() + .transform(value => value.toLowerCase()) + .pipe(z.literal('bearer')), + expires_in: z.number().int().positive().max(MAX_BITBUCKET_TOKEN_EXPIRY_SECONDS), + scope: z.string(), + scopes: z.string().optional(), + }) + .strict(); + +export type BitbucketOAuthTokens = { + accessToken: string; + refreshToken: string; + tokenType: 'bearer'; + expiresIn: number; + scopes: string[]; +}; + +const BitbucketUserPayloadSchema = z.object({ + uuid: NonEmptyProviderStringSchema, + nickname: NonEmptyProviderStringSchema, + display_name: NonEmptyProviderStringSchema, +}); + +export type BitbucketUser = { + uuid: string; + nickname: string; + displayName: string; +}; + +const BitbucketWorkspacePageSchema = z.object({ + values: z.array( + z.object({ + workspace: z.object({ + uuid: NonEmptyProviderStringSchema, + slug: NonEmptyProviderStringSchema, + name: NonEmptyProviderStringSchema.optional(), + }), + }) + ), + next: z.string().min(1).optional(), +}); + +export const BITBUCKET_OAUTH_SCOPES = [ + 'account', + 'repository:write', + 'pullrequest', + 'webhook', +] as const; + +const BITBUCKET_OAUTH_SCOPE_ALIASES: Record = { + 'read:account:bitbucket-legacy': ['account'], + 'read:email:bitbucket-legacy': ['email'], + 'read:repository:bitbucket-legacy': ['repository'], + 'write:repository:bitbucket-legacy': ['repository:write'], + 'read:webhook:bitbucket-legacy': ['webhook'], + 'write:webhook:bitbucket-legacy': ['webhook'], + 'admin:webhook:bitbucket-legacy': ['webhook'], + pullrequest: ['pullrequest'], + 'read:pullrequest:bitbucket-legacy': ['pullrequest'], + offline_access: [], +}; + +function expandBitbucketOAuthScopeClosure(scopes: Iterable): Set { + const closure = new Set(scopes); + if (closure.has('repository:write')) { + closure.add('repository'); + } + if (closure.has('account')) { + closure.add('email'); + } + return closure; +} + +function normalizeBitbucketOAuthScopes(scope: string): string[] { + const canonicalScopes = new Set(); + for (const rawScope of scope.split(/\s+/).filter(Boolean)) { + for (const canonicalScope of BITBUCKET_OAUTH_SCOPE_ALIASES[rawScope.toLowerCase()] ?? [ + rawScope.toLowerCase(), + ]) { + canonicalScopes.add(canonicalScope); + } + } + + const returnedScopes = expandBitbucketOAuthScopeClosure(canonicalScopes); + const allowedScopes = expandBitbucketOAuthScopeClosure(BITBUCKET_OAUTH_SCOPES); + if (BITBUCKET_OAUTH_SCOPES.some(requiredScope => !returnedScopes.has(requiredScope))) { + const missingScopes = BITBUCKET_OAUTH_SCOPES.filter( + requiredScope => !returnedScopes.has(requiredScope) + ); + throw new Error( + `Bitbucket OAuth token exchange returned invalid credentials: scope_mismatch missing=${missingScopes.join(',') || 'none'} observed=${[...returnedScopes].sort().join(',') || 'none'}` + ); + } + return [...returnedScopes].filter(scope => allowedScopes.has(scope)).sort(); +} + +function describeBitbucketTokenResponseShape(responseBody: unknown): unknown { + if (!responseBody || typeof responseBody !== 'object' || Array.isArray(responseBody)) { + return { type: Array.isArray(responseBody) ? 'array' : typeof responseBody }; + } + + return Object.fromEntries( + Object.entries(responseBody).map(([key, value]) => [ + key, + key.includes('token') && typeof value === 'string' ? 'redacted-string' : typeof value, + ]) + ); +} + +async function readBitbucketJson( + response: Response, + invalidResponseMessage: string +): Promise { + try { + return await response.json(); + } catch { + throw new Error(invalidResponseMessage); + } +} + +function validateBitbucketWorkspacePageUrl(value: string): string { + let url: URL; + try { + url = new URL(value); + } catch { + throw new Error('Bitbucket refused unsafe workspace pagination URL'); + } + + if ( + (value !== BITBUCKET_WORKSPACES_URL && !value.startsWith(`${BITBUCKET_WORKSPACES_URL}?`)) || + url.protocol !== 'https:' || + url.origin !== 'https://api.bitbucket.org' || + url.hostname !== 'api.bitbucket.org' || + url.pathname !== '/2.0/user/workspaces' || + url.username !== '' || + url.password !== '' || + url.port !== '' || + value.includes('#') + ) { + throw new Error('Bitbucket refused unsafe workspace pagination URL'); + } + + return value; +} + +export function buildBitbucketOAuthUrl(state: string): string { + if (!BITBUCKET_CLIENT_ID) { + throw new Error('Bitbucket OAuth client ID is not configured'); + } + + const url = new URL(BITBUCKET_AUTHORIZE_URL); + url.searchParams.set('client_id', BITBUCKET_CLIENT_ID); + url.searchParams.set('response_type', 'code'); + url.searchParams.set('scope', BITBUCKET_OAUTH_SCOPES.join(' ')); + url.searchParams.set('state', state); + return url.toString(); +} + +export async function exchangeBitbucketOAuthCode(code: string): Promise { + if (!BITBUCKET_CLIENT_ID || !BITBUCKET_CLIENT_SECRET) { + throw new Error('Bitbucket OAuth credentials are not configured'); + } + + const body = new URLSearchParams({ + grant_type: 'authorization_code', + code, + }); + const basicAuth = Buffer.from(`${BITBUCKET_CLIENT_ID}:${BITBUCKET_CLIENT_SECRET}`).toString( + 'base64' + ); + const response = await fetch(BITBUCKET_TOKEN_URL, { + method: 'POST', + redirect: 'manual', + headers: { + Accept: 'application/json', + Authorization: `Basic ${basicAuth}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: body.toString(), + }); + + if (!response.ok) { + throw new Error(`Bitbucket OAuth token exchange failed (${response.status})`); + } + + const invalidCredentialsMessage = 'Bitbucket OAuth token exchange returned invalid credentials'; + const responseBody = await readBitbucketJson(response, invalidCredentialsMessage); + const parsedTokens = BitbucketOAuthTokenPayloadSchema.safeParse(responseBody); + if (!parsedTokens.success) { + if (process.env.NODE_ENV === 'development') { + console.error('Bitbucket OAuth token response failed validation', { + shape: describeBitbucketTokenResponseShape(responseBody), + issues: parsedTokens.error.issues.map(issue => ({ + code: issue.code, + path: issue.path, + message: issue.message, + })), + }); + } + throw new Error(`${invalidCredentialsMessage}: token_response_schema`); + } + + return { + accessToken: parsedTokens.data.access_token, + refreshToken: parsedTokens.data.refresh_token, + tokenType: 'bearer', + expiresIn: parsedTokens.data.expires_in, + scopes: normalizeBitbucketOAuthScopes(parsedTokens.data.scope), + }; +} + +export async function fetchBitbucketUser(accessToken: string): Promise { + const response = await fetch(BITBUCKET_CURRENT_USER_URL, { + redirect: 'manual', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }); + if (!response.ok) { + throw new Error(`Bitbucket current-user request failed (${response.status})`); + } + + const invalidIdentityMessage = 'Bitbucket current-user request returned an invalid identity'; + const responseBody = await readBitbucketJson(response, invalidIdentityMessage); + const parsedUser = BitbucketUserPayloadSchema.safeParse(responseBody); + if (!parsedUser.success) { + throw new Error(invalidIdentityMessage); + } + return { + uuid: parsedUser.data.uuid, + nickname: parsedUser.data.nickname, + displayName: parsedUser.data.display_name, + }; +} + +export async function fetchBitbucketWorkspaces(accessToken: string): Promise { + const workspaces: BitbucketWorkspace[] = []; + const visitedUrls = new Set(); + let nextUrl: string | undefined = BITBUCKET_WORKSPACES_URL; + + while (nextUrl) { + if (visitedUrls.size >= MAX_BITBUCKET_WORKSPACE_PAGES) { + throw new Error('Bitbucket workspace pagination exceeded page limit'); + } + + const pageUrl = validateBitbucketWorkspacePageUrl(nextUrl); + const pageKey = new URL(pageUrl).toString(); + if (visitedUrls.has(pageKey)) { + throw new Error('Bitbucket workspace pagination cycle detected'); + } + visitedUrls.add(pageKey); + + const response = await fetch(pageUrl, { + redirect: 'manual', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }); + if (!response.ok) { + throw new Error(`Bitbucket workspace request failed (${response.status})`); + } + + const invalidResponseMessage = 'Bitbucket workspace request returned an invalid response'; + const responseBody = await readBitbucketJson(response, invalidResponseMessage); + const page = BitbucketWorkspacePageSchema.safeParse(responseBody); + if (!page.success) { + throw new Error(invalidResponseMessage); + } + if (workspaces.length + page.data.values.length > MAX_BITBUCKET_WORKSPACES) { + throw new Error('Bitbucket workspace pagination exceeded item limit'); + } + workspaces.push( + ...page.data.values.map(({ workspace }) => ({ + uuid: workspace.uuid, + slug: workspace.slug, + name: workspace.name ?? workspace.slug, + })) + ); + if (page.data.next && workspaces.length >= MAX_BITBUCKET_WORKSPACES) { + throw new Error('Bitbucket workspace pagination exceeded item limit'); + } + nextUrl = page.data.next; + } + + return workspaces; +} diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/credentials.test.ts b/apps/web/src/lib/integrations/platforms/bitbucket/credentials.test.ts new file mode 100644 index 0000000000..ccd5ec58d4 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/credentials.test.ts @@ -0,0 +1,394 @@ +/* eslint-disable drizzle/enforce-delete-with-where */ +import { generateKeyPairSync } from 'node:crypto'; +import { decryptKeyedEnvelope } from '@kilocode/encryption'; +import { db } from '@/lib/drizzle'; +import type { Owner } from '@/lib/integrations/core/types'; +import { createTestOrganization } from '@/tests/helpers/organization.helper'; +import { insertTestUser } from '@/tests/helpers/user.helper'; +import { + kilocode_users, + organization_memberships, + organizations, + platform_integrations, + platform_oauth_credentials, +} from '@kilocode/db/schema'; +import { and, eq } from 'drizzle-orm'; +import { + BITBUCKET_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + BitbucketIntegrationConnectionConflictError, + buildBitbucketOAuthCredentialAad, + storeBitbucketIntegration, +} from './credentials'; + +const testKeyPair = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, +}); +const mockBitbucketCredentialEncryptionConfig = { + keyId: 'bitbucket-credential-key-v1', + publicKey: Buffer.from(testKeyPair.publicKey).toString('base64'), +}; + +jest.mock('@/lib/config.server', () => ({ + get BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID() { + return mockBitbucketCredentialEncryptionConfig.keyId; + }, + get BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY() { + return mockBitbucketCredentialEncryptionConfig.publicKey; + }, +})); + +function integrationInput( + owner: Owner, + authorizedByUserId: string, + suffix = 'new' +): Parameters[0] { + return { + owner, + authorizedByUserId, + bitbucketUser: { uuid: `{bitbucket-user-${suffix}}`, nickname: `bucket-${suffix}` }, + tokens: { + accessToken: `access-token-${suffix}`, + refreshToken: `refresh-token-${suffix}`, + tokenType: 'bearer', + expiresIn: 3600, + scopes: ['account', 'pullrequest', 'repository', 'repository:write', 'webhook'], + }, + availableWorkspaces: [ + { + uuid: `{workspace-${suffix}}`, + slug: `workspace-${suffix}`, + name: `Workspace ${suffix}`, + }, + ], + }; +} + +async function insertExistingBitbucketIntegration(kiloUserId: string) { + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: kiloUserId, + created_by_user_id: kiloUserId, + platform: 'bitbucket', + integration_type: 'oauth', + platform_installation_id: 'workspace-old', + platform_account_id: 'workspace-old', + platform_account_login: 'workspace-old', + scopes: ['account', 'pullrequest', 'repository', 'repository:write', 'webhook'], + repository_access: 'all', + integration_status: 'active', + metadata: { + state: 'active', + workspace: { uuid: 'workspace-old', slug: 'workspace-old', name: 'Workspace Old' }, + }, + }) + .returning(); + if (!integration) throw new Error('Expected existing Bitbucket integration'); + + const [credential] = await db + .insert(platform_oauth_credentials) + .values({ + platform_integration_id: integration.id, + platform: 'bitbucket', + authorized_by_user_id: kiloUserId, + provider_subject_id: 'bitbucket-user-old', + provider_subject_login: 'bucket-old', + access_token_encrypted: 'old-access-envelope', + access_token_expires_at: '2030-01-01T00:00:00.000Z', + refresh_token_encrypted: 'old-refresh-envelope', + }) + .returning(); + if (!credential) throw new Error('Expected existing Bitbucket credential'); + + return { integration, credential }; +} + +describe('Bitbucket OAuth credential storage', () => { + beforeEach(() => { + mockBitbucketCredentialEncryptionConfig.keyId = 'bitbucket-credential-key-v1'; + mockBitbucketCredentialEncryptionConfig.publicKey = Buffer.from(testKeyPair.publicKey).toString( + 'base64' + ); + }); + + afterEach(async () => { + await db.delete(platform_oauth_credentials); + await db.delete(platform_integrations); + await db.delete(organizations); + await db.delete(kilocode_users); + }); + + it('automatically activates an integration with one available workspace', async () => { + const user = await insertTestUser(); + const owner = { type: 'user', id: user.id } as const; + const accessToken = 'bitbucket-access-token-plaintext'; + const refreshToken = 'bitbucket-refresh-token-plaintext'; + + const result = await storeBitbucketIntegration({ + ...integrationInput(owner, user.id), + bitbucketUser: { uuid: '{BITBUCKET-USER-UUID}', nickname: 'octobucket' }, + tokens: { + accessToken, + refreshToken, + tokenType: 'bearer', + expiresIn: 3600, + scopes: ['account', 'pullrequest', 'repository', 'repository:write', 'webhook'], + }, + availableWorkspaces: [ + { uuid: '{WORKSPACE-UUID}', slug: 'kilo-workspace', name: 'Kilo Workspace' }, + ], + }); + + const [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, result.integrationId)); + const [credential] = await db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.platform_integration_id, result.integrationId)); + if (!integration || !credential || !credential.access_token_expires_at) { + throw new Error('Expected complete Bitbucket integration'); + } + + expect(result.status).toBe('connected'); + expect(integration).toEqual( + expect.objectContaining({ + owned_by_user_id: user.id, + owned_by_organization_id: null, + created_by_user_id: user.id, + platform: 'bitbucket', + platform_installation_id: 'workspace-uuid', + platform_account_id: 'workspace-uuid', + platform_account_login: 'kilo-workspace', + integration_status: 'active', + metadata: { + state: 'active', + workspace: { uuid: 'workspace-uuid', slug: 'kilo-workspace', name: 'Kilo Workspace' }, + }, + }) + ); + expect(credential).toEqual( + expect.objectContaining({ + platform: 'bitbucket', + authorized_by_user_id: user.id, + provider_subject_id: 'bitbucket-user-uuid', + provider_subject_login: 'octobucket', + credential_version: 1, + }) + ); + expect(new Date(credential.access_token_expires_at).toISOString()).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ + ); + expect(JSON.stringify({ integration, credential })).not.toContain(accessToken); + expect(JSON.stringify({ integration, credential })).not.toContain(refreshToken); + + const accessAad = buildBitbucketOAuthCredentialAad({ + credentialId: credential.id, + integrationId: integration.id, + owner, + authorizedByUserId: user.id, + kind: 'access', + }); + const privateKeys = { + active: { + keyId: mockBitbucketCredentialEncryptionConfig.keyId, + privateKeyPem: testKeyPair.privateKey, + }, + }; + expect( + decryptKeyedEnvelope( + credential.access_token_encrypted, + BITBUCKET_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + privateKeys, + accessAad + ) + ).toBe(accessToken); + expect(() => + decryptKeyedEnvelope( + credential.access_token_encrypted, + BITBUCKET_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + privateKeys, + buildBitbucketOAuthCredentialAad({ + credentialId: credential.id, + integrationId: integration.id, + owner: { type: 'org', id: crypto.randomUUID() }, + authorizedByUserId: user.id, + kind: 'access', + }) + ) + ).toThrow(); + }); + + it('keeps multiple available workspaces pending for explicit selection', async () => { + const user = await insertTestUser(); + const owner = { type: 'user', id: user.id } as const; + const input = integrationInput(owner, user.id); + input.availableWorkspaces.push({ + uuid: '{workspace-second}', + slug: 'workspace-second', + name: 'Workspace Second', + }); + + const result = await storeBitbucketIntegration(input); + + const [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, result.integrationId)); + + expect(result.status).toBe('workspace_selection_required'); + expect(integration).toEqual( + expect.objectContaining({ + platform_installation_id: null, + platform_account_id: null, + platform_account_login: null, + integration_status: 'pending', + metadata: { + state: 'workspace_selection_required', + availableWorkspaces: [ + { uuid: 'workspace-new', slug: 'workspace-new', name: 'Workspace new' }, + { + uuid: 'workspace-second', + slug: 'workspace-second', + name: 'Workspace Second', + }, + ], + }, + }) + ); + }); + + it.each([ + ['missing', ''], + ['invalid', Buffer.from('not-an-rsa-public-key').toString('base64')], + ['decrypt-capable', Buffer.from(testKeyPair.privateKey).toString('base64')], + ])('keeps the existing integration when encryption configuration is %s', async (_, publicKey) => { + const user = await insertTestUser(); + const existing = await insertExistingBitbucketIntegration(user.id); + mockBitbucketCredentialEncryptionConfig.publicKey = publicKey; + + await expect( + storeBitbucketIntegration(integrationInput({ type: 'user', id: user.id }, user.id)) + ).rejects.toThrow(); + + await expect( + db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.owned_by_user_id, user.id)) + ).resolves.toEqual([existing.integration]); + await expect( + db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.authorized_by_user_id, user.id)) + ).resolves.toEqual([existing.credential]); + }); + + it("does not replace the caller's owned integration without disconnecting first", async () => { + const user = await insertTestUser(); + const existing = await insertExistingBitbucketIntegration(user.id); + + await expect( + storeBitbucketIntegration(integrationInput({ type: 'user', id: user.id }, user.id)) + ).rejects.toThrow(BitbucketIntegrationConnectionConflictError); + + const integrations = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.owned_by_user_id, user.id)); + const credentials = await db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.authorized_by_user_id, user.id)); + + expect(integrations).toHaveLength(1); + expect(credentials).toHaveLength(1); + expect(integrations[0]?.id).toBe(existing.integration.id); + expect(credentials[0]?.id).toBe(existing.credential.id); + }); + + it('preserves an organization integration when callback authorization was revoked', async () => { + const user = await insertTestUser(); + const organization = await createTestOrganization('Revoked Callback Org', user.id, 0); + const owner = { type: 'org', id: organization.id } as const; + const existing = await storeBitbucketIntegration(integrationInput(owner, user.id, 'existing')); + + await db + .delete(organization_memberships) + .where( + and( + eq(organization_memberships.organization_id, organization.id), + eq(organization_memberships.kilo_user_id, user.id) + ) + ); + + await expect( + storeBitbucketIntegration(integrationInput(owner, user.id, 'replacement')) + ).rejects.toThrow('no longer authorized'); + await expect( + db + .select({ id: platform_integrations.id }) + .from(platform_integrations) + .where(eq(platform_integrations.owned_by_organization_id, organization.id)) + ).resolves.toEqual([{ id: existing.integrationId }]); + }); + + it('rechecks current platform-admin access inside the storage transaction', async () => { + const admin = await insertTestUser(); + const organizationOwner = await insertTestUser(); + const organization = await createTestOrganization( + 'Platform Admin Callback Org', + organizationOwner.id, + 0 + ); + const owner = { type: 'org', id: organization.id } as const; + await db.update(kilocode_users).set({ is_admin: true }).where(eq(kilocode_users.id, admin.id)); + + const existing = await storeBitbucketIntegration( + integrationInput(owner, admin.id, 'admin-existing') + ); + await db.update(kilocode_users).set({ is_admin: false }).where(eq(kilocode_users.id, admin.id)); + + await expect( + storeBitbucketIntegration(integrationInput(owner, admin.id, 'admin-replacement')) + ).rejects.toThrow('no longer authorized'); + await expect( + db + .select({ id: platform_integrations.id }) + .from(platform_integrations) + .where(eq(platform_integrations.owned_by_organization_id, organization.id)) + ).resolves.toEqual([{ id: existing.integrationId }]); + }); + + it('allows one Bitbucket identity to authorize personal and organization integrations', async () => { + const user = await insertTestUser(); + const firstOrganization = await createTestOrganization('First Org', user.id, 0); + const secondOrganization = await createTestOrganization('Second Org', user.id, 0); + const owners: Owner[] = [ + { type: 'user', id: user.id }, + { type: 'org', id: firstOrganization.id }, + { type: 'org', id: secondOrganization.id }, + ]; + + for (const owner of owners) { + await storeBitbucketIntegration(integrationInput(owner, user.id, 'shared-identity')); + } + + const credentials = await db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.provider_subject_id, 'bitbucket-user-shared-identity')); + const integrations = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.platform, 'bitbucket')); + + expect(credentials).toHaveLength(3); + expect(integrations).toHaveLength(3); + expect(new Set(credentials.map(credential => credential.platform_integration_id)).size).toBe(3); + }); +}); diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/credentials.ts b/apps/web/src/lib/integrations/platforms/bitbucket/credentials.ts new file mode 100644 index 0000000000..878f107be1 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/credentials.ts @@ -0,0 +1,227 @@ +import 'server-only'; + +import { createPublicKey, randomUUID } from 'node:crypto'; +import { + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID, + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY, +} from '@/lib/config.server'; +import { db } from '@/lib/drizzle'; +import { INTEGRATION_STATUS, PLATFORM } from '@/lib/integrations/core/constants'; +import type { Owner } from '@/lib/integrations/core/types'; +import { encryptKeyedEnvelope } from '@kilocode/encryption'; +import { + kilocode_users, + organization_memberships, + platform_integrations, + platform_oauth_credentials, + type NewPlatformOAuthCredential, +} from '@kilocode/db/schema'; +import { and, eq, inArray, isNull, sql } from 'drizzle-orm'; +import type { BitbucketOAuthTokens, BitbucketUser } from './adapter'; +import { BitbucketIntegrationMetadataSchema, type BitbucketWorkspace } from './metadata'; + +export const BITBUCKET_OAUTH_CREDENTIAL_ENVELOPE_SCHEME = + 'bitbucket-oauth-credential-rsa-aes-256-gcm'; + +export class BitbucketIntegrationAuthorizationError extends Error {} +export class BitbucketIntegrationConnectionConflictError extends Error { + constructor() { + super('Bitbucket is already connected for this owner'); + this.name = 'BitbucketIntegrationConnectionConflictError'; + } +} + +export function buildBitbucketOAuthCredentialAad(input: { + credentialId: string; + integrationId: string; + owner: Owner; + authorizedByUserId: string; + kind: 'access' | 'refresh'; +}): string { + return JSON.stringify({ + scheme: BITBUCKET_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + version: 1, + platform: PLATFORM.BITBUCKET, + credentialId: input.credentialId, + integrationId: input.integrationId, + owner: input.owner, + authorizedByUserId: input.authorizedByUserId, + kind: input.kind, + }); +} + +export type StoreBitbucketIntegrationInput = { + owner: Owner; + authorizedByUserId: string; + bitbucketUser: Pick; + tokens: BitbucketOAuthTokens; + availableWorkspaces: BitbucketWorkspace[]; +}; + +function normalizeBitbucketUuid(value: string): string { + const unbraced = value.startsWith('{') && value.endsWith('}') ? value.slice(1, -1) : value; + return unbraced.toLowerCase(); +} + +function requireCredentialEncryptionKey(): { keyId: string; publicKeyPem: Buffer } { + const keyId = BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID; + const encodedPublicKey = BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY; + if (!keyId || keyId.trim() !== keyId || !encodedPublicKey) { + throw new Error('Bitbucket OAuth credential encryption is not configured'); + } + + const publicKeyPem = Buffer.from(encodedPublicKey, 'base64'); + try { + if (publicKeyPem.toString('utf8').includes('PRIVATE KEY')) { + throw new Error('Private key material is not allowed'); + } + const publicKey = createPublicKey(publicKeyPem); + if (publicKey.asymmetricKeyType !== 'rsa') { + throw new Error('RSA public key is required'); + } + } catch { + throw new Error('Bitbucket OAuth credential encryption is not configured'); + } + return { keyId, publicKeyPem }; +} + +function ownerCondition(owner: Owner) { + return owner.type === 'user' + ? eq(platform_integrations.owned_by_user_id, owner.id) + : eq(platform_integrations.owned_by_organization_id, owner.id); +} + +export async function storeBitbucketIntegration(input: StoreBitbucketIntegrationInput): Promise<{ + status: 'connected' | 'workspace_selection_required'; + integrationId: string; +}> { + const integrationId = randomUUID(); + const credentialId = randomUUID(); + const providerSubjectId = normalizeBitbucketUuid(input.bitbucketUser.uuid); + const availableWorkspaces = input.availableWorkspaces.map(workspace => ({ + uuid: normalizeBitbucketUuid(workspace.uuid), + slug: workspace.slug, + name: workspace.name, + })); + const selectedWorkspace = availableWorkspaces.length === 1 ? availableWorkspaces[0] : undefined; + const metadata = BitbucketIntegrationMetadataSchema.parse( + selectedWorkspace + ? { state: 'active', workspace: selectedWorkspace } + : { state: 'workspace_selection_required', availableWorkspaces } + ); + const status = selectedWorkspace ? 'connected' : 'workspace_selection_required'; + const encryptionKey = requireCredentialEncryptionKey(); + const accessTokenExpiresAt = new Date(Date.now() + input.tokens.expiresIn * 1000).toISOString(); + const accessTokenEncrypted = encryptKeyedEnvelope( + input.tokens.accessToken, + BITBUCKET_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + encryptionKey, + buildBitbucketOAuthCredentialAad({ + credentialId, + integrationId, + owner: input.owner, + authorizedByUserId: input.authorizedByUserId, + kind: 'access', + }) + ); + const refreshTokenEncrypted = encryptKeyedEnvelope( + input.tokens.refreshToken, + BITBUCKET_OAUTH_CREDENTIAL_ENVELOPE_SCHEME, + encryptionKey, + buildBitbucketOAuthCredentialAad({ + credentialId, + integrationId, + owner: input.owner, + authorizedByUserId: input.authorizedByUserId, + kind: 'refresh', + }) + ); + const credentialValues: NewPlatformOAuthCredential = { + id: credentialId, + platform_integration_id: integrationId, + platform: PLATFORM.BITBUCKET, + authorized_by_user_id: input.authorizedByUserId, + provider_subject_id: providerSubjectId, + provider_subject_login: input.bitbucketUser.nickname, + access_token_encrypted: accessTokenEncrypted, + access_token_expires_at: accessTokenExpiresAt, + refresh_token_encrypted: refreshTokenEncrypted, + }; + + return db.transaction(async tx => { + await tx.execute( + sql`SELECT pg_advisory_xact_lock(hashtextextended(${`bitbucket-oauth-owner:${input.owner.type}:${input.owner.id}`}, 0))` + ); + + if (input.owner.type === 'org') { + const [authorizer] = await tx + .select({ isAdmin: kilocode_users.is_admin }) + .from(kilocode_users) + .where( + and( + eq(kilocode_users.id, input.authorizedByUserId), + isNull(kilocode_users.blocked_reason) + ) + ) + .for('update'); + if (!authorizer) { + throw new BitbucketIntegrationAuthorizationError( + 'Bitbucket integration authorizer is no longer authorized' + ); + } + + if (!authorizer.isAdmin) { + const [membership] = await tx + .select({ id: organization_memberships.id }) + .from(organization_memberships) + .where( + and( + eq(organization_memberships.organization_id, input.owner.id), + eq(organization_memberships.kilo_user_id, input.authorizedByUserId), + inArray(organization_memberships.role, ['owner', 'billing_manager']) + ) + ) + .for('update'); + if (!membership) { + throw new BitbucketIntegrationAuthorizationError( + 'Bitbucket integration authorizer is no longer authorized' + ); + } + } + } + + const [currentIntegration] = await tx + .select({ id: platform_integrations.id }) + .from(platform_integrations) + .where( + and(ownerCondition(input.owner), eq(platform_integrations.platform, PLATFORM.BITBUCKET)) + ) + .for('update'); + if (currentIntegration) { + throw new BitbucketIntegrationConnectionConflictError(); + } + + await tx.insert(platform_integrations).values({ + id: integrationId, + owned_by_user_id: input.owner.type === 'user' ? input.owner.id : null, + owned_by_organization_id: input.owner.type === 'org' ? input.owner.id : null, + created_by_user_id: input.authorizedByUserId, + platform: PLATFORM.BITBUCKET, + integration_type: 'oauth', + platform_installation_id: selectedWorkspace?.uuid ?? null, + platform_account_id: selectedWorkspace?.uuid ?? null, + platform_account_login: selectedWorkspace?.slug ?? null, + permissions: null, + scopes: [...input.tokens.scopes], + repository_access: 'all', + repositories: null, + integration_status: selectedWorkspace + ? INTEGRATION_STATUS.ACTIVE + : INTEGRATION_STATUS.PENDING, + metadata, + }); + await tx.insert(platform_oauth_credentials).values(credentialValues); + + return { status, integrationId }; + }); +} diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/metadata.test.ts b/apps/web/src/lib/integrations/platforms/bitbucket/metadata.test.ts new file mode 100644 index 0000000000..7e061b68d7 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/metadata.test.ts @@ -0,0 +1,133 @@ +import { + BitbucketIntegrationMetadataSchema, + BitbucketWorkspaceAccessTokenMetadataSchema, +} from './metadata'; + +describe('BitbucketWorkspaceAccessTokenMetadataSchema', () => { + it('retains only the workspace display name', () => { + expect( + BitbucketWorkspaceAccessTokenMetadataSchema.parse({ displayName: 'Kilo Workspace' }) + ).toEqual({ displayName: 'Kilo Workspace' }); + expect( + BitbucketWorkspaceAccessTokenMetadataSchema.safeParse({ + displayName: 'Kilo Workspace', + workspaceUuid: '{workspace-uuid}', + }).success + ).toBe(false); + expect( + BitbucketWorkspaceAccessTokenMetadataSchema.safeParse({ + displayName: 'Kilo Workspace', + accessToken: 'must-not-be-stored', + }).success + ).toBe(false); + }); +}); + +describe('BitbucketIntegrationMetadataSchema', () => { + it('accepts pending metadata with available workspaces', () => { + const metadata = { + state: 'workspace_selection_required', + availableWorkspaces: [ + { + uuid: '{workspace-uuid}', + slug: 'kilo-workspace', + name: 'Kilo Workspace', + }, + ], + }; + + expect(BitbucketIntegrationMetadataSchema.parse(metadata)).toEqual(metadata); + }); + + it('accepts active metadata with one selected workspace', () => { + const metadata = { + state: 'active', + workspace: { + uuid: '{workspace-uuid}', + slug: 'kilo-workspace', + name: 'Kilo Workspace', + }, + }; + + expect(BitbucketIntegrationMetadataSchema.parse(metadata)).toEqual(metadata); + }); + + it('rejects pending metadata without an available workspace', () => { + expect( + BitbucketIntegrationMetadataSchema.safeParse({ + state: 'workspace_selection_required', + availableWorkspaces: [], + }).success + ).toBe(false); + }); + + it('rejects pending metadata above the workspace item limit', () => { + expect( + BitbucketIntegrationMetadataSchema.safeParse({ + state: 'workspace_selection_required', + availableWorkspaces: Array.from({ length: 501 }, (_, index) => ({ + uuid: `{workspace-${index}}`, + slug: `workspace-${index}`, + name: `Workspace ${index}`, + })), + }).success + ).toBe(false); + }); + + it.each(['uuid', 'slug', 'name'])('rejects blank workspace %s values', field => { + expect( + BitbucketIntegrationMetadataSchema.safeParse({ + state: 'active', + workspace: { + uuid: '{workspace-uuid}', + slug: 'kilo-workspace', + name: 'Kilo Workspace', + [field]: ' ', + }, + }).success + ).toBe(false); + }); + + it.each([ + ['uuid', ' {workspace-uuid}'], + ['slug', 'kilo-workspace '], + ['name', ' Kilo Workspace'], + ])('rejects whitespace-padded workspace %s values', (field, value) => { + expect( + BitbucketIntegrationMetadataSchema.safeParse({ + state: 'active', + workspace: { + uuid: '{workspace-uuid}', + slug: 'kilo-workspace', + name: 'Kilo Workspace', + [field]: value, + }, + }).success + ).toBe(false); + }); + + it.each([ + { + state: 'workspace_selection_required', + availableWorkspaces: [ + { + uuid: '{workspace-uuid}', + slug: 'kilo-workspace', + name: 'Kilo Workspace', + }, + ], + access_token: 'must-not-be-stored', + }, + { + state: 'active', + workspace: { + uuid: '{workspace-uuid}', + slug: 'kilo-workspace', + name: 'Kilo Workspace', + links: { self: { href: 'https://api.bitbucket.org/2.0/workspaces/kilo-workspace' } }, + }, + }, + ])('rejects secret-shaped and provider payload fields instead of stripping them', metadata => { + expect(BitbucketIntegrationMetadataSchema.safeParse(metadata).success).toBe(false); + }); +}); diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/metadata.ts b/apps/web/src/lib/integrations/platforms/bitbucket/metadata.ts new file mode 100644 index 0000000000..498b22ced5 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/metadata.ts @@ -0,0 +1,44 @@ +import { z } from 'zod'; + +export const MAX_BITBUCKET_WORKSPACES = 500; + +const NonEmptyMetadataStringSchema = z + .string() + .min(1) + .refine(value => value.trim() === value); + +export const BitbucketWorkspaceSchema = z + .object({ + uuid: NonEmptyMetadataStringSchema, + slug: NonEmptyMetadataStringSchema, + name: NonEmptyMetadataStringSchema, + }) + .strict(); + +export const BitbucketWorkspaceAccessTokenMetadataSchema = z + .object({ + displayName: NonEmptyMetadataStringSchema, + }) + .strict(); + +const PendingBitbucketIntegrationMetadataSchema = z + .object({ + state: z.literal('workspace_selection_required'), + availableWorkspaces: z.array(BitbucketWorkspaceSchema).min(1).max(MAX_BITBUCKET_WORKSPACES), + }) + .strict(); + +const ActiveBitbucketIntegrationMetadataSchema = z + .object({ + state: z.literal('active'), + workspace: BitbucketWorkspaceSchema, + }) + .strict(); + +export const BitbucketIntegrationMetadataSchema = z.discriminatedUnion('state', [ + PendingBitbucketIntegrationMetadataSchema, + ActiveBitbucketIntegrationMetadataSchema, +]); + +export type BitbucketWorkspace = z.infer; +export type BitbucketIntegrationMetadata = z.infer; diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/oauth-integration.ts b/apps/web/src/lib/integrations/platforms/bitbucket/oauth-integration.ts new file mode 100644 index 0000000000..1f0d538fa1 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/oauth-integration.ts @@ -0,0 +1,278 @@ +import 'server-only'; + +import { TRPCError } from '@trpc/server'; +import { and, eq, isNull } from 'drizzle-orm'; +import { z } from 'zod'; +import { db } from '@/lib/drizzle'; +import { INTEGRATION_STATUS, PLATFORM } from '@/lib/integrations/core/constants'; +import type { Owner } from '@/lib/integrations/core/types'; +import { + scheduleBitbucketRepositoryCachePrime, + listBitbucketRepositories, +} from './repository-cache'; +import { BitbucketIntegrationMetadataSchema, type BitbucketWorkspace } from './metadata'; +import { BitbucketRepositorySchema } from './token-service-client'; +import { platform_integrations, platform_oauth_credentials } from '@kilocode/db/schema'; + +const CachedRepositorySchema = z + .object({ + id: z.uuid(), + name: z.string().min(1), + full_name: z.string().min(3), + private: z.boolean(), + default_branch: z.string().min(1).optional(), + }) + .strict(); + +function ownerCondition(owner: Owner) { + return owner.type === 'user' + ? and( + eq(platform_integrations.owned_by_user_id, owner.id), + isNull(platform_integrations.owned_by_organization_id) + ) + : eq(platform_integrations.owned_by_organization_id, owner.id); +} + +function oauthIntegrationCondition(owner: Owner, integrationId?: string) { + const conditions = [ + ownerCondition(owner), + eq(platform_integrations.platform, PLATFORM.BITBUCKET), + eq(platform_integrations.integration_type, 'oauth'), + ]; + if (integrationId) { + conditions.push(eq(platform_integrations.id, integrationId)); + } + return and(...conditions); +} + +function emptyRepositoryCache() { + return { + status: 'uninitialized' as const, + repositories: [], + syncedAt: null, + }; +} + +function readCachedRepositories( + value: unknown, + syncedAt: string | null, + workspace: BitbucketWorkspace +) { + if (value === null || syncedAt === null) return emptyRepositoryCache(); + const repositories = z.array(CachedRepositorySchema).safeParse(value); + if (!repositories.success) return emptyRepositoryCache(); + + return { + status: 'available' as const, + repositories: repositories.data.map(repository => ({ + id: repository.id, + workspaceUuid: workspace.uuid, + name: repository.name, + fullName: repository.full_name, + private: repository.private, + defaultBranch: repository.default_branch, + })), + syncedAt: new Date(syncedAt).toISOString(), + }; +} + +function projectWorkspace(workspace: BitbucketWorkspace) { + return { + uuid: workspace.uuid, + slug: workspace.slug, + displayName: workspace.name, + }; +} + +async function findBitbucketOAuthIntegration(owner: Owner) { + const [row] = await db + .select({ + integrationId: platform_integrations.id, + integrationStatus: platform_integrations.integration_status, + metadata: platform_integrations.metadata, + repositories: platform_integrations.repositories, + repositoriesSyncedAt: platform_integrations.repositories_synced_at, + nickname: platform_oauth_credentials.provider_subject_login, + revokedAt: platform_oauth_credentials.revoked_at, + credentialId: platform_oauth_credentials.id, + }) + .from(platform_integrations) + .leftJoin( + platform_oauth_credentials, + and( + eq(platform_oauth_credentials.platform_integration_id, platform_integrations.id), + eq(platform_oauth_credentials.platform, PLATFORM.BITBUCKET) + ) + ) + .where(oauthIntegrationCondition(owner)) + .limit(1); + return row ?? null; +} + +export async function getBitbucketOAuthIntegrationStatus(owner: Owner, canManage: boolean) { + const row = await findBitbucketOAuthIntegration(owner); + if (!row) return null; + + const base = { + method: 'oauth' as const, + integrationId: row.integrationId, + integrationStatus: row.integrationStatus, + invalidatedAt: null, + invalidationReason: null, + expiresAt: null, + lastValidatedAt: null, + repositoryCache: emptyRepositoryCache(), + canManage, + }; + + const metadata = BitbucketIntegrationMetadataSchema.safeParse(row.metadata); + const authorizingNickname = row.nickname ?? null; + if (!row.credentialId || !row.nickname || row.revokedAt || !metadata.success) { + return { + ...base, + status: 'reconnect_required' as const, + recoveryAction: 'disconnect_and_connect' as const, + workspace: null, + authorizingNickname, + }; + } + + if ( + row.integrationStatus === INTEGRATION_STATUS.PENDING && + metadata.data.state === 'workspace_selection_required' + ) { + return { + ...base, + status: 'workspace_selection_required' as const, + recoveryAction: null, + workspace: null, + authorizingNickname: canManage ? authorizingNickname : null, + availableWorkspaces: canManage ? metadata.data.availableWorkspaces : [], + }; + } + + if (row.integrationStatus === INTEGRATION_STATUS.ACTIVE && metadata.data.state === 'active') { + return { + ...base, + status: 'connected' as const, + recoveryAction: null, + workspace: projectWorkspace(metadata.data.workspace), + authorizingNickname, + repositoryCache: readCachedRepositories( + row.repositories, + row.repositoriesSyncedAt, + metadata.data.workspace + ), + }; + } + + return { + ...base, + status: 'reconnect_required' as const, + recoveryAction: 'disconnect_and_connect' as const, + workspace: null, + authorizingNickname, + }; +} + +export async function selectBitbucketOAuthWorkspace(input: { + owner: Owner; + kiloUserId: string; + workspaceUuid: string; + workspaceSlug: string; +}) { + const row = await findBitbucketOAuthIntegration(input.owner); + if (!row || row.integrationStatus !== INTEGRATION_STATUS.PENDING || row.revokedAt) { + throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'Reconnect Bitbucket first' }); + } + + const metadata = BitbucketIntegrationMetadataSchema.safeParse(row.metadata); + if (!metadata.success || metadata.data.state !== 'workspace_selection_required') { + throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'Reconnect Bitbucket first' }); + } + const selectedWorkspace = metadata.data.availableWorkspaces.find( + workspace => workspace.uuid === input.workspaceUuid && workspace.slug === input.workspaceSlug + ); + if (!selectedWorkspace) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Select an available workspace' }); + } + + const activeMetadata = BitbucketIntegrationMetadataSchema.parse({ + state: 'active', + workspace: selectedWorkspace, + }); + const [updated] = await db + .update(platform_integrations) + .set({ + platform_installation_id: selectedWorkspace.uuid, + platform_account_id: selectedWorkspace.uuid, + platform_account_login: selectedWorkspace.slug, + integration_status: INTEGRATION_STATUS.ACTIVE, + metadata: activeMetadata, + updated_at: new Date().toISOString(), + }) + .where( + and( + eq(platform_integrations.id, row.integrationId), + oauthIntegrationCondition(input.owner), + eq(platform_integrations.integration_status, INTEGRATION_STATUS.PENDING) + ) + ) + .returning({ id: platform_integrations.id }); + if (!updated) { + throw new TRPCError({ code: 'CONFLICT', message: 'Bitbucket connection changed' }); + } + scheduleBitbucketRepositoryCachePrime({ + owner: input.owner, + kiloUserId: input.kiloUserId, + integrationId: row.integrationId, + }); + return { success: true, workspace: selectedWorkspace }; +} + +export async function disconnectBitbucketOAuthIntegration(input: { + owner: Owner; + integrationId?: string; +}) { + const [deleted] = await db + .delete(platform_integrations) + .where(oauthIntegrationCondition(input.owner, input.integrationId)) + .returning({ id: platform_integrations.id }); + if (!deleted) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'The Bitbucket integration was not found' }); + } + return { success: true, integrationId: deleted.id }; +} + +export async function refreshBitbucketOAuthRepositories(input: { + owner: Owner; + kiloUserId: string; + expectedIntegrationId: string; +}) { + return listBitbucketRepositories({ + owner: input.owner, + kiloUserId: input.kiloUserId, + forceRefresh: true, + expectedIntegrationId: input.expectedIntegrationId, + }); +} + +export const BitbucketOrganizationRepositoryListResultSchema = z.discriminatedUnion('status', [ + z + .object({ + status: z.literal('available'), + repositories: z.array(BitbucketRepositorySchema), + syncedAt: z.iso.datetime(), + }) + .strict(), + z.object({ status: z.literal('not_connected') }).strict(), + z.object({ status: z.literal('workspace_selection_required') }).strict(), + z.object({ status: z.literal('reconnect_required') }).strict(), + z.object({ status: z.literal('insufficient_permissions') }).strict(), + z.object({ status: z.literal('temporarily_unavailable') }).strict(), + z.object({ status: z.literal('invalid_request') }).strict(), +]); + +export type BitbucketOrganizationRepositoryListResult = z.infer< + typeof BitbucketOrganizationRepositoryListResultSchema +>; diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/repository-cache.test.ts b/apps/web/src/lib/integrations/platforms/bitbucket/repository-cache.test.ts new file mode 100644 index 0000000000..e6963c8dbb --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/repository-cache.test.ts @@ -0,0 +1,391 @@ +/* eslint-disable drizzle/enforce-delete-with-where */ +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + jest, +} from '@jest/globals'; +import type { Organization, User } from '@kilocode/db/schema'; +import { + kilocode_users, + organizations, + platform_integrations, + platform_oauth_credentials, +} from '@kilocode/db/schema'; +import { db } from '@/lib/drizzle'; +import { createTestOrganization } from '@/tests/helpers/organization.helper'; +import { insertTestUser } from '@/tests/helpers/user.helper'; +import { eq } from 'drizzle-orm'; +import type { BitbucketRepositoryListResult } from './token-service-client'; +import type { + listBitbucketRepositories as ListBitbucketRepositories, + primeBitbucketRepositoryCache as PrimeBitbucketRepositoryCache, +} from './repository-cache'; + +const mockFetchBitbucketRepositoriesFromTokenService = + jest.fn< + (kiloUserId: string, organizationId?: string) => Promise + >(); + +jest.mock('./token-service-client', () => ({ + fetchBitbucketRepositoriesFromTokenService: mockFetchBitbucketRepositoriesFromTokenService, +})); + +let listBitbucketRepositories: typeof ListBitbucketRepositories; +let primeBitbucketRepositoryCache: typeof PrimeBitbucketRepositoryCache; + +const WORKSPACE = { + uuid: '123e4567-e89b-12d3-a456-426614174020', + slug: 'acme', + name: 'Acme', +}; +const CACHED_AT = '2026-06-23T08:00:00.000Z'; +const CACHED_REPOSITORY = { + id: '123e4567-e89b-12d3-a456-426614174021', + name: 'widgets', + full_name: 'acme/widgets', + private: true, + default_branch: 'main', +}; +const LIVE_REPOSITORY = { + id: CACHED_REPOSITORY.id, + workspaceUuid: WORKSPACE.uuid, + name: CACHED_REPOSITORY.name, + fullName: CACHED_REPOSITORY.full_name, + private: CACHED_REPOSITORY.private, + defaultBranch: CACHED_REPOSITORY.default_branch, +}; +const REFRESHED_REPOSITORY = { + id: '123e4567-e89b-12d3-a456-426614174022', + workspaceUuid: WORKSPACE.uuid, + name: 'gadgets', + fullName: 'acme/gadgets', + private: false, +}; + +function deferred() { + let resolvePromise: ((value: T) => void) | undefined; + const promise = new Promise(resolve => { + resolvePromise = resolve; + }); + return { + promise, + resolve(value: T) { + if (!resolvePromise) throw new Error('Deferred promise is not initialized'); + resolvePromise(value); + }, + }; +} + +async function insertActiveIntegration( + userId: string, + cache: { + repositories?: Array | null; + syncedAt?: string | null; + } = {}, + organizationId?: string +) { + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_user_id: organizationId ? null : userId, + owned_by_organization_id: organizationId ?? null, + created_by_user_id: userId, + platform: 'bitbucket', + integration_type: 'oauth', + platform_installation_id: WORKSPACE.uuid, + platform_account_id: WORKSPACE.uuid, + platform_account_login: WORKSPACE.slug, + scopes: ['account', 'email', 'pullrequest', 'repository', 'repository:write', 'webhook'], + repository_access: 'all', + repositories: cache.repositories === undefined ? [CACHED_REPOSITORY] : cache.repositories, + repositories_synced_at: cache.syncedAt === undefined ? CACHED_AT : cache.syncedAt, + integration_status: 'active', + metadata: { state: 'active', workspace: WORKSPACE }, + }) + .returning(); + if (!integration) throw new Error('Expected Bitbucket integration'); + await db.insert(platform_oauth_credentials).values({ + platform_integration_id: integration.id, + platform: 'bitbucket', + authorized_by_user_id: userId, + provider_subject_id: '123e4567-e89b-12d3-a456-426614174010', + provider_subject_login: 'bucket-user', + access_token_encrypted: 'access-envelope', + access_token_expires_at: '2030-01-01T00:00:00.000Z', + refresh_token_encrypted: 'refresh-envelope', + }); + return integration; +} + +describe('Bitbucket repository cache', () => { + let user: User; + let organization: Organization; + + beforeAll(async () => { + ({ listBitbucketRepositories, primeBitbucketRepositoryCache } = + await import('./repository-cache')); + user = await insertTestUser(); + organization = await createTestOrganization('Bitbucket Cache Org', user.id, 0); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(async () => { + await db.delete(platform_oauth_credentials); + await db.delete(platform_integrations); + }); + + afterAll(async () => { + await db.delete(organizations); + await db.delete(kilocode_users); + }); + + it('returns an initialized repository cache without calling Bitbucket', async () => { + await insertActiveIntegration(user.id); + + await expect( + listBitbucketRepositories({ + owner: { type: 'user', id: user.id }, + kiloUserId: user.id, + }) + ).resolves.toEqual({ + status: 'available', + repositories: [ + { + id: CACHED_REPOSITORY.id, + workspaceUuid: WORKSPACE.uuid, + name: CACHED_REPOSITORY.name, + fullName: CACHED_REPOSITORY.full_name, + private: true, + defaultBranch: CACHED_REPOSITORY.default_branch, + }, + ], + syncedAt: CACHED_AT, + }); + expect(mockFetchBitbucketRepositoriesFromTokenService).not.toHaveBeenCalled(); + }); + + it('treats an empty synchronized repository list as an initialized cache', async () => { + await insertActiveIntegration(user.id, { repositories: [] }); + + await expect( + listBitbucketRepositories({ + owner: { type: 'user', id: user.id }, + kiloUserId: user.id, + }) + ).resolves.toEqual({ + status: 'available', + repositories: [], + syncedAt: CACHED_AT, + }); + expect(mockFetchBitbucketRepositoriesFromTokenService).not.toHaveBeenCalled(); + }); + + it('fetches and persists repositories when the cache is uninitialized', async () => { + const integration = await insertActiveIntegration(user.id, { + repositories: null, + syncedAt: null, + }); + mockFetchBitbucketRepositoriesFromTokenService.mockResolvedValue({ + status: 'available', + repositories: [LIVE_REPOSITORY], + }); + + await expect( + listBitbucketRepositories({ + owner: { type: 'user', id: user.id }, + kiloUserId: user.id, + }) + ).resolves.toMatchObject({ + status: 'available', + repositories: [LIVE_REPOSITORY], + syncedAt: expect.any(String), + }); + expect(mockFetchBitbucketRepositoriesFromTokenService).toHaveBeenCalledWith(user.id, undefined); + + const [updated] = await db + .select({ + repositories: platform_integrations.repositories, + syncedAt: platform_integrations.repositories_synced_at, + }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(updated).toMatchObject({ + repositories: [CACHED_REPOSITORY], + syncedAt: expect.any(String), + }); + }); + + it('isolates organization cache misses and forwards the organization owner', async () => { + await insertActiveIntegration(user.id); + const integration = await insertActiveIntegration( + user.id, + { repositories: null, syncedAt: null }, + organization.id + ); + mockFetchBitbucketRepositoriesFromTokenService.mockResolvedValue({ + status: 'available', + repositories: [LIVE_REPOSITORY], + }); + + await expect( + listBitbucketRepositories({ + owner: { type: 'org', id: organization.id }, + kiloUserId: user.id, + }) + ).resolves.toMatchObject({ + status: 'available', + repositories: [LIVE_REPOSITORY], + }); + expect(mockFetchBitbucketRepositoriesFromTokenService).toHaveBeenCalledWith( + user.id, + organization.id + ); + + const [updated] = await db + .select({ repositories: platform_integrations.repositories }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(updated?.repositories).toEqual([CACHED_REPOSITORY]); + }); + + it('primes an uninitialized repository cache through the real cache boundary', async () => { + const integration = await insertActiveIntegration(user.id, { + repositories: null, + syncedAt: null, + }); + mockFetchBitbucketRepositoriesFromTokenService.mockResolvedValue({ + status: 'available', + repositories: [LIVE_REPOSITORY], + }); + + await primeBitbucketRepositoryCache({ + owner: { type: 'user', id: user.id }, + kiloUserId: user.id, + integrationId: integration.id, + }); + + const [updated] = await db + .select({ + repositories: platform_integrations.repositories, + syncedAt: platform_integrations.repositories_synced_at, + }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(updated?.repositories).toEqual([CACHED_REPOSITORY]); + expect(updated?.syncedAt).toBeTruthy(); + }); + + it('replaces an initialized cache when refresh is forced', async () => { + const integration = await insertActiveIntegration(user.id); + mockFetchBitbucketRepositoriesFromTokenService.mockResolvedValue({ + status: 'available', + repositories: [REFRESHED_REPOSITORY], + }); + + await expect( + listBitbucketRepositories({ + owner: { type: 'user', id: user.id }, + kiloUserId: user.id, + forceRefresh: true, + }) + ).resolves.toMatchObject({ + status: 'available', + repositories: [REFRESHED_REPOSITORY], + }); + + const [updated] = await db + .select({ repositories: platform_integrations.repositories }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(updated?.repositories).toEqual([ + { + id: REFRESHED_REPOSITORY.id, + name: REFRESHED_REPOSITORY.name, + full_name: REFRESHED_REPOSITORY.fullName, + private: REFRESHED_REPOSITORY.private, + }, + ]); + }); + + it('prevents a slower overlapping refresh from overwriting the winning cache', async () => { + const integration = await insertActiveIntegration(user.id); + const firstResponse = deferred(); + const secondResponse = deferred(); + const bothRequestsStarted = deferred(); + let requestCount = 0; + mockFetchBitbucketRepositoriesFromTokenService.mockImplementation(() => { + requestCount += 1; + if (requestCount === 2) bothRequestsStarted.resolve(); + return requestCount === 1 ? firstResponse.promise : secondResponse.promise; + }); + + const firstRefresh = listBitbucketRepositories({ + owner: { type: 'user', id: user.id }, + kiloUserId: user.id, + forceRefresh: true, + }); + const secondRefresh = listBitbucketRepositories({ + owner: { type: 'user', id: user.id }, + kiloUserId: user.id, + forceRefresh: true, + }); + await bothRequestsStarted.promise; + + secondResponse.resolve({ status: 'available', repositories: [REFRESHED_REPOSITORY] }); + await expect(secondRefresh).resolves.toMatchObject({ + status: 'available', + repositories: [REFRESHED_REPOSITORY], + }); + firstResponse.resolve({ status: 'available', repositories: [LIVE_REPOSITORY] }); + await expect(firstRefresh).resolves.toMatchObject({ + status: 'available', + repositories: [REFRESHED_REPOSITORY], + }); + + const [updated] = await db + .select({ repositories: platform_integrations.repositories }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(updated?.repositories).toEqual([ + { + id: REFRESHED_REPOSITORY.id, + name: REFRESHED_REPOSITORY.name, + full_name: REFRESHED_REPOSITORY.fullName, + private: REFRESHED_REPOSITORY.private, + }, + ]); + }); + + it('preserves an initialized cache when a forced refresh is temporarily unavailable', async () => { + const integration = await insertActiveIntegration(user.id); + mockFetchBitbucketRepositoriesFromTokenService.mockResolvedValue({ + status: 'temporarily_unavailable', + }); + + await expect( + listBitbucketRepositories({ + owner: { type: 'user', id: user.id }, + kiloUserId: user.id, + forceRefresh: true, + }) + ).resolves.toEqual({ status: 'temporarily_unavailable' }); + + const [unchanged] = await db + .select({ + repositories: platform_integrations.repositories, + syncedAt: platform_integrations.repositories_synced_at, + }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(unchanged?.repositories).toEqual([CACHED_REPOSITORY]); + expect(new Date(unchanged?.syncedAt ?? '').toISOString()).toBe(CACHED_AT); + }); +}); diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/repository-cache.ts b/apps/web/src/lib/integrations/platforms/bitbucket/repository-cache.ts new file mode 100644 index 0000000000..6cb9bc8755 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/repository-cache.ts @@ -0,0 +1,237 @@ +import 'server-only'; + +import { and, eq, isNull } from 'drizzle-orm'; +import { z } from 'zod'; +import { platform_integrations, platform_oauth_credentials } from '@kilocode/db/schema'; +import { captureException, captureMessage } from '@sentry/nextjs'; +import { after } from 'next/server'; +import { db } from '@/lib/drizzle'; +import { INTEGRATION_STATUS, PLATFORM } from '@/lib/integrations/core/constants'; +import type { Owner } from '@/lib/integrations/core/types'; +import { BitbucketIntegrationMetadataSchema, type BitbucketWorkspace } from './metadata'; +import { + BitbucketRepositorySchema, + fetchBitbucketRepositoriesFromTokenService, +} from './token-service-client'; + +const CachedBitbucketRepositorySchema = z + .object({ + id: z.string().min(1), + name: z.string().min(1), + full_name: z.string().min(3), + private: z.boolean(), + default_branch: z.string().min(1).optional(), + }) + .strict(); + +export const CachedBitbucketRepositoryListResultSchema = z.discriminatedUnion('status', [ + z + .object({ + status: z.literal('available'), + repositories: z.array(BitbucketRepositorySchema), + syncedAt: z.iso.datetime(), + }) + .strict(), + z.object({ status: z.literal('not_connected') }).strict(), + z.object({ status: z.literal('workspace_selection_required') }).strict(), + z.object({ status: z.literal('reconnect_required') }).strict(), + z.object({ status: z.literal('insufficient_permissions') }).strict(), + z.object({ status: z.literal('temporarily_unavailable') }).strict(), + z.object({ status: z.literal('invalid_request') }).strict(), +]); + +export type CachedBitbucketRepositoryListResult = z.infer< + typeof CachedBitbucketRepositoryListResultSchema +>; + +type ListBitbucketRepositoriesInput = { + owner: Owner; + kiloUserId: string; + forceRefresh?: boolean; + expectedIntegrationId?: string; +}; + +type PrimeBitbucketRepositoryCacheInput = { + owner: Owner; + kiloUserId: string; + integrationId: string; +}; + +function ownerCondition(owner: Owner) { + return owner.type === 'user' + ? and( + eq(platform_integrations.owned_by_user_id, owner.id), + isNull(platform_integrations.owned_by_organization_id) + ) + : eq(platform_integrations.owned_by_organization_id, owner.id); +} + +function readCachedRepositories( + value: unknown, + syncedAt: string | null, + workspace: BitbucketWorkspace +): Extract | null { + if (value === null || !syncedAt) return null; + const repositories = z.array(CachedBitbucketRepositorySchema).safeParse(value); + if (!repositories.success) return null; + return { + status: 'available', + repositories: repositories.data.map(repository => ({ + id: repository.id, + workspaceUuid: workspace.uuid, + name: repository.name, + fullName: repository.full_name, + private: repository.private, + defaultBranch: repository.default_branch, + })), + syncedAt: new Date(syncedAt).toISOString(), + }; +} + +export async function listBitbucketRepositories({ + owner, + kiloUserId, + forceRefresh = false, + expectedIntegrationId, +}: ListBitbucketRepositoriesInput): Promise { + const [row] = await db + .select({ + integrationId: platform_integrations.id, + integrationStatus: platform_integrations.integration_status, + installationId: platform_integrations.platform_installation_id, + accountId: platform_integrations.platform_account_id, + accountLogin: platform_integrations.platform_account_login, + metadata: platform_integrations.metadata, + repositories: platform_integrations.repositories, + repositoriesSyncedAt: platform_integrations.repositories_synced_at, + credentialId: platform_oauth_credentials.id, + revokedAt: platform_oauth_credentials.revoked_at, + }) + .from(platform_integrations) + .leftJoin( + platform_oauth_credentials, + and( + eq(platform_oauth_credentials.platform_integration_id, platform_integrations.id), + eq(platform_oauth_credentials.platform, PLATFORM.BITBUCKET) + ) + ) + .where(and(ownerCondition(owner), eq(platform_integrations.platform, PLATFORM.BITBUCKET))) + .limit(1); + + if (!row) return { status: 'not_connected' }; + if (expectedIntegrationId && row.integrationId !== expectedIntegrationId) { + return { status: 'temporarily_unavailable' }; + } + if (!row.credentialId || row.revokedAt) return { status: 'reconnect_required' }; + + const metadata = BitbucketIntegrationMetadataSchema.safeParse(row.metadata); + if (!metadata.success) return { status: 'reconnect_required' }; + if ( + row.integrationStatus === INTEGRATION_STATUS.PENDING && + metadata.data.state === 'workspace_selection_required' + ) { + return { status: 'workspace_selection_required' }; + } + if ( + row.integrationStatus !== INTEGRATION_STATUS.ACTIVE || + metadata.data.state !== 'active' || + row.installationId !== metadata.data.workspace.uuid || + row.accountId !== metadata.data.workspace.uuid || + row.accountLogin !== metadata.data.workspace.slug + ) { + return { status: 'reconnect_required' }; + } + + const cachedResult = readCachedRepositories( + row.repositories, + row.repositoriesSyncedAt, + metadata.data.workspace + ); + if (!forceRefresh && cachedResult) return cachedResult; + + const result = await fetchBitbucketRepositoriesFromTokenService( + kiloUserId, + owner.type === 'org' ? owner.id : undefined + ); + if (result.status !== 'available') return result; + + const repositories = result.repositories.map(repository => ({ + id: repository.id, + name: repository.name, + full_name: repository.fullName, + private: repository.private, + default_branch: repository.defaultBranch, + })); + const previousSyncedAtMs = row.repositoriesSyncedAt + ? new Date(row.repositoriesSyncedAt).getTime() + : 0; + const syncedAt = new Date( + Math.max(Date.now(), Number.isFinite(previousSyncedAtMs) ? previousSyncedAtMs + 1 : 0) + ).toISOString(); + const cacheVersionCondition = row.repositoriesSyncedAt + ? eq(platform_integrations.repositories_synced_at, row.repositoriesSyncedAt) + : isNull(platform_integrations.repositories_synced_at); + const [updated] = await db + .update(platform_integrations) + .set({ + repositories, + repositories_synced_at: syncedAt, + auth_invalid_at: null, + auth_invalid_reason: null, + updated_at: syncedAt, + }) + .where( + and( + eq(platform_integrations.id, row.integrationId), + ownerCondition(owner), + eq(platform_integrations.platform, PLATFORM.BITBUCKET), + eq(platform_integrations.integration_status, INTEGRATION_STATUS.ACTIVE), + cacheVersionCondition, + eq(platform_integrations.platform_installation_id, metadata.data.workspace.uuid), + eq(platform_integrations.platform_account_id, metadata.data.workspace.uuid), + eq(platform_integrations.platform_account_login, metadata.data.workspace.slug) + ) + ) + .returning({ id: platform_integrations.id }); + if (!updated) { + return listBitbucketRepositories({ owner, kiloUserId }); + } + + return { + ...result, + syncedAt, + }; +} + +export function scheduleBitbucketRepositoryCachePrime( + input: PrimeBitbucketRepositoryCacheInput +): void { + after(() => primeBitbucketRepositoryCache(input)); +} + +export async function primeBitbucketRepositoryCache({ + owner, + kiloUserId, + integrationId, +}: PrimeBitbucketRepositoryCacheInput): Promise { + try { + const result = await listBitbucketRepositories({ + owner, + kiloUserId, + forceRefresh: true, + expectedIntegrationId: integrationId, + }); + if (result.status !== 'available') { + captureMessage('Bitbucket repository cache prime failed', { + level: 'warning', + tags: { source: 'bitbucket_repository_cache', operation: 'prime' }, + extra: { integrationId, ownerType: owner.type, status: result.status }, + }); + } + } catch (error) { + captureException(error, { + tags: { source: 'bitbucket_repository_cache', operation: 'prime' }, + extra: { integrationId, ownerType: owner.type }, + }); + } +} diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/token-service-client.test.ts b/apps/web/src/lib/integrations/platforms/bitbucket/token-service-client.test.ts new file mode 100644 index 0000000000..b3391ef601 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/token-service-client.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from '@jest/globals'; +import { BitbucketRepositoryListResultSchema } from './token-service-client'; + +describe('BitbucketRepositoryListResultSchema', () => { + it.each(['insufficient_permissions', 'invalid_request'] as const)( + 'accepts the static token-service %s result', + status => { + expect(BitbucketRepositoryListResultSchema.parse({ status })).toEqual({ status }); + } + ); +}); diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/token-service-client.ts b/apps/web/src/lib/integrations/platforms/bitbucket/token-service-client.ts new file mode 100644 index 0000000000..09712c1186 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/token-service-client.ts @@ -0,0 +1,75 @@ +import 'server-only'; + +import { z } from 'zod'; +import { GIT_TOKEN_SERVICE_API_URL } from '@/lib/config.server'; +import { + BITBUCKET_REPOSITORY_LIST_AUDIENCE, + generateInternalServiceToken, + TOKEN_EXPIRY, +} from '@/lib/tokens'; + +export const BitbucketRepositorySchema = z + .object({ + id: z.uuid(), + workspaceUuid: z.uuid(), + name: z.string().min(1), + fullName: z.string().min(3), + private: z.boolean(), + defaultBranch: z.string().min(1).optional(), + }) + .strict(); + +export const BitbucketRepositoryListResultSchema = z.discriminatedUnion('status', [ + z + .object({ status: z.literal('available'), repositories: z.array(BitbucketRepositorySchema) }) + .strict(), + z.object({ status: z.literal('invalid_request') }).strict(), + z.object({ status: z.literal('not_connected') }).strict(), + z.object({ status: z.literal('reconnect_required') }).strict(), + z.object({ status: z.literal('insufficient_permissions') }).strict(), + z.object({ status: z.literal('temporarily_unavailable') }).strict(), +]); + +export type BitbucketRepository = z.infer; +export type BitbucketRepositoryListResult = z.infer; + +export async function fetchBitbucketRepositoriesFromTokenService( + kiloUserId: string, + organizationId?: string +): Promise { + if (!GIT_TOKEN_SERVICE_API_URL) return { status: 'temporarily_unavailable' }; + const serviceToken = generateInternalServiceToken(kiloUserId, { + expiresIn: TOKEN_EXPIRY.fiveMinutes, + audience: BITBUCKET_REPOSITORY_LIST_AUDIENCE, + organizationId, + }); + + let response: Response; + try { + response = await fetch(`${GIT_TOKEN_SERVICE_API_URL}/internal/bitbucket/repositories`, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${serviceToken}`, + }, + signal: AbortSignal.timeout(30_000), + }); + } catch { + return { status: 'temporarily_unavailable' }; + } + if (!response.ok) return { status: 'temporarily_unavailable' }; + + try { + const parsed = BitbucketRepositoryListResultSchema.safeParse(await response.json()); + return parsed.success ? parsed.data : { status: 'temporarily_unavailable' }; + } catch { + return { status: 'temporarily_unavailable' }; + } +} + +export function fetchBitbucketWorkspaceAccessTokenRepositoriesFromTokenService( + kiloUserId: string, + organizationId: string +): Promise { + return fetchBitbucketRepositoriesFromTokenService(kiloUserId, organizationId); +} diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-adapter.test.ts b/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-adapter.test.ts new file mode 100644 index 0000000000..4fb03dc4a5 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-adapter.test.ts @@ -0,0 +1,483 @@ +import { + validateBitbucketWorkspaceAccessToken, + type BitbucketWorkspaceAccessTokenRepository, +} from './workspace-access-token-adapter'; + +const ACCESS_TOKEN = 'ATCT-successful-workspace-token'; +const WORKSPACE_UUID = '11111111-1111-4111-8111-111111111111'; + +function authenticatedJson(body: unknown, init: ResponseInit = {}): Response { + const headers = new Headers(init.headers); + headers.set('Content-Type', 'application/json'); + headers.set('X-Credential-Type', 'workspace_access_token'); + headers.set('X-OAuth-Scopes', 'repository:write account pullrequest webhook'); + return Response.json(body, { ...init, headers }); +} + +function workspaceDiscoveryJson(overrides: Record = {}): Response { + return authenticatedJson({ + pagelen: 2, + values: [ + { + workspace: { + uuid: `{${WORKSPACE_UUID.toUpperCase()}}`, + slug: 'acme', + ...overrides, + }, + }, + ], + }); +} + +function workspaceDetailsJson(): Response { + return authenticatedJson({ + uuid: `{${WORKSPACE_UUID.toUpperCase()}}`, + slug: 'acme', + name: 'Acme Workspace', + links: { self: { href: 'must-not-be-projected' } }, + }); +} + +function repository( + uuid: string, + slug: string, + overrides: Record = {} +): Record { + return { + uuid: `{${uuid}}`, + name: slug === 'api' ? 'API' : 'Web', + slug, + full_name: `acme/${slug}`, + is_private: true, + workspace: { + uuid: `{${WORKSPACE_UUID}}`, + slug: 'acme', + }, + mainbranch: { name: 'main' }, + ...overrides, + }; +} + +function generatedRepository(index: number): Record { + const uuidSuffix = index.toString(16).padStart(12, '0'); + return repository(`00000000-0000-4000-8000-${uuidSuffix}`, `repo-${index}`); +} + +function expectedRepository( + id: string, + name: string, + fullName: string +): BitbucketWorkspaceAccessTokenRepository { + return { + id, + workspaceUuid: WORKSPACE_UUID, + name, + fullName, + private: true, + defaultBranch: 'main', + }; +} + +describe('Bitbucket Workspace Access Token adapter', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('validates the workspace-bound credential and returns the complete repository projection', async () => { + const secondPageUrl = 'https://api.bitbucket.org/2.0/repositories/acme?pagelen=50&page=2'; + const fetchMock = jest + .spyOn(global, 'fetch') + .mockResolvedValueOnce(workspaceDiscoveryJson()) + .mockResolvedValueOnce(workspaceDetailsJson()) + .mockResolvedValueOnce(authenticatedJson({ pagelen: 1, values: [] })) + .mockResolvedValueOnce( + authenticatedJson({ + pagelen: 50, + values: [repository('22222222-2222-4222-8222-222222222222', 'api')], + next: secondPageUrl, + }) + ) + .mockResolvedValueOnce( + authenticatedJson({ + pagelen: 50, + values: [repository('33333333-3333-4333-8333-333333333333', 'web')], + }) + ); + + await expect( + validateBitbucketWorkspaceAccessToken({ + accessToken: ACCESS_TOKEN, + }) + ).resolves.toEqual({ + workspace: { + uuid: WORKSPACE_UUID, + slug: 'acme', + displayName: 'Acme Workspace', + }, + providerCredentialType: 'workspace_access_token', + providerScopes: ['account', 'pullrequest', 'repository:write', 'webhook'], + repositories: [ + expectedRepository('22222222-2222-4222-8222-222222222222', 'API', 'acme/api'), + expectedRepository('33333333-3333-4333-8333-333333333333', 'Web', 'acme/web'), + ], + }); + + expect(fetchMock).toHaveBeenCalledTimes(5); + expect(fetchMock.mock.calls.map(([url]) => url)).toEqual([ + 'https://api.bitbucket.org/2.0/user/workspaces?pagelen=2', + 'https://api.bitbucket.org/2.0/workspaces/acme', + 'https://api.bitbucket.org/2.0/workspaces/acme/members?pagelen=1', + 'https://api.bitbucket.org/2.0/repositories/acme?pagelen=50', + secondPageUrl, + ]); + for (const [url, init] of fetchMock.mock.calls) { + expect(String(url)).not.toContain('role=contributor'); + expect(init).toEqual( + expect.objectContaining({ + redirect: 'manual', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${ACCESS_TOKEN}`, + }, + signal: expect.any(AbortSignal), + }) + ); + } + }); + + it('rejects missing credential-type evidence before requesting members or repositories', async () => { + const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValueOnce( + Response.json( + { + pagelen: 2, + values: [{ workspace: { uuid: `{${WORKSPACE_UUID}}`, slug: 'acme' } }], + }, + { + headers: { + 'X-OAuth-Scopes': 'account repository:write pullrequest webhook', + }, + } + ) + ); + + await expect( + validateBitbucketWorkspaceAccessToken({ + accessToken: ACCESS_TOKEN, + }) + ).rejects.toMatchObject({ code: 'credential_type_missing' }); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).not.toHaveBeenCalledWith( + 'https://api.bitbucket.org/2.0/workspaces/acme/members?pagelen=1', + expect.anything() + ); + expect(fetchMock).not.toHaveBeenCalledWith( + 'https://api.bitbucket.org/2.0/repositories/acme?pagelen=50', + expect.anything() + ); + }); + + it('rejects ambiguous workspace discovery before requesting workspace details', async () => { + const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValueOnce( + authenticatedJson({ + pagelen: 2, + values: [ + { workspace: { uuid: `{${WORKSPACE_UUID}}`, slug: 'acme' } }, + { + workspace: { + uuid: '{44444444-4444-4444-8444-444444444444}', + slug: 'other', + }, + }, + ], + }) + ); + + await expect( + validateBitbucketWorkspaceAccessToken({ + accessToken: ACCESS_TOKEN, + }) + ).rejects.toMatchObject({ code: 'workspace_discovery_failed' }); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it('rejects credentials that Bitbucket classifies as another token type', async () => { + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + Response.json( + { + pagelen: 2, + values: [{ workspace: { uuid: `{${WORKSPACE_UUID}}`, slug: 'acme' } }], + }, + { + headers: { + 'X-Credential-Type': 'repo_access_token', + 'X-OAuth-Scopes': 'account repository:write pullrequest webhook', + }, + } + ) + ); + + const validation = validateBitbucketWorkspaceAccessToken({ + accessToken: ACCESS_TOKEN, + }); + await expect(validation).rejects.toMatchObject({ + name: 'BitbucketWorkspaceAccessTokenError', + code: 'credential_type_invalid', + }); + await expect(validation).rejects.not.toThrow(ACCESS_TOKEN); + }); + + it.each([ + [undefined, 'scope_evidence_missing'], + ['account repository', 'insufficient_scopes'], + ['account repository:write', 'insufficient_scopes'], + ['account repository:write webhook', 'insufficient_scopes'], + ['repository:write', 'insufficient_scopes'], + ] as const)('rejects missing required provider scopes', async (scopeHeader, expectedCode) => { + const headers: Record = { + 'X-Credential-Type': 'workspace_access_token', + }; + if (scopeHeader) headers['X-OAuth-Scopes'] = scopeHeader; + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + Response.json( + { + pagelen: 2, + values: [{ workspace: { uuid: `{${WORKSPACE_UUID}}`, slug: 'acme' } }], + }, + { headers } + ) + ); + + await expect( + validateBitbucketWorkspaceAccessToken({ + accessToken: ACCESS_TOKEN, + }) + ).rejects.toMatchObject({ code: expectedCode }); + }); + + it('rejects a credential whose workspace UUID differs from the expected binding', async () => { + const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValueOnce(workspaceDiscoveryJson()); + + await expect( + validateBitbucketWorkspaceAccessToken({ + expectedWorkspaceUuid: '44444444-4444-4444-8444-444444444444', + accessToken: ACCESS_TOKEN, + }) + ).rejects.toMatchObject({ code: 'workspace_mismatch' }); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it('requires access to the submitted workspace members endpoint', async () => { + const fetchMock = jest + .spyOn(global, 'fetch') + .mockResolvedValueOnce(workspaceDiscoveryJson()) + .mockResolvedValueOnce(workspaceDetailsJson()) + .mockResolvedValueOnce(new Response(null, { status: 403 })); + + await expect( + validateBitbucketWorkspaceAccessToken({ + accessToken: ACCESS_TOKEN, + }) + ).rejects.toMatchObject({ code: 'permission_denied' }); + expect(fetchMock).toHaveBeenCalledTimes(3); + }); + + it('rejects non-ATCT credentials before making a provider request', async () => { + const fetchMock = jest.spyOn(global, 'fetch'); + + await expect( + validateBitbucketWorkspaceAccessToken({ + accessToken: 'oauth-secret-that-must-not-leak', + }) + ).rejects.toMatchObject({ code: 'invalid_token_format' }); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it.each([ + [ + () => + Promise.resolve( + new Response(null, { status: 302, headers: { Location: 'https://evil.example' } }) + ), + 'redirect_rejected', + ], + [() => Promise.reject(new DOMException('timed out', 'TimeoutError')), 'request_timeout'], + [ + () => + Promise.resolve( + new Response('{}', { + headers: { + 'Content-Type': 'application/json', + 'Content-Length': '1000001', + }, + }) + ), + 'response_too_large', + ], + ] as const)( + 'sanitizes bounded provider transport failures', + async (providerResult, expectedCode) => { + jest.spyOn(global, 'fetch').mockImplementationOnce(providerResult); + + const validation = validateBitbucketWorkspaceAccessToken({ + accessToken: ACCESS_TOKEN, + }); + await expect(validation).rejects.toMatchObject({ code: expectedCode }); + await expect(validation).rejects.not.toThrow(ACCESS_TOKEN); + } + ); + + it('classifies a timeout that aborts response body streaming after headers arrive', async () => { + const abortController = new AbortController(); + jest.spyOn(AbortSignal, 'timeout').mockReturnValue(abortController.signal); + const body = new ReadableStream({ + start(controller) { + abortController.signal.addEventListener('abort', () => { + controller.error(abortController.signal.reason); + }); + }, + }); + jest.spyOn(global, 'fetch').mockImplementationOnce(async () => { + queueMicrotask(() => { + abortController.abort(new DOMException('Provider request timed out', 'TimeoutError')); + }); + return new Response(body, { + headers: { + 'Content-Type': 'application/json', + 'X-Credential-Type': 'workspace_access_token', + 'X-OAuth-Scopes': 'repository:write account pullrequest webhook', + }, + }); + }); + + await expect( + validateBitbucketWorkspaceAccessToken({ + accessToken: ACCESS_TOKEN, + }) + ).rejects.toMatchObject({ code: 'request_timeout' }); + }); + + it.each(['Pagelen', 'PAGELEN'])( + 'rejects repository pagination with the case-variant %s query parameter', + async pageLengthName => { + const unsafeNext = `https://api.bitbucket.org/2.0/repositories/acme?${pageLengthName}=50&page=2`; + const fetchMock = jest + .spyOn(global, 'fetch') + .mockResolvedValueOnce(workspaceDiscoveryJson()) + .mockResolvedValueOnce(workspaceDetailsJson()) + .mockResolvedValueOnce(authenticatedJson({ pagelen: 1, values: [] })) + .mockResolvedValueOnce(authenticatedJson({ pagelen: 50, values: [], next: unsafeNext })); + + await expect( + validateBitbucketWorkspaceAccessToken({ + accessToken: ACCESS_TOKEN, + }) + ).rejects.toMatchObject({ code: 'invalid_pagination' }); + expect(fetchMock).toHaveBeenCalledTimes(4); + expect(fetchMock).not.toHaveBeenCalledWith(unsafeNext, expect.anything()); + } + ); + + it('rejects repository pagination that introduces role=contributor', async () => { + const unsafeNext = + 'https://api.bitbucket.org/2.0/repositories/acme?role=contributor&pagelen=50&page=2'; + const fetchMock = jest + .spyOn(global, 'fetch') + .mockResolvedValueOnce(workspaceDiscoveryJson()) + .mockResolvedValueOnce(workspaceDetailsJson()) + .mockResolvedValueOnce(authenticatedJson({ pagelen: 1, values: [] })) + .mockResolvedValueOnce(authenticatedJson({ pagelen: 50, values: [], next: unsafeNext })); + + await expect( + validateBitbucketWorkspaceAccessToken({ + accessToken: ACCESS_TOKEN, + }) + ).rejects.toMatchObject({ code: 'invalid_pagination' }); + expect(fetchMock).toHaveBeenCalledTimes(4); + expect(fetchMock).not.toHaveBeenCalledWith(unsafeNext, expect.anything()); + }); + + it('stops repository pagination at the page limit', async () => { + let repositoryPage = 0; + const fetchMock = jest.spyOn(global, 'fetch').mockImplementation(async url => { + const endpoint = String(url); + if (endpoint.endsWith('/user/workspaces?pagelen=2')) { + return workspaceDiscoveryJson(); + } + if (endpoint.endsWith('/workspaces/acme')) { + return workspaceDetailsJson(); + } + if (endpoint.includes('/members?')) { + return authenticatedJson({ pagelen: 1, values: [] }); + } + repositoryPage += 1; + return authenticatedJson({ + pagelen: 50, + values: [], + next: `https://api.bitbucket.org/2.0/repositories/acme?pagelen=50&page=${repositoryPage + 1}`, + }); + }); + + await expect( + validateBitbucketWorkspaceAccessToken({ + accessToken: ACCESS_TOKEN, + }) + ).rejects.toMatchObject({ code: 'page_limit_exceeded' }); + expect(repositoryPage).toBe(20); + expect(fetchMock).toHaveBeenCalledTimes(23); + }); + + it('stops repository pagination at the item limit', async () => { + let repositoryPage = 0; + const fetchMock = jest.spyOn(global, 'fetch').mockImplementation(async url => { + const endpoint = String(url); + if (endpoint.endsWith('/user/workspaces?pagelen=2')) { + return workspaceDiscoveryJson(); + } + if (endpoint.endsWith('/workspaces/acme')) { + return workspaceDetailsJson(); + } + if (endpoint.includes('/members?')) { + return authenticatedJson({ pagelen: 1, values: [] }); + } + const firstIndex = repositoryPage * 50 + 1; + repositoryPage += 1; + return authenticatedJson({ + pagelen: 50, + values: Array.from({ length: 50 }, (_, offset) => generatedRepository(firstIndex + offset)), + next: `https://api.bitbucket.org/2.0/repositories/acme?pagelen=50&page=${repositoryPage + 1}`, + }); + }); + + await expect( + validateBitbucketWorkspaceAccessToken({ + accessToken: ACCESS_TOKEN, + }) + ).rejects.toMatchObject({ code: 'item_limit_exceeded' }); + expect(repositoryPage).toBe(10); + expect(fetchMock).toHaveBeenCalledTimes(13); + }); + + it('rejects repository projections outside the exact workspace full name', async () => { + jest + .spyOn(global, 'fetch') + .mockResolvedValueOnce(workspaceDiscoveryJson()) + .mockResolvedValueOnce(workspaceDetailsJson()) + .mockResolvedValueOnce(authenticatedJson({ pagelen: 1, values: [] })) + .mockResolvedValueOnce( + authenticatedJson({ + pagelen: 50, + values: [ + repository('22222222-2222-4222-8222-222222222222', 'api', { + full_name: 'other/api', + }), + ], + }) + ); + + await expect( + validateBitbucketWorkspaceAccessToken({ + accessToken: ACCESS_TOKEN, + }) + ).rejects.toMatchObject({ code: 'invalid_response' }); + }); +}); diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-adapter.ts b/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-adapter.ts new file mode 100644 index 0000000000..f0baecb604 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-adapter.ts @@ -0,0 +1,562 @@ +import 'server-only'; + +import { + BITBUCKET_WORKSPACE_ACCESS_TOKEN_PROVIDER_CREDENTIAL_TYPE, + hasBitbucketAccessTokenFamilyPrefix, + hasRequiredBitbucketWorkspaceAccessTokenScopes, + normalizeBitbucketWorkspaceAccessTokenScopes, +} from '@kilocode/worker-utils/bitbucket-workspace-access-token'; +import { z } from 'zod'; + +const BITBUCKET_API_ORIGIN = 'https://api.bitbucket.org'; +const BITBUCKET_PROVIDER_TIMEOUT_MS = 10_000; +const BITBUCKET_WORKSPACE_DISCOVERY_PAGE_LENGTH = 2; +const BITBUCKET_REPOSITORY_PAGE_LENGTH = 50; +const BITBUCKET_MAX_REPOSITORY_PAGES = 20; +const BITBUCKET_MAX_REPOSITORY_ITEMS = 500; +const BITBUCKET_MAX_RESPONSE_BYTES = 1_000_000; +const BITBUCKET_MAX_ACCESS_TOKEN_LENGTH = 8_192; +const BITBUCKET_MAX_SCOPE_HEADER_LENGTH = 4_096; + +const ProviderStringSchema = z + .string() + .min(1) + .max(255) + .refine(value => value.trim() === value); + +const BitbucketWorkspacePayloadSchema = z.object({ + uuid: z.string(), + slug: z.string(), + name: ProviderStringSchema, +}); + +const BitbucketWorkspaceAccessPayloadSchema = z.object({ + workspace: z.object({ + uuid: z.string(), + slug: z.string(), + }), +}); + +const BitbucketWorkspaceDiscoveryPageSchema = z + .object({ + pagelen: z.number().int().positive().max(BITBUCKET_WORKSPACE_DISCOVERY_PAGE_LENGTH), + values: z + .array(BitbucketWorkspaceAccessPayloadSchema) + .max(BITBUCKET_WORKSPACE_DISCOVERY_PAGE_LENGTH), + next: z.string().min(1).optional(), + }) + .refine(page => page.values.length <= page.pagelen); + +const BitbucketMembersPageSchema = z.object({ + pagelen: z.number().int().positive().max(1), + values: z.array(z.unknown()).max(1), +}); + +const BitbucketRepositoryPayloadSchema = z.object({ + uuid: z.string(), + name: ProviderStringSchema, + slug: z.string(), + full_name: z.string(), + is_private: z.boolean(), + workspace: z.object({ + uuid: z.string(), + slug: z.string(), + }), + mainbranch: z.object({ name: ProviderStringSchema }).nullable().optional(), +}); + +const BitbucketRepositoryPageSchema = z + .object({ + pagelen: z.number().int().positive().max(BITBUCKET_REPOSITORY_PAGE_LENGTH), + values: z.array(BitbucketRepositoryPayloadSchema).max(BITBUCKET_REPOSITORY_PAGE_LENGTH), + next: z.string().min(1).optional(), + }) + .refine(page => page.values.length <= page.pagelen); + +type BitbucketRepositoryPayload = z.infer; + +export type BitbucketWorkspaceAccessTokenErrorCode = + | 'invalid_token_format' + | 'invalid_workspace_slug' + | 'authentication_rejected' + | 'permission_denied' + | 'rate_limited' + | 'provider_unavailable' + | 'request_failed' + | 'request_timeout' + | 'redirect_rejected' + | 'response_too_large' + | 'invalid_response' + | 'invalid_pagination' + | 'page_limit_exceeded' + | 'item_limit_exceeded' + | 'credential_type_missing' + | 'credential_type_invalid' + | 'scope_evidence_missing' + | 'insufficient_scopes' + | 'workspace_discovery_failed' + | 'workspace_mismatch'; + +const ERROR_MESSAGES: Record = { + invalid_token_format: 'The Bitbucket Workspace Access Token format is invalid', + invalid_workspace_slug: 'The Bitbucket workspace slug is invalid', + authentication_rejected: 'Bitbucket rejected the Workspace Access Token', + permission_denied: 'The Bitbucket Workspace Access Token cannot access the requested workspace', + rate_limited: 'Bitbucket temporarily rate limited credential validation', + provider_unavailable: 'Bitbucket is temporarily unavailable', + request_failed: 'Bitbucket credential validation failed', + request_timeout: 'Bitbucket credential validation timed out', + redirect_rejected: 'Bitbucket returned an unsafe redirect', + response_too_large: 'Bitbucket returned an oversized response', + invalid_response: 'Bitbucket returned an invalid response', + invalid_pagination: 'Bitbucket returned unsafe repository pagination', + page_limit_exceeded: 'Bitbucket repository pagination exceeded the page limit', + item_limit_exceeded: 'Bitbucket repository pagination exceeded the repository limit', + credential_type_missing: 'Bitbucket did not report the credential type', + credential_type_invalid: 'The credential is not a Bitbucket Workspace Access Token', + scope_evidence_missing: 'Bitbucket did not report credential scopes', + insufficient_scopes: 'The Bitbucket Workspace Access Token is missing required permissions', + workspace_discovery_failed: + 'Bitbucket did not report exactly one workspace for the Workspace Access Token', + workspace_mismatch: 'The Bitbucket Workspace Access Token does not match the requested workspace', +}; + +export class BitbucketWorkspaceAccessTokenError extends Error { + constructor(readonly code: BitbucketWorkspaceAccessTokenErrorCode) { + super(ERROR_MESSAGES[code]); + this.name = 'BitbucketWorkspaceAccessTokenError'; + } +} + +export type BitbucketWorkspaceAccessTokenRepository = { + id: string; + workspaceUuid: string; + name: string; + fullName: string; + private: boolean; + defaultBranch?: string; +}; + +export type BitbucketWorkspaceAccessTokenValidation = { + workspace: { + uuid: string; + slug: string; + displayName: string; + }; + providerCredentialType: 'workspace_access_token'; + providerScopes: string[]; + repositories: BitbucketWorkspaceAccessTokenRepository[]; +}; + +export type ValidateBitbucketWorkspaceAccessTokenInput = { + accessToken: string; + expectedWorkspaceUuid?: string; + fetch?: typeof fetch; +}; + +function hasNonVisibleAscii(value: string): boolean { + for (let index = 0; index < value.length; index += 1) { + const code = value.charCodeAt(index); + if (code < 0x21 || code > 0x7e) return true; + } + return false; +} + +function isValidBitbucketSlug(value: string): boolean { + return ( + value.length <= 255 && /^[a-z0-9][a-z0-9_.-]*$/.test(value) && value !== '.' && value !== '..' + ); +} + +function normalizeBitbucketUuid(value: string): string | null { + const unbraced = value.startsWith('{') && value.endsWith('}') ? value.slice(1, -1) : value; + const normalized = unbraced.toLowerCase(); + return z.uuid().safeParse(normalized).success ? normalized : null; +} + +function requireAccessTokenFormat(accessToken: string): void { + if ( + !hasBitbucketAccessTokenFamilyPrefix(accessToken) || + accessToken.length > BITBUCKET_MAX_ACCESS_TOKEN_LENGTH || + hasNonVisibleAscii(accessToken) + ) { + throw new BitbucketWorkspaceAccessTokenError('invalid_token_format'); + } +} + +function workspaceEndpoint(workspaceSlug: string): string { + return `${BITBUCKET_API_ORIGIN}/2.0/workspaces/${encodeURIComponent(workspaceSlug)}`; +} + +function workspaceDiscoveryEndpoint(): string { + return `${BITBUCKET_API_ORIGIN}/2.0/user/workspaces?pagelen=${BITBUCKET_WORKSPACE_DISCOVERY_PAGE_LENGTH}`; +} + +function membersEndpoint(workspaceSlug: string): string { + return `${workspaceEndpoint(workspaceSlug)}/members?pagelen=1`; +} + +function repositoriesEndpoint(workspaceSlug: string): string { + return `${BITBUCKET_API_ORIGIN}/2.0/repositories/${encodeURIComponent(workspaceSlug)}?pagelen=${BITBUCKET_REPOSITORY_PAGE_LENGTH}`; +} + +function isTimeoutError(error: unknown): boolean { + return ( + typeof error === 'object' && error !== null && 'name' in error && error.name === 'TimeoutError' + ); +} + +function mapResponseStatus(status: number): never { + if (status === 401) throw new BitbucketWorkspaceAccessTokenError('authentication_rejected'); + if (status === 403) throw new BitbucketWorkspaceAccessTokenError('permission_denied'); + if (status === 429) throw new BitbucketWorkspaceAccessTokenError('rate_limited'); + if (status >= 500) throw new BitbucketWorkspaceAccessTokenError('provider_unavailable'); + throw new BitbucketWorkspaceAccessTokenError('request_failed'); +} + +async function readBoundedJson(response: Response, signal: AbortSignal): Promise { + const contentType = response.headers.get('Content-Type')?.split(';', 1)[0].trim().toLowerCase(); + if (contentType !== 'application/json' || !response.body) { + throw new BitbucketWorkspaceAccessTokenError('invalid_response'); + } + + const contentLength = response.headers.get('Content-Length'); + if (contentLength) { + if (!/^[0-9]+$/.test(contentLength)) { + throw new BitbucketWorkspaceAccessTokenError('invalid_response'); + } + if (Number(contentLength) > BITBUCKET_MAX_RESPONSE_BYTES) { + throw new BitbucketWorkspaceAccessTokenError('response_too_large'); + } + } + + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let totalBytes = 0; + try { + while (true) { + const chunk = await reader.read(); + if (chunk.done) break; + const chunkValue: unknown = chunk.value; + if (!(chunkValue instanceof Uint8Array)) { + throw new BitbucketWorkspaceAccessTokenError('invalid_response'); + } + totalBytes += chunkValue.byteLength; + if (totalBytes > BITBUCKET_MAX_RESPONSE_BYTES) { + try { + await reader.cancel(); + } catch { + // The bounded read remains failed if cancellation also fails. + } + throw new BitbucketWorkspaceAccessTokenError('response_too_large'); + } + chunks.push(chunkValue); + } + } catch (error) { + if (error instanceof BitbucketWorkspaceAccessTokenError) throw error; + if (isTimeoutError(error) || (signal.aborted && isTimeoutError(signal.reason))) { + throw new BitbucketWorkspaceAccessTokenError('request_timeout'); + } + throw new BitbucketWorkspaceAccessTokenError('invalid_response'); + } finally { + reader.releaseLock(); + } + + const body = new Uint8Array(totalBytes); + let offset = 0; + for (const chunk of chunks) { + body.set(chunk, offset); + offset += chunk.byteLength; + } + + try { + return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(body)); + } catch { + throw new BitbucketWorkspaceAccessTokenError('invalid_response'); + } +} + +async function fetchBitbucketJson( + endpoint: string, + accessToken: string, + fetchImplementation: typeof fetch +): Promise<{ payload: unknown; headers: Headers }> { + const signal = AbortSignal.timeout(BITBUCKET_PROVIDER_TIMEOUT_MS); + let response: Response; + try { + response = await fetchImplementation(endpoint, { + redirect: 'manual', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + signal, + }); + } catch (error) { + if (isTimeoutError(error) || (signal.aborted && isTimeoutError(signal.reason))) { + throw new BitbucketWorkspaceAccessTokenError('request_timeout'); + } + throw new BitbucketWorkspaceAccessTokenError('request_failed'); + } + + if ( + (response.status >= 300 && response.status < 400) || + response.redirected || + (response.url !== '' && response.url !== endpoint) + ) { + throw new BitbucketWorkspaceAccessTokenError('redirect_rejected'); + } + if (response.status !== 200) mapResponseStatus(response.status); + + return { + payload: await readBoundedJson(response, signal), + headers: response.headers, + }; +} + +function readCredentialEvidence(headers: Headers): { + providerCredentialType: 'workspace_access_token'; + providerScopes: string[]; +} { + const credentialType = headers.get('X-Credential-Type'); + if (!credentialType) { + throw new BitbucketWorkspaceAccessTokenError('credential_type_missing'); + } + if (credentialType !== BITBUCKET_WORKSPACE_ACCESS_TOKEN_PROVIDER_CREDENTIAL_TYPE) { + throw new BitbucketWorkspaceAccessTokenError('credential_type_invalid'); + } + + const scopeHeader = headers.get('X-OAuth-Scopes'); + if (!scopeHeader || scopeHeader.length > BITBUCKET_MAX_SCOPE_HEADER_LENGTH) { + throw new BitbucketWorkspaceAccessTokenError('scope_evidence_missing'); + } + const providerScopes = normalizeBitbucketWorkspaceAccessTokenScopes(scopeHeader); + if (providerScopes.length === 0) { + throw new BitbucketWorkspaceAccessTokenError('scope_evidence_missing'); + } + if (!hasRequiredBitbucketWorkspaceAccessTokenScopes(providerScopes)) { + throw new BitbucketWorkspaceAccessTokenError('insufficient_scopes'); + } + + return { + providerCredentialType: BITBUCKET_WORKSPACE_ACCESS_TOKEN_PROVIDER_CREDENTIAL_TYPE, + providerScopes, + }; +} + +function normalizeDiscoveredWorkspace(payload: { uuid: string; slug: string }): { + uuid: string; + slug: string; +} { + const uuid = normalizeBitbucketUuid(payload.uuid); + if (!uuid || !isValidBitbucketSlug(payload.slug)) { + throw new BitbucketWorkspaceAccessTokenError('workspace_discovery_failed'); + } + return { uuid, slug: payload.slug }; +} + +async function discoverWorkspace(input: { accessToken: string; fetch: typeof fetch }): Promise<{ + workspace: { uuid: string; slug: string }; + evidence: { + providerCredentialType: 'workspace_access_token'; + providerScopes: string[]; + }; +}> { + const discoveryResult = await fetchBitbucketJson( + workspaceDiscoveryEndpoint(), + input.accessToken, + input.fetch + ); + const evidence = readCredentialEvidence(discoveryResult.headers); + const page = BitbucketWorkspaceDiscoveryPageSchema.safeParse(discoveryResult.payload); + if (!page.success || page.data.values.length !== 1 || page.data.next) { + throw new BitbucketWorkspaceAccessTokenError('workspace_discovery_failed'); + } + + return { + workspace: normalizeDiscoveredWorkspace(page.data.values[0].workspace), + evidence, + }; +} + +function normalizeRepository( + repository: BitbucketRepositoryPayload, + workspace: { uuid: string; slug: string } +): BitbucketWorkspaceAccessTokenRepository { + const id = normalizeBitbucketUuid(repository.uuid); + const repositoryWorkspaceUuid = normalizeBitbucketUuid(repository.workspace.uuid); + if ( + !id || + repositoryWorkspaceUuid !== workspace.uuid || + repository.workspace.slug !== workspace.slug || + !isValidBitbucketSlug(repository.slug) || + repository.full_name !== `${workspace.slug}/${repository.slug}` + ) { + throw new BitbucketWorkspaceAccessTokenError('invalid_response'); + } + + return { + id, + workspaceUuid: workspace.uuid, + name: repository.name, + fullName: repository.full_name, + private: repository.is_private, + ...(repository.mainbranch ? { defaultBranch: repository.mainbranch.name } : {}), + }; +} + +function validateRepositoryNextLink(value: string, workspaceSlug: string): string { + const expectedPath = `/2.0/repositories/${encodeURIComponent(workspaceSlug)}`; + const rawUrl = /^https:\/\/api\.bitbucket\.org([^?#]*)(\?[^#]*)?$/.exec(value); + if (!rawUrl || rawUrl[1] !== expectedPath || hasNonVisibleAscii(value)) { + throw new BitbucketWorkspaceAccessTokenError('invalid_pagination'); + } + + let parsed: URL; + try { + parsed = new URL(value); + decodeURIComponent(`${parsed.pathname}${parsed.search}`); + } catch { + throw new BitbucketWorkspaceAccessTokenError('invalid_pagination'); + } + + const queryParameterNames = [...parsed.searchParams.keys()]; + const hasRoleParameter = queryParameterNames.some(name => name.toLowerCase() === 'role'); + const hasCaseVariantPageLength = queryParameterNames.some( + name => name !== 'pagelen' && name.toLowerCase() === 'pagelen' + ); + const pageLengths = parsed.searchParams.getAll('pagelen'); + if ( + parsed.protocol !== 'https:' || + parsed.origin !== BITBUCKET_API_ORIGIN || + parsed.username !== '' || + parsed.password !== '' || + parsed.port !== '' || + parsed.pathname !== expectedPath || + parsed.href !== value || + hasRoleParameter || + hasCaseVariantPageLength || + pageLengths.length > 1 || + (pageLengths.length === 1 && + (!/^[1-9][0-9]*$/.test(pageLengths[0]) || + Number(pageLengths[0]) > BITBUCKET_REPOSITORY_PAGE_LENGTH)) + ) { + throw new BitbucketWorkspaceAccessTokenError('invalid_pagination'); + } + + return value; +} + +async function listWorkspaceRepositories(input: { + workspace: { uuid: string; slug: string }; + accessToken: string; + fetch: typeof fetch; +}): Promise { + const repositories: BitbucketWorkspaceAccessTokenRepository[] = []; + const repositoryIds = new Set(); + const repositoryFullNames = new Set(); + const visitedEndpoints = new Set(); + let endpoint: string | undefined = repositoriesEndpoint(input.workspace.slug); + + for (let pageNumber = 0; endpoint; pageNumber += 1) { + if (pageNumber >= BITBUCKET_MAX_REPOSITORY_PAGES) { + throw new BitbucketWorkspaceAccessTokenError('page_limit_exceeded'); + } + if (visitedEndpoints.has(endpoint)) { + throw new BitbucketWorkspaceAccessTokenError('invalid_pagination'); + } + visitedEndpoints.add(endpoint); + + const { payload } = await fetchBitbucketJson(endpoint, input.accessToken, input.fetch); + const page = BitbucketRepositoryPageSchema.safeParse(payload); + if (!page.success) throw new BitbucketWorkspaceAccessTokenError('invalid_response'); + + for (const repositoryPayload of page.data.values) { + const repository = normalizeRepository(repositoryPayload, input.workspace); + if (repositoryIds.has(repository.id) || repositoryFullNames.has(repository.fullName)) { + throw new BitbucketWorkspaceAccessTokenError('invalid_response'); + } + repositoryIds.add(repository.id); + repositoryFullNames.add(repository.fullName); + repositories.push(repository); + if (repositories.length > BITBUCKET_MAX_REPOSITORY_ITEMS) { + throw new BitbucketWorkspaceAccessTokenError('item_limit_exceeded'); + } + } + + if (!page.data.next) return repositories; + if (repositories.length >= BITBUCKET_MAX_REPOSITORY_ITEMS) { + throw new BitbucketWorkspaceAccessTokenError('item_limit_exceeded'); + } + endpoint = validateRepositoryNextLink(page.data.next, input.workspace.slug); + } + + return repositories; +} + +export async function validateBitbucketWorkspaceAccessToken( + input: ValidateBitbucketWorkspaceAccessTokenInput +): Promise { + requireAccessTokenFormat(input.accessToken); + const expectedWorkspaceUuid = input.expectedWorkspaceUuid + ? normalizeBitbucketUuid(input.expectedWorkspaceUuid) + : undefined; + if (input.expectedWorkspaceUuid && !expectedWorkspaceUuid) { + throw new BitbucketWorkspaceAccessTokenError('workspace_mismatch'); + } + const fetchImplementation = input.fetch ?? globalThis.fetch; + const discovered = await discoverWorkspace({ + accessToken: input.accessToken, + fetch: fetchImplementation, + }); + if (expectedWorkspaceUuid && discovered.workspace.uuid !== expectedWorkspaceUuid) { + throw new BitbucketWorkspaceAccessTokenError('workspace_mismatch'); + } + + const workspaceResult = await fetchBitbucketJson( + workspaceEndpoint(discovered.workspace.slug), + input.accessToken, + fetchImplementation + ); + const workspacePayload = BitbucketWorkspacePayloadSchema.safeParse(workspaceResult.payload); + if (!workspacePayload.success) { + throw new BitbucketWorkspaceAccessTokenError('invalid_response'); + } + + const workspaceUuid = normalizeBitbucketUuid(workspacePayload.data.uuid); + if ( + !workspaceUuid || + workspaceUuid !== discovered.workspace.uuid || + workspacePayload.data.slug !== discovered.workspace.slug || + !isValidBitbucketSlug(workspacePayload.data.slug) || + (expectedWorkspaceUuid && workspaceUuid !== expectedWorkspaceUuid) + ) { + throw new BitbucketWorkspaceAccessTokenError('workspace_mismatch'); + } + const workspace = { + uuid: workspaceUuid, + slug: workspacePayload.data.slug, + displayName: workspacePayload.data.name, + }; + + const membersResult = await fetchBitbucketJson( + membersEndpoint(workspace.slug), + input.accessToken, + fetchImplementation + ); + if (!BitbucketMembersPageSchema.safeParse(membersResult.payload).success) { + throw new BitbucketWorkspaceAccessTokenError('invalid_response'); + } + + const repositories = await listWorkspaceRepositories({ + workspace, + accessToken: input.accessToken, + fetch: fetchImplementation, + }); + + return { + workspace, + ...discovered.evidence, + repositories, + }; +} diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-credentials.test.ts b/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-credentials.test.ts new file mode 100644 index 0000000000..a251c0637c --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-credentials.test.ts @@ -0,0 +1,1134 @@ +/* eslint-disable drizzle/enforce-delete-with-where */ +import { generateKeyPairSync } from 'node:crypto'; +import { decryptKeyedEnvelope } from '@kilocode/encryption'; +import { + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + buildBitbucketWorkspaceAccessTokenAad, +} from '@kilocode/worker-utils/bitbucket-workspace-access-token'; +import { db } from '@/lib/drizzle'; +import { createTestOrganization } from '@/tests/helpers/organization.helper'; +import { insertTestUser } from '@/tests/helpers/user.helper'; +import { + kilocode_users, + organization_audit_logs, + organization_memberships, + organizations, + platform_access_token_credentials, + platform_integrations, +} from '@kilocode/db/schema'; +import { and, eq } from 'drizzle-orm'; +import { createAuditLog } from '@/lib/organizations/organization-audit-logs'; +import type * as OrganizationAuditLogsModule from '@/lib/organizations/organization-audit-logs'; + +import { + connectBitbucketWorkspaceAccessToken, + disconnectBitbucketWorkspaceAccessToken, + rotateBitbucketWorkspaceAccessToken, +} from './workspace-access-token-credentials'; +import { + getBitbucketWorkspaceAccessTokenStatus, + readCachedBitbucketWorkspaceAccessTokenRepositories, +} from './workspace-access-token-repository-cache'; + +jest.mock('@/lib/organizations/organization-audit-logs', () => { + const actual = jest.requireActual( + '@/lib/organizations/organization-audit-logs' + ); + return { ...actual, createAuditLog: jest.fn(actual.createAuditLog) }; +}); + +const actualCreateAuditLog = jest.requireActual( + '@/lib/organizations/organization-audit-logs' +).createAuditLog; +const mockCreateAuditLog = jest.mocked(createAuditLog); + +const testKeyPair = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, +}); +const mockBitbucketCredentialEncryptionConfig = { + keyId: 'bitbucket-workspace-token-key-v1', + publicKey: Buffer.from(testKeyPair.publicKey).toString('base64'), +}; + +jest.mock('@/lib/config.server', () => ({ + get BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID() { + return mockBitbucketCredentialEncryptionConfig.keyId; + }, + get BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY() { + return mockBitbucketCredentialEncryptionConfig.publicKey; + }, +})); + +const ACCESS_TOKEN = 'ATCT-connect-workspace-secret'; +const WORKSPACE_UUID = '11111111-1111-4111-8111-111111111111'; +const REPOSITORY_UUID = '22222222-2222-4222-8222-222222222222'; + +function authenticatedJson( + body: unknown, + scopeHeader = 'account repository repository:write pullrequest webhook' +): Response { + return Response.json(body, { + headers: { + 'X-Credential-Type': 'workspace_access_token', + 'X-OAuth-Scopes': scopeHeader, + }, + }); +} + +function credentialPrivateKeys() { + return { + active: { + keyId: mockBitbucketCredentialEncryptionConfig.keyId, + privateKeyPem: testKeyPair.privateKey, + }, + }; +} + +function mockSuccessfulProviderValidation( + options: { + displayName?: string; + repositoryUuid?: string; + repositoryName?: string; + repositorySlug?: string; + scopeHeader?: string; + emptyRepositories?: boolean; + beforeRepositoriesResponse?: () => Promise; + } = {} +): jest.SpiedFunction { + const repositoryUuid = options.repositoryUuid ?? REPOSITORY_UUID; + const repositoryName = options.repositoryName ?? 'API'; + const repositorySlug = options.repositorySlug ?? 'api'; + return jest + .spyOn(global, 'fetch') + .mockResolvedValueOnce( + authenticatedJson( + { + pagelen: 2, + values: [{ workspace: { uuid: `{${WORKSPACE_UUID}}`, slug: 'acme' } }], + }, + options.scopeHeader + ) + ) + .mockResolvedValueOnce( + authenticatedJson( + { + uuid: `{${WORKSPACE_UUID}}`, + slug: 'acme', + name: options.displayName ?? 'Acme Workspace', + }, + options.scopeHeader + ) + ) + .mockResolvedValueOnce(authenticatedJson({ pagelen: 1, values: [] })) + .mockImplementationOnce(async () => { + await options.beforeRepositoriesResponse?.(); + return authenticatedJson({ + pagelen: 50, + values: options.emptyRepositories + ? [] + : [ + { + uuid: `{${repositoryUuid}}`, + name: repositoryName, + slug: repositorySlug, + full_name: `acme/${repositorySlug}`, + is_private: true, + workspace: { uuid: `{${WORKSPACE_UUID}}`, slug: 'acme' }, + mainbranch: { name: 'main' }, + }, + ], + }); + }); +} + +describe('Bitbucket Workspace Access Token credentials', () => { + beforeEach(() => { + mockCreateAuditLog.mockImplementation(actualCreateAuditLog); + mockBitbucketCredentialEncryptionConfig.keyId = 'bitbucket-workspace-token-key-v1'; + mockBitbucketCredentialEncryptionConfig.publicKey = Buffer.from(testKeyPair.publicKey).toString( + 'base64' + ); + }); + + afterEach(async () => { + jest.restoreAllMocks(); + await db.delete(organization_audit_logs); + await db.delete(platform_access_token_credentials); + await db.delete(platform_integrations); + await db.delete(organizations); + await db.delete(kilocode_users); + }); + + it('atomically connects an organization workspace with encrypted credentials, initialized cache, and audit', async () => { + const actor = await insertTestUser(); + const organization = await createTestOrganization('Acme Organization', actor.id, 0); + mockSuccessfulProviderValidation(); + + const result = await connectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + accessToken: ACCESS_TOKEN, + }); + + const [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, result.integrationId)); + const [credential] = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.platform_integration_id, result.integrationId)); + const [audit] = await db + .select() + .from(organization_audit_logs) + .where(eq(organization_audit_logs.organization_id, organization.id)); + if (!integration || !credential || !audit) { + throw new Error('Expected the complete Bitbucket organization integration'); + } + + expect(result).toEqual({ + integrationId: integration.id, + workspace: { + uuid: WORKSPACE_UUID, + slug: 'acme', + displayName: 'Acme Workspace', + }, + credentialVersion: 1, + repositoryCount: 1, + validatedAt: expect.any(String), + }); + expect(integration).toEqual( + expect.objectContaining({ + owned_by_organization_id: organization.id, + owned_by_user_id: null, + created_by_user_id: actor.id, + platform: 'bitbucket', + integration_type: 'workspace_access_token', + platform_account_id: WORKSPACE_UUID, + platform_account_login: 'acme', + platform_installation_id: null, + permissions: null, + scopes: null, + repository_access: 'all', + repositories: [ + { + id: REPOSITORY_UUID, + name: 'API', + full_name: 'acme/api', + private: true, + default_branch: 'main', + }, + ], + integration_status: 'active', + auth_invalid_at: null, + auth_invalid_reason: null, + metadata: { displayName: 'Acme Workspace' }, + }) + ); + expect(new Date(integration.repositories_synced_at ?? '').toISOString()).toBe( + result.validatedAt + ); + expect(credential).toEqual( + expect.objectContaining({ + platform_integration_id: integration.id, + owned_by_organization_id: organization.id, + platform: 'bitbucket', + integration_type: 'workspace_access_token', + expires_at: null, + provider_credential_type: 'workspace_access_token', + provider_scopes: ['account', 'pullrequest', 'repository', 'repository:write', 'webhook'], + credential_version: 1, + }) + ); + expect(new Date(credential.provider_verified_at).toISOString()).toBe(result.validatedAt); + expect(new Date(credential.last_validated_at).toISOString()).toBe(result.validatedAt); + + const aad = buildBitbucketWorkspaceAccessTokenAad({ + credentialId: credential.id, + integrationId: integration.id, + organizationId: organization.id, + credentialVersion: 1, + }); + expect( + decryptKeyedEnvelope( + credential.token_encrypted, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + { + active: { + keyId: mockBitbucketCredentialEncryptionConfig.keyId, + privateKeyPem: testKeyPair.privateKey, + }, + }, + aad + ) + ).toBe(ACCESS_TOKEN); + + expect(audit).toEqual( + expect.objectContaining({ + action: 'organization.settings.change', + actor_id: actor.id, + actor_email: actor.google_user_email, + actor_name: actor.google_user_name, + organization_id: organization.id, + }) + ); + expect(audit.message).toContain('connected'); + expect(audit.message).toContain(integration.id); + expect(audit.message).toContain(WORKSPACE_UUID); + expect(audit.message).toContain('acme'); + + const persistedAndReturned = JSON.stringify({ result, integration, credential, audit }); + expect(persistedAndReturned).not.toContain(ACCESS_TOKEN); + expect(integration.metadata).toEqual({ displayName: 'Acme Workspace' }); + }); + + it('connects an empty workspace with an initialized available cache', async () => { + const actor = await insertTestUser(); + const organization = await createTestOrganization('Empty Workspace Organization', actor.id, 0); + mockSuccessfulProviderValidation({ emptyRepositories: true }); + + const connected = await connectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + accessToken: ACCESS_TOKEN, + }); + + expect(connected.repositoryCount).toBe(0); + await expect(getBitbucketWorkspaceAccessTokenStatus(organization.id)).resolves.toMatchObject({ + status: 'connected', + repositoryCache: { + status: 'available', + repositories: [], + syncedAt: expect.any(String), + }, + }); + await expect( + readCachedBitbucketWorkspaceAccessTokenRepositories({ organizationId: organization.id }) + ).resolves.toMatchObject({ + status: 'available', + repositories: [], + syncedAt: expect.any(String), + }); + + const [integration] = await db + .select({ + repositories: platform_integrations.repositories, + syncedAt: platform_integrations.repositories_synced_at, + }) + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integrationId)); + expect(integration?.repositories).toEqual([]); + expect(integration?.syncedAt).not.toBeNull(); + }); + + it('uses one canonical organization UUID for connect, rotation, AAD, persistence, and disconnect', async () => { + const actor = await insertTestUser(); + const organization = await createTestOrganization('Canonical Organization', actor.id, 0); + const uppercaseOrganizationId = organization.id.toUpperCase(); + mockSuccessfulProviderValidation(); + + const connected = await connectBitbucketWorkspaceAccessToken({ + organizationId: uppercaseOrganizationId, + actorUserId: actor.id, + accessToken: ACCESS_TOKEN, + }); + const [connectedCredential] = await db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, connected.integrationId) + ); + if (!connectedCredential) throw new Error('Expected connected credential'); + + expect(connectedCredential.owned_by_organization_id).toBe(organization.id); + expect( + decryptKeyedEnvelope( + connectedCredential.token_encrypted, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + credentialPrivateKeys(), + buildBitbucketWorkspaceAccessTokenAad({ + credentialId: connectedCredential.id, + integrationId: connected.integrationId, + organizationId: organization.id, + credentialVersion: 1, + }) + ) + ).toBe(ACCESS_TOKEN); + + const replacementToken = 'ATCT-canonical-rotation-secret'; + mockSuccessfulProviderValidation(); + const rotated = await rotateBitbucketWorkspaceAccessToken({ + organizationId: uppercaseOrganizationId, + actorUserId: actor.id, + integrationId: connected.integrationId, + accessToken: replacementToken, + }); + const [rotatedCredential] = await db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, connected.integrationId) + ); + if (!rotatedCredential) throw new Error('Expected rotated credential'); + + expect( + decryptKeyedEnvelope( + rotatedCredential.token_encrypted, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + credentialPrivateKeys(), + buildBitbucketWorkspaceAccessTokenAad({ + credentialId: rotatedCredential.id, + integrationId: connected.integrationId, + organizationId: organization.id, + credentialVersion: rotated.credentialVersion, + }) + ) + ).toBe(replacementToken); + + await expect( + disconnectBitbucketWorkspaceAccessToken({ + organizationId: uppercaseOrganizationId, + actorUserId: actor.id, + integrationId: connected.integrationId, + }) + ).resolves.toEqual({ integrationId: connected.integrationId }); + await expect( + db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.owned_by_organization_id, organization.id)) + ).resolves.toEqual([]); + }); + + it('does not let a stale initial connect overwrite a newer Workspace Access Token integration', async () => { + const actor = await insertTestUser(); + const organization = await createTestOrganization( + 'Concurrent Connect Organization', + actor.id, + 0 + ); + const winnerToken = 'ATCT-newer-connect-secret'; + const winnerRepositoryUuid = '33333333-3333-4333-8333-333333333333'; + let winner: Awaited> | undefined; + mockSuccessfulProviderValidation({ + displayName: 'Stale Workspace', + beforeRepositoriesResponse: async () => { + mockSuccessfulProviderValidation({ + displayName: 'Current Workspace', + repositoryUuid: winnerRepositoryUuid, + repositoryName: 'Web', + repositorySlug: 'web', + }); + winner = await connectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + accessToken: winnerToken, + }); + }, + }); + + await expect( + connectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + accessToken: 'ATCT-stale-connect-secret', + }) + ).rejects.toMatchObject({ code: 'credential_conflict' }); + if (!winner) throw new Error('Expected the newer connection to win'); + + const [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.owned_by_organization_id, organization.id)); + const [credential] = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.owned_by_organization_id, organization.id)); + const audits = await db + .select() + .from(organization_audit_logs) + .where(eq(organization_audit_logs.organization_id, organization.id)); + if (!integration || !credential) throw new Error('Expected the winning connection'); + + expect(integration).toEqual( + expect.objectContaining({ + id: winner.integrationId, + metadata: { displayName: 'Current Workspace' }, + repositories: [ + { + id: winnerRepositoryUuid, + name: 'Web', + full_name: 'acme/web', + private: true, + default_branch: 'main', + }, + ], + }) + ); + expect(audits).toHaveLength(1); + expect(audits[0]?.message).toContain(winner.integrationId); + expect( + decryptKeyedEnvelope( + credential.token_encrypted, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + credentialPrivateKeys(), + buildBitbucketWorkspaceAccessTokenAad({ + credentialId: credential.id, + integrationId: winner.integrationId, + organizationId: organization.id, + credentialVersion: 1, + }) + ) + ).toBe(winnerToken); + }); + + it('requires disconnect before replacing an existing OAuth integration', async () => { + const actor = await insertTestUser(); + const organization = await createTestOrganization('Dormant OAuth Organization', actor.id, 0); + const [oauthIntegration] = await db + .insert(platform_integrations) + .values({ + owned_by_organization_id: organization.id, + owned_by_user_id: null, + created_by_user_id: actor.id, + platform: 'bitbucket', + integration_type: 'oauth', + platform_account_id: WORKSPACE_UUID, + platform_account_login: 'old-oauth-workspace', + repository_access: 'all', + integration_status: 'inactive', + metadata: {}, + }) + .returning(); + if (!oauthIntegration) throw new Error('Expected dormant OAuth integration'); + mockSuccessfulProviderValidation(); + + await expect( + connectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + accessToken: ACCESS_TOKEN, + }) + ).rejects.toMatchObject({ code: 'credential_conflict' }); + + await expect( + db + .select({ id: platform_integrations.id, type: platform_integrations.integration_type }) + .from(platform_integrations) + .where(eq(platform_integrations.owned_by_organization_id, organization.id)) + ).resolves.toEqual([{ id: oauthIntegration.id, type: 'oauth' }]); + }); + + it('rotates the fenced credential generation and atomically replaces cache and invalidation state', async () => { + const actor = await insertTestUser(); + const organization = await createTestOrganization('Rotation Organization', actor.id, 0); + mockSuccessfulProviderValidation(); + const connected = await connectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + accessToken: ACCESS_TOKEN, + }); + const [oldCredential] = await db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, connected.integrationId) + ); + if (!oldCredential) throw new Error('Expected initial credential'); + await db + .update(platform_integrations) + .set({ + auth_invalid_at: '2026-06-01T00:00:00.000Z', + auth_invalid_reason: 'provider_rejected', + }) + .where(eq(platform_integrations.id, connected.integrationId)); + + const replacementToken = 'ATCT-rotated-workspace-secret'; + const replacementRepositoryUuid = '33333333-3333-4333-8333-333333333333'; + mockSuccessfulProviderValidation({ + displayName: 'Acme Workspace Renamed', + repositoryUuid: replacementRepositoryUuid, + repositoryName: 'Web', + repositorySlug: 'web', + scopeHeader: 'pullrequest repository:write account webhook', + }); + const rotated = await rotateBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + integrationId: connected.integrationId, + accessToken: replacementToken, + }); + + const [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integrationId)); + const [credential] = await db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, connected.integrationId) + ); + const audits = await db + .select() + .from(organization_audit_logs) + .where(eq(organization_audit_logs.organization_id, organization.id)); + if (!integration || !credential) throw new Error('Expected rotated integration'); + + expect(rotated).toEqual({ + integrationId: connected.integrationId, + workspace: { + uuid: WORKSPACE_UUID, + slug: 'acme', + displayName: 'Acme Workspace Renamed', + }, + credentialVersion: 2, + repositoryCount: 1, + validatedAt: expect.any(String), + }); + expect(credential.id).toBe(oldCredential.id); + expect(credential.credential_version).toBe(2); + expect(credential.token_encrypted).not.toBe(oldCredential.token_encrypted); + expect(credential.provider_scopes).toEqual([ + 'account', + 'pullrequest', + 'repository:write', + 'webhook', + ]); + expect(credential.expires_at).toBeNull(); + expect(new Date(credential.provider_verified_at).toISOString()).toBe(rotated.validatedAt); + expect(new Date(credential.last_validated_at).toISOString()).toBe(rotated.validatedAt); + expect(integration).toEqual( + expect.objectContaining({ + auth_invalid_at: null, + auth_invalid_reason: null, + metadata: { displayName: 'Acme Workspace Renamed' }, + repositories: [ + { + id: replacementRepositoryUuid, + name: 'Web', + full_name: 'acme/web', + private: true, + default_branch: 'main', + }, + ], + }) + ); + expect(new Date(integration.repositories_synced_at ?? '').toISOString()).toBe( + rotated.validatedAt + ); + + const privateKeys = { + active: { + keyId: mockBitbucketCredentialEncryptionConfig.keyId, + privateKeyPem: testKeyPair.privateKey, + }, + }; + expect( + decryptKeyedEnvelope( + credential.token_encrypted, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + privateKeys, + buildBitbucketWorkspaceAccessTokenAad({ + credentialId: credential.id, + integrationId: integration.id, + organizationId: organization.id, + credentialVersion: 2, + }) + ) + ).toBe(replacementToken); + expect(() => + decryptKeyedEnvelope( + credential.token_encrypted, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + privateKeys, + buildBitbucketWorkspaceAccessTokenAad({ + credentialId: credential.id, + integrationId: integration.id, + organizationId: organization.id, + credentialVersion: 1, + }) + ) + ).toThrow(); + + expect(audits).toHaveLength(2); + expect(audits.map(audit => audit.message)).toEqual([ + expect.stringContaining('connected'), + expect.stringContaining('rotated'), + ]); + expect(JSON.stringify({ rotated, integration, credential, audits })).not.toContain( + replacementToken + ); + }); + + it('rolls back credential, cache, invalidation, and audit when rotation audit writing fails', async () => { + const actor = await insertTestUser(); + const organization = await createTestOrganization('Audit Rollback Organization', actor.id, 0); + mockSuccessfulProviderValidation(); + const connected = await connectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + accessToken: ACCESS_TOKEN, + }); + await db + .update(platform_integrations) + .set({ + auth_invalid_at: '2026-06-01T00:00:00.000Z', + auth_invalid_reason: 'provider_rejected', + }) + .where(eq(platform_integrations.id, connected.integrationId)); + const [integrationBefore] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integrationId)); + const [credentialBefore] = await db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, connected.integrationId) + ); + if (!integrationBefore || !credentialBefore) throw new Error('Expected connected integration'); + + mockSuccessfulProviderValidation({ + displayName: 'Must Roll Back', + repositoryUuid: '88888888-8888-4888-8888-888888888888', + repositoryName: 'Rollback', + repositorySlug: 'rollback', + }); + mockCreateAuditLog.mockRejectedValueOnce(new Error('audit write failed')); + + await expect( + rotateBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + integrationId: connected.integrationId, + accessToken: 'ATCT-audit-rollback-secret', + }) + ).rejects.toThrow('audit write failed'); + + const [integrationAfter] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integrationId)); + const [credentialAfter] = await db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, connected.integrationId) + ); + const audits = await db + .select() + .from(organization_audit_logs) + .where(eq(organization_audit_logs.organization_id, organization.id)); + + expect(credentialAfter).toEqual(credentialBefore); + expect(integrationAfter).toEqual(integrationBefore); + expect(audits).toHaveLength(1); + expect(audits[0]?.message).toContain('connected'); + }); + + it('preserves the winning rotation when the credential generation changes during validation', async () => { + const actor = await insertTestUser(); + const organization = await createTestOrganization( + 'Concurrent Rotation Organization', + actor.id, + 0 + ); + mockSuccessfulProviderValidation(); + const connected = await connectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + accessToken: ACCESS_TOKEN, + }); + const winnerToken = 'ATCT-winning-rotation-secret'; + const winnerRepositoryUuid = '44444444-4444-4444-8444-444444444444'; + let winner: Awaited> | undefined; + mockSuccessfulProviderValidation({ + displayName: 'Stale Rotation Workspace', + beforeRepositoriesResponse: async () => { + mockSuccessfulProviderValidation({ + displayName: 'Winning Rotation Workspace', + repositoryUuid: winnerRepositoryUuid, + repositoryName: 'Worker', + repositorySlug: 'worker', + }); + winner = await rotateBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + integrationId: connected.integrationId, + accessToken: winnerToken, + }); + }, + }); + + await expect( + rotateBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + integrationId: connected.integrationId, + accessToken: 'ATCT-stale-rotation-secret', + }) + ).rejects.toMatchObject({ code: 'credential_conflict' }); + if (!winner) throw new Error('Expected the newer rotation to win'); + + const [integration] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integrationId)); + const [credential] = await db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, connected.integrationId) + ); + const audits = await db + .select() + .from(organization_audit_logs) + .where(eq(organization_audit_logs.organization_id, organization.id)); + if (!integration || !credential) throw new Error('Expected the winning rotation'); + + expect(credential.credential_version).toBe(winner.credentialVersion); + expect(integration).toEqual( + expect.objectContaining({ + metadata: { displayName: 'Winning Rotation Workspace' }, + repositories: [ + { + id: winnerRepositoryUuid, + name: 'Worker', + full_name: 'acme/worker', + private: true, + default_branch: 'main', + }, + ], + }) + ); + expect(audits).toHaveLength(2); + expect(audits.map(audit => audit.message)).toEqual([ + expect.stringContaining('connected'), + expect.stringContaining('rotated'), + ]); + expect( + decryptKeyedEnvelope( + credential.token_encrypted, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + credentialPrivateKeys(), + buildBitbucketWorkspaceAccessTokenAad({ + credentialId: credential.id, + integrationId: connected.integrationId, + organizationId: organization.id, + credentialVersion: winner.credentialVersion, + }) + ) + ).toBe(winnerToken); + }); + + it('preserves the current credential and cache when replacement validation fails', async () => { + const actor = await insertTestUser(); + const organization = await createTestOrganization( + 'Validation Failure Organization', + actor.id, + 0 + ); + mockSuccessfulProviderValidation(); + const connected = await connectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + accessToken: ACCESS_TOKEN, + }); + const [integrationBefore] = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integrationId)); + const [credentialBefore] = await db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, connected.integrationId) + ); + + const rejectedToken = 'ATCT-rejected-rotation-secret'; + jest.spyOn(global, 'fetch').mockResolvedValueOnce( + Response.json( + { + pagelen: 2, + values: [{ workspace: { uuid: `{${WORKSPACE_UUID}}`, slug: 'acme' } }], + }, + { + headers: { + 'X-Credential-Type': 'oauth2', + 'X-OAuth-Scopes': 'account repository repository:write pullrequest webhook', + }, + } + ) + ); + + await expect( + rotateBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + integrationId: connected.integrationId, + accessToken: rejectedToken, + }) + ).rejects.toMatchObject({ code: 'credential_type_invalid' }); + + await expect( + db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integrationId)) + ).resolves.toEqual([integrationBefore]); + await expect( + db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, connected.integrationId) + ) + ).resolves.toEqual([credentialBefore]); + await expect( + db + .select() + .from(organization_audit_logs) + .where(eq(organization_audit_logs.organization_id, organization.id)) + ).resolves.toHaveLength(1); + }); + + it('preserves the current integration when replacement encryption fails', async () => { + const actor = await insertTestUser(); + const organization = await createTestOrganization( + 'Encryption Failure Organization', + actor.id, + 0 + ); + mockSuccessfulProviderValidation(); + const connected = await connectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + accessToken: ACCESS_TOKEN, + }); + const integrationsBefore = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.owned_by_organization_id, organization.id)); + const credentialsBefore = await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.owned_by_organization_id, organization.id)); + + mockSuccessfulProviderValidation({ displayName: 'Replacement Workspace' }); + mockBitbucketCredentialEncryptionConfig.publicKey = + Buffer.from('not-an-rsa-public-key').toString('base64'); + await expect( + connectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + accessToken: 'ATCT-unencrypted-replacement-secret', + }) + ).rejects.toMatchObject({ code: 'encryption_failed' }); + + await expect( + db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.owned_by_organization_id, organization.id)) + ).resolves.toEqual(integrationsBefore); + await expect( + db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.owned_by_organization_id, organization.id)) + ).resolves.toEqual(credentialsBefore); + expect(integrationsBefore).toEqual([expect.objectContaining({ id: connected.integrationId })]); + await expect( + db + .select() + .from(organization_audit_logs) + .where(eq(organization_audit_logs.organization_id, organization.id)) + ).resolves.toHaveLength(1); + }); + + it('atomically audits disconnect and cascades the organization credential', async () => { + const actor = await insertTestUser(); + const organization = await createTestOrganization('Disconnect Organization', actor.id, 0); + mockSuccessfulProviderValidation(); + const connected = await connectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + accessToken: ACCESS_TOKEN, + }); + + await expect( + disconnectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + integrationId: connected.integrationId, + }) + ).resolves.toEqual({ integrationId: connected.integrationId }); + + await expect( + db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, connected.integrationId)) + ).resolves.toEqual([]); + await expect( + db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, connected.integrationId) + ) + ).resolves.toEqual([]); + const audits = await db + .select() + .from(organization_audit_logs) + .where(eq(organization_audit_logs.organization_id, organization.id)); + expect(audits).toHaveLength(2); + expect(audits[1]).toEqual( + expect.objectContaining({ + action: 'organization.settings.change', + actor_id: actor.id, + message: expect.stringContaining('disconnected'), + }) + ); + expect(audits[1]?.message).toContain(connected.integrationId); + expect(audits[1]?.message).toContain(WORKSPACE_UUID); + expect(JSON.stringify(audits)).not.toContain(ACCESS_TOKEN); + }); + + it('revalidates the current organization role inside the locked mutation transaction', async () => { + const actor = await insertTestUser(); + const organization = await createTestOrganization( + 'Role Revalidation Organization', + actor.id, + 0 + ); + mockSuccessfulProviderValidation(); + const connected = await connectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + accessToken: ACCESS_TOKEN, + }); + const [credentialBefore] = await db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, connected.integrationId) + ); + + mockSuccessfulProviderValidation({ + beforeRepositoriesResponse: async () => { + await db + .delete(organization_memberships) + .where( + and( + eq(organization_memberships.organization_id, organization.id), + eq(organization_memberships.kilo_user_id, actor.id) + ) + ); + }, + }); + await expect( + rotateBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + integrationId: connected.integrationId, + accessToken: 'ATCT-role-revalidation-secret', + }) + ).rejects.toMatchObject({ code: 'unauthorized' }); + + await expect( + db + .select() + .from(platform_access_token_credentials) + .where( + eq(platform_access_token_credentials.platform_integration_id, connected.integrationId) + ) + ).resolves.toEqual([credentialBefore]); + await expect( + db + .select() + .from(organization_audit_logs) + .where(eq(organization_audit_logs.organization_id, organization.id)) + ).resolves.toHaveLength(1); + }); + + it.each(['billing_manager', 'platform_admin'] as const)( + 'allows a current unblocked %s to connect and records that actor', + async actorKind => { + const organizationOwner = await insertTestUser(); + const actor = await insertTestUser(); + const organization = await createTestOrganization( + `${actorKind} Organization`, + organizationOwner.id, + 0 + ); + if (actorKind === 'billing_manager') { + await db.insert(organization_memberships).values({ + organization_id: organization.id, + kilo_user_id: actor.id, + role: 'billing_manager', + }); + } else { + await db + .update(kilocode_users) + .set({ is_admin: true }) + .where(eq(kilocode_users.id, actor.id)); + } + mockSuccessfulProviderValidation(); + + const connected = await connectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + accessToken: ACCESS_TOKEN, + }); + + const [audit] = await db + .select() + .from(organization_audit_logs) + .where(eq(organization_audit_logs.organization_id, organization.id)); + expect(connected.workspace.uuid).toBe(WORKSPACE_UUID); + expect(audit).toEqual(expect.objectContaining({ actor_id: actor.id })); + } + ); + + it('rejects an invalid organization ID before authority or provider access', async () => { + const fetchMock = jest.spyOn(global, 'fetch'); + + await expect( + connectBitbucketWorkspaceAccessToken({ + organizationId: 'not-an-organization-uuid', + actorUserId: 'unresolved-actor', + accessToken: ACCESS_TOKEN, + }) + ).rejects.toMatchObject({ code: 'invalid_organization_id' }); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('rejects a blocked organization owner before sending the credential to Bitbucket', async () => { + const actor = await insertTestUser(); + const organization = await createTestOrganization('Blocked Owner Organization', actor.id, 0); + await db + .update(kilocode_users) + .set({ blocked_reason: 'policy_violation' }) + .where(eq(kilocode_users.id, actor.id)); + const fetchMock = jest.spyOn(global, 'fetch'); + + await expect( + connectBitbucketWorkspaceAccessToken({ + organizationId: organization.id, + actorUserId: actor.id, + accessToken: ACCESS_TOKEN, + }) + ).rejects.toMatchObject({ code: 'unauthorized' }); + expect(fetchMock).not.toHaveBeenCalled(); + await expect( + db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.owned_by_organization_id, organization.id)) + ).resolves.toEqual([]); + }); +}); diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-credentials.ts b/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-credentials.ts new file mode 100644 index 0000000000..e5c820d0f9 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-credentials.ts @@ -0,0 +1,544 @@ +import 'server-only'; + +import { createPublicKey, randomUUID } from 'node:crypto'; +import { + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID, + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY, +} from '@/lib/config.server'; +import { db, type DrizzleTransaction } from '@/lib/drizzle'; +import { INTEGRATION_STATUS } from '@/lib/integrations/core/constants'; +import { createAuditLog } from '@/lib/organizations/organization-audit-logs'; +import { encryptKeyedEnvelope } from '@kilocode/encryption'; +import { + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM, + buildBitbucketWorkspaceAccessTokenAad, +} from '@kilocode/worker-utils/bitbucket-workspace-access-token'; +import { platform_access_token_credentials, platform_integrations } from '@kilocode/db/schema'; +import { and, eq } from 'drizzle-orm'; +import { z } from 'zod'; +import { + validateBitbucketWorkspaceAccessToken, + type BitbucketWorkspaceAccessTokenRepository, + type BitbucketWorkspaceAccessTokenValidation, +} from './workspace-access-token-adapter'; +import { BitbucketWorkspaceAccessTokenMetadataSchema } from './metadata'; +import { + BitbucketWorkspaceAccessTokenOrganizationAuthorizationError, + lockBitbucketWorkspaceAccessTokenOrganization as acquireOrganizationCredentialLock, + requireBitbucketWorkspaceAccessTokenOrganizationManager, + type BitbucketWorkspaceAccessTokenAuthorizedActor as AuthorizedActor, +} from './workspace-access-token-organization-authorization'; + +const INITIAL_CREDENTIAL_VERSION = 1; + +export type BitbucketWorkspaceAccessTokenCredentialErrorCode = + | 'unauthorized' + | 'invalid_organization_id' + | 'organization_not_found' + | 'not_connected' + | 'credential_conflict' + | 'encryption_failed'; + +const ERROR_MESSAGES: Record = { + unauthorized: 'The current user cannot manage this organization integration', + invalid_organization_id: 'The organization ID is invalid', + organization_not_found: 'The organization was not found', + not_connected: 'The Bitbucket organization integration was not found', + credential_conflict: 'The Bitbucket credential changed during this operation', + encryption_failed: 'Bitbucket credential encryption is unavailable', +}; + +export class BitbucketWorkspaceAccessTokenCredentialError extends Error { + constructor(readonly code: BitbucketWorkspaceAccessTokenCredentialErrorCode) { + super(ERROR_MESSAGES[code]); + this.name = 'BitbucketWorkspaceAccessTokenCredentialError'; + } +} + +type CredentialEncryptionKey = { + keyId: string; + publicKeyPem: Buffer; +}; + +export type ConnectBitbucketWorkspaceAccessTokenInput = { + organizationId: string; + actorUserId: string; + accessToken: string; +}; + +export type RotateBitbucketWorkspaceAccessTokenInput = { + organizationId: string; + actorUserId: string; + integrationId: string; + accessToken: string; +}; + +export type DisconnectBitbucketWorkspaceAccessTokenInput = { + organizationId: string; + actorUserId: string; + integrationId: string; +}; + +export type BitbucketWorkspaceAccessTokenMutationResult = { + integrationId: string; + workspace: BitbucketWorkspaceAccessTokenValidation['workspace']; + credentialVersion: number; + repositoryCount: number; + validatedAt: string; +}; + +function canonicalizeOrganizationId(value: string): string { + const canonical = value.toLowerCase(); + if (value.trim() !== value || !z.uuid().safeParse(canonical).success) { + throw new BitbucketWorkspaceAccessTokenCredentialError('invalid_organization_id'); + } + return canonical; +} + +function requireCredentialEncryptionKey(): CredentialEncryptionKey { + const keyId = BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID; + const encodedPublicKey = BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY; + if (!keyId || keyId.trim() !== keyId || !encodedPublicKey) { + throw new BitbucketWorkspaceAccessTokenCredentialError('encryption_failed'); + } + + const publicKeyPem = Buffer.from(encodedPublicKey, 'base64'); + try { + if (publicKeyPem.toString('utf8').includes('PRIVATE KEY')) { + throw new Error('Private key material is not allowed'); + } + const publicKey = createPublicKey(publicKeyPem); + if (publicKey.asymmetricKeyType !== 'rsa') { + throw new Error('RSA public key is required'); + } + } catch { + throw new BitbucketWorkspaceAccessTokenCredentialError('encryption_failed'); + } + + return { keyId, publicKeyPem }; +} + +async function requireOrganizationManager( + tx: DrizzleTransaction, + organizationId: string, + actorUserId: string +): Promise { + try { + return await requireBitbucketWorkspaceAccessTokenOrganizationManager( + tx, + organizationId, + actorUserId + ); + } catch (error) { + if (error instanceof BitbucketWorkspaceAccessTokenOrganizationAuthorizationError) { + throw new BitbucketWorkspaceAccessTokenCredentialError(error.code); + } + throw error; + } +} + +async function preauthorizeOrganizationManager( + organizationId: string, + actorUserId: string +): Promise { + await db.transaction(tx => requireOrganizationManager(tx, organizationId, actorUserId)); +} + +function encryptAccessToken(input: { + accessToken: string; + credentialId: string; + integrationId: string; + organizationId: string; + credentialVersion: number; +}): string { + const encryptionKey = requireCredentialEncryptionKey(); + try { + return encryptKeyedEnvelope( + input.accessToken, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + encryptionKey, + buildBitbucketWorkspaceAccessTokenAad({ + credentialId: input.credentialId, + integrationId: input.integrationId, + organizationId: input.organizationId, + credentialVersion: input.credentialVersion, + }) + ); + } catch { + throw new BitbucketWorkspaceAccessTokenCredentialError('encryption_failed'); + } +} + +function cacheRepositories(repositories: BitbucketWorkspaceAccessTokenRepository[]) { + return repositories.map(repository => ({ + id: repository.id, + name: repository.name, + full_name: repository.fullName, + private: repository.private, + default_branch: repository.defaultBranch, + })); +} + +function auditMessage(input: { + action: 'connected' | 'rotated' | 'disconnected'; + integrationId: string; + workspaceUuid: string; + workspaceSlug: string; +}): string { + return `Bitbucket Workspace Access Token ${input.action} (integration ${input.integrationId}, workspace ${input.workspaceUuid}, slug ${input.workspaceSlug})`; +} + +async function writeAudit(input: { + tx: DrizzleTransaction; + actor: AuthorizedActor; + organizationId: string; + action: 'connected' | 'rotated' | 'disconnected'; + integrationId: string; + workspaceUuid: string; + workspaceSlug: string; +}): Promise { + await createAuditLog({ + tx: input.tx, + action: 'organization.settings.change', + actor_email: input.actor.email, + actor_id: input.actor.id, + actor_name: input.actor.name, + organization_id: input.organizationId, + message: auditMessage(input), + }); +} + +export async function disconnectBitbucketWorkspaceAccessToken( + input: DisconnectBitbucketWorkspaceAccessTokenInput +): Promise<{ integrationId: string }> { + const organizationId = canonicalizeOrganizationId(input.organizationId); + await preauthorizeOrganizationManager(organizationId, input.actorUserId); + + return db.transaction(async tx => { + await acquireOrganizationCredentialLock(tx, organizationId); + const actor = await requireOrganizationManager(tx, organizationId, input.actorUserId); + const [integration] = await tx + .select({ + id: platform_integrations.id, + workspaceUuid: platform_integrations.platform_account_id, + workspaceSlug: platform_integrations.platform_account_login, + }) + .from(platform_integrations) + .where( + and( + eq(platform_integrations.id, input.integrationId), + eq(platform_integrations.owned_by_organization_id, organizationId), + eq(platform_integrations.platform, BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM), + eq( + platform_integrations.integration_type, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE + ) + ) + ) + .for('update'); + if (!integration || !integration.workspaceUuid || !integration.workspaceSlug) { + throw new BitbucketWorkspaceAccessTokenCredentialError('not_connected'); + } + + await writeAudit({ + tx, + actor, + organizationId, + action: 'disconnected', + integrationId: integration.id, + workspaceUuid: integration.workspaceUuid, + workspaceSlug: integration.workspaceSlug, + }); + const [deleted] = await tx + .delete(platform_integrations) + .where( + and( + eq(platform_integrations.id, integration.id), + eq(platform_integrations.owned_by_organization_id, organizationId), + eq(platform_integrations.platform, BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM), + eq( + platform_integrations.integration_type, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE + ) + ) + ) + .returning({ id: platform_integrations.id }); + if (!deleted) { + throw new BitbucketWorkspaceAccessTokenCredentialError('credential_conflict'); + } + + return { integrationId: integration.id }; + }); +} + +export async function rotateBitbucketWorkspaceAccessToken( + input: RotateBitbucketWorkspaceAccessTokenInput +): Promise { + const organizationId = canonicalizeOrganizationId(input.organizationId); + await preauthorizeOrganizationManager(organizationId, input.actorUserId); + const [observed] = await db + .select({ + integrationId: platform_integrations.id, + workspaceUuid: platform_integrations.platform_account_id, + workspaceSlug: platform_integrations.platform_account_login, + credentialId: platform_access_token_credentials.id, + credentialVersion: platform_access_token_credentials.credential_version, + }) + .from(platform_integrations) + .innerJoin( + platform_access_token_credentials, + eq(platform_access_token_credentials.platform_integration_id, platform_integrations.id) + ) + .where( + and( + eq(platform_integrations.id, input.integrationId), + eq(platform_integrations.owned_by_organization_id, organizationId), + eq(platform_integrations.platform, BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM), + eq( + platform_integrations.integration_type, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE + ), + eq(platform_access_token_credentials.owned_by_organization_id, organizationId) + ) + ); + if (!observed || !observed.workspaceUuid || !observed.workspaceSlug) { + throw new BitbucketWorkspaceAccessTokenCredentialError('not_connected'); + } + const observedWorkspaceUuid = observed.workspaceUuid; + const observedWorkspaceSlug = observed.workspaceSlug; + if (observed.credentialVersion >= 2_147_483_647) { + throw new BitbucketWorkspaceAccessTokenCredentialError('credential_conflict'); + } + + const validation = await validateBitbucketWorkspaceAccessToken({ + expectedWorkspaceUuid: observedWorkspaceUuid, + accessToken: input.accessToken, + }); + const credentialVersion = observed.credentialVersion + 1; + const validatedAt = new Date().toISOString(); + const tokenEncrypted = encryptAccessToken({ + accessToken: input.accessToken, + credentialId: observed.credentialId, + integrationId: observed.integrationId, + organizationId, + credentialVersion, + }); + const metadata = BitbucketWorkspaceAccessTokenMetadataSchema.parse({ + displayName: validation.workspace.displayName, + }); + const repositories = cacheRepositories(validation.repositories); + + return db.transaction(async tx => { + await acquireOrganizationCredentialLock(tx, organizationId); + const actor = await requireOrganizationManager(tx, organizationId, input.actorUserId); + const [current] = await tx + .select({ + integrationId: platform_integrations.id, + workspaceUuid: platform_integrations.platform_account_id, + workspaceSlug: platform_integrations.platform_account_login, + credentialId: platform_access_token_credentials.id, + credentialVersion: platform_access_token_credentials.credential_version, + }) + .from(platform_integrations) + .innerJoin( + platform_access_token_credentials, + eq(platform_access_token_credentials.platform_integration_id, platform_integrations.id) + ) + .where( + and( + eq(platform_integrations.id, input.integrationId), + eq(platform_integrations.owned_by_organization_id, organizationId), + eq(platform_integrations.platform, BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM), + eq( + platform_integrations.integration_type, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE + ), + eq(platform_access_token_credentials.owned_by_organization_id, organizationId) + ) + ) + .for('update'); + if (!current) { + throw new BitbucketWorkspaceAccessTokenCredentialError('not_connected'); + } + if ( + current.credentialId !== observed.credentialId || + current.credentialVersion !== observed.credentialVersion || + current.workspaceUuid !== observedWorkspaceUuid || + current.workspaceSlug !== observedWorkspaceSlug + ) { + throw new BitbucketWorkspaceAccessTokenCredentialError('credential_conflict'); + } + + const [updatedCredential] = await tx + .update(platform_access_token_credentials) + .set({ + token_encrypted: tokenEncrypted, + expires_at: null, + provider_credential_type: validation.providerCredentialType, + provider_scopes: validation.providerScopes, + provider_verified_at: validatedAt, + credential_version: credentialVersion, + last_validated_at: validatedAt, + updated_at: validatedAt, + }) + .where( + and( + eq(platform_access_token_credentials.id, observed.credentialId), + eq(platform_access_token_credentials.platform_integration_id, observed.integrationId), + eq(platform_access_token_credentials.owned_by_organization_id, organizationId), + eq(platform_access_token_credentials.credential_version, observed.credentialVersion) + ) + ) + .returning({ id: platform_access_token_credentials.id }); + if (!updatedCredential) { + throw new BitbucketWorkspaceAccessTokenCredentialError('credential_conflict'); + } + + const [updatedIntegration] = await tx + .update(platform_integrations) + .set({ + platform_account_login: validation.workspace.slug, + metadata, + repositories, + repositories_synced_at: validatedAt, + auth_invalid_at: null, + auth_invalid_reason: null, + updated_at: validatedAt, + }) + .where( + and( + eq(platform_integrations.id, observed.integrationId), + eq(platform_integrations.owned_by_organization_id, organizationId), + eq(platform_integrations.platform, BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM), + eq( + platform_integrations.integration_type, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE + ), + eq(platform_integrations.platform_account_id, observedWorkspaceUuid) + ) + ) + .returning({ id: platform_integrations.id }); + if (!updatedIntegration) { + throw new BitbucketWorkspaceAccessTokenCredentialError('credential_conflict'); + } + + await writeAudit({ + tx, + actor, + organizationId, + action: 'rotated', + integrationId: observed.integrationId, + workspaceUuid: validation.workspace.uuid, + workspaceSlug: validation.workspace.slug, + }); + + return { + integrationId: observed.integrationId, + workspace: validation.workspace, + credentialVersion, + repositoryCount: repositories.length, + validatedAt, + }; + }); +} + +export async function connectBitbucketWorkspaceAccessToken( + input: ConnectBitbucketWorkspaceAccessTokenInput +): Promise { + const organizationId = canonicalizeOrganizationId(input.organizationId); + await preauthorizeOrganizationManager(organizationId, input.actorUserId); + const validation = await validateBitbucketWorkspaceAccessToken({ + accessToken: input.accessToken, + }); + + const integrationId = randomUUID(); + const credentialId = randomUUID(); + const credentialVersion = INITIAL_CREDENTIAL_VERSION; + const validatedAt = new Date().toISOString(); + const tokenEncrypted = encryptAccessToken({ + accessToken: input.accessToken, + credentialId, + integrationId, + organizationId, + credentialVersion, + }); + const metadata = BitbucketWorkspaceAccessTokenMetadataSchema.parse({ + displayName: validation.workspace.displayName, + }); + const repositories = cacheRepositories(validation.repositories); + + return db.transaction(async tx => { + await acquireOrganizationCredentialLock(tx, organizationId); + const actor = await requireOrganizationManager(tx, organizationId, input.actorUserId); + + const [currentBitbucketIntegration] = await tx + .select({ id: platform_integrations.id }) + .from(platform_integrations) + .where( + and( + eq(platform_integrations.owned_by_organization_id, organizationId), + eq(platform_integrations.platform, BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM) + ) + ) + .for('update'); + if (currentBitbucketIntegration) { + throw new BitbucketWorkspaceAccessTokenCredentialError('credential_conflict'); + } + + await tx.insert(platform_integrations).values({ + id: integrationId, + owned_by_organization_id: organizationId, + owned_by_user_id: null, + created_by_user_id: input.actorUserId, + platform: BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM, + integration_type: BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE, + platform_installation_id: null, + platform_account_id: validation.workspace.uuid, + platform_account_login: validation.workspace.slug, + permissions: null, + scopes: null, + repository_access: 'all', + repositories, + repositories_synced_at: validatedAt, + auth_invalid_at: null, + auth_invalid_reason: null, + integration_status: INTEGRATION_STATUS.ACTIVE, + metadata, + updated_at: validatedAt, + }); + await tx.insert(platform_access_token_credentials).values({ + id: credentialId, + platform_integration_id: integrationId, + owned_by_organization_id: organizationId, + platform: BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM, + integration_type: BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE, + token_encrypted: tokenEncrypted, + expires_at: null, + provider_credential_type: validation.providerCredentialType, + provider_scopes: validation.providerScopes, + provider_verified_at: validatedAt, + credential_version: credentialVersion, + last_validated_at: validatedAt, + updated_at: validatedAt, + }); + await writeAudit({ + tx, + actor, + organizationId, + action: 'connected', + integrationId, + workspaceUuid: validation.workspace.uuid, + workspaceSlug: validation.workspace.slug, + }); + + return { + integrationId, + workspace: validation.workspace, + credentialVersion, + repositoryCount: repositories.length, + validatedAt, + }; + }); +} diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-organization-authorization.ts b/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-organization-authorization.ts new file mode 100644 index 0000000000..1754ef3ab9 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-organization-authorization.ts @@ -0,0 +1,84 @@ +import 'server-only'; + +import { kilocode_users, organization_memberships, organizations } from '@kilocode/db/schema'; +import { buildBitbucketOrganizationCredentialLockKey } from '@kilocode/worker-utils/bitbucket-workspace-access-token'; +import { and, eq, inArray, isNull, sql } from 'drizzle-orm'; +import type { DrizzleTransaction } from '@/lib/drizzle'; + +export type BitbucketWorkspaceAccessTokenAuthorizedActor = { + id: string; + email: string | null; + name: string | null; +}; + +export type BitbucketWorkspaceAccessTokenAuthorizationErrorCode = + | 'unauthorized' + | 'organization_not_found'; + +export class BitbucketWorkspaceAccessTokenOrganizationAuthorizationError extends Error { + constructor(readonly code: BitbucketWorkspaceAccessTokenAuthorizationErrorCode) { + super( + code === 'organization_not_found' + ? 'The organization was not found' + : 'The current user cannot manage this organization integration' + ); + this.name = 'BitbucketWorkspaceAccessTokenOrganizationAuthorizationError'; + } +} + +export async function lockBitbucketWorkspaceAccessTokenOrganization( + tx: DrizzleTransaction, + organizationId: string +): Promise { + await tx.execute( + sql`SELECT pg_advisory_xact_lock(hashtextextended(${buildBitbucketOrganizationCredentialLockKey(organizationId)}, 0))` + ); +} + +export async function requireBitbucketWorkspaceAccessTokenOrganizationManager( + tx: DrizzleTransaction, + organizationId: string, + actorUserId: string +): Promise { + const [organization] = await tx + .select({ id: organizations.id }) + .from(organizations) + .where(eq(organizations.id, organizationId)) + .for('update'); + if (!organization) { + throw new BitbucketWorkspaceAccessTokenOrganizationAuthorizationError('organization_not_found'); + } + + const [actor] = await tx + .select({ + id: kilocode_users.id, + email: kilocode_users.google_user_email, + name: kilocode_users.google_user_name, + isAdmin: kilocode_users.is_admin, + }) + .from(kilocode_users) + .where(and(eq(kilocode_users.id, actorUserId), isNull(kilocode_users.blocked_reason))) + .for('update'); + if (!actor) { + throw new BitbucketWorkspaceAccessTokenOrganizationAuthorizationError('unauthorized'); + } + + if (!actor.isAdmin) { + const [membership] = await tx + .select({ id: organization_memberships.id }) + .from(organization_memberships) + .where( + and( + eq(organization_memberships.organization_id, organizationId), + eq(organization_memberships.kilo_user_id, actorUserId), + inArray(organization_memberships.role, ['owner', 'billing_manager']) + ) + ) + .for('update'); + if (!membership) { + throw new BitbucketWorkspaceAccessTokenOrganizationAuthorizationError('unauthorized'); + } + } + + return { id: actor.id, email: actor.email, name: actor.name }; +} diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-repository-cache.test.ts b/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-repository-cache.test.ts new file mode 100644 index 0000000000..249bbf4980 --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-repository-cache.test.ts @@ -0,0 +1,764 @@ +/* eslint-disable drizzle/enforce-delete-with-where */ +import { afterAll, afterEach, beforeAll, describe, expect, it, jest } from '@jest/globals'; +import { + kilocode_users, + organization_memberships, + organizations, + platform_access_token_credentials, + platform_integrations, + type Organization, + type User, +} from '@kilocode/db/schema'; +import { db } from '@/lib/drizzle'; +import { eq } from 'drizzle-orm'; +import { createTestOrganization } from '@/tests/helpers/organization.helper'; +import { insertTestUser } from '@/tests/helpers/user.helper'; +import type { fetchBitbucketRepositoriesForOrganization as FetchForOrganization } from '@/lib/cloud-agent/bitbucket-integration-helpers'; +import type { BitbucketRepositoryListResult } from './token-service-client'; +import type * as TokenServiceClientModule from './token-service-client'; +import type { + getBitbucketWorkspaceAccessTokenStatus as GetStatus, + readCachedBitbucketWorkspaceAccessTokenRepositories as ReadRepositories, + refreshBitbucketWorkspaceAccessTokenRepositories as RefreshRepositories, +} from './workspace-access-token-repository-cache'; + +const mockFetchBitbucketRepositoriesFromTokenService = + jest.fn<(kiloUserId: string, organizationId: string) => Promise>(); + +jest.mock('./token-service-client', () => ({ + BitbucketRepositorySchema: + jest.requireActual('./token-service-client') + .BitbucketRepositorySchema, + fetchBitbucketWorkspaceAccessTokenRepositoriesFromTokenService: + mockFetchBitbucketRepositoriesFromTokenService, +})); + +const WORKSPACE_UUID = '11111111-1111-4111-8111-111111111111'; +const REPOSITORY_UUID = '22222222-2222-4222-8222-222222222222'; +const CACHED_AT = '2026-06-24T08:00:00.000Z'; +const WINNER_CACHED_AT = '2026-06-24T09:00:00.000Z'; +const CACHED_REPOSITORY = { + id: REPOSITORY_UUID, + name: 'API', + full_name: 'acme/api', + private: true, + default_branch: 'main', +}; +const REFRESHED_REPOSITORY = { + id: '33333333-3333-4333-8333-333333333333', + workspaceUuid: WORKSPACE_UUID, + name: 'Web', + fullName: 'acme/web', + private: false, +}; + +let fetchBitbucketRepositoriesForOrganization: typeof FetchForOrganization; +let getBitbucketWorkspaceAccessTokenStatus: typeof GetStatus; +let readCachedBitbucketWorkspaceAccessTokenRepositories: typeof ReadRepositories; +let refreshBitbucketWorkspaceAccessTokenRepositories: typeof RefreshRepositories; + +async function insertStaticIntegration( + organizationId: string, + actorUserId: string, + cache: { repositories?: Array | null; syncedAt?: string | null } = {} +) { + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_organization_id: organizationId, + owned_by_user_id: null, + created_by_user_id: actorUserId, + platform: 'bitbucket', + integration_type: 'workspace_access_token', + platform_account_id: WORKSPACE_UUID, + platform_account_login: 'acme', + platform_installation_id: null, + repository_access: 'all', + repositories: cache.repositories === undefined ? [CACHED_REPOSITORY] : cache.repositories, + repositories_synced_at: cache.syncedAt === undefined ? CACHED_AT : cache.syncedAt, + integration_status: 'active', + metadata: { displayName: 'Acme Workspace' }, + }) + .returning(); + if (!integration) throw new Error('Expected static Bitbucket integration'); + + await db.insert(platform_access_token_credentials).values({ + platform_integration_id: integration.id, + owned_by_organization_id: organizationId, + platform: 'bitbucket', + integration_type: 'workspace_access_token', + token_encrypted: 'ciphertext', + expires_at: '2030-01-01T23:59:59.999Z', + provider_credential_type: 'workspace_access_token', + provider_scopes: ['account', 'pullrequest', 'repository', 'repository:write', 'webhook'], + provider_verified_at: CACHED_AT, + credential_version: 1, + last_validated_at: CACHED_AT, + }); + + return integration; +} + +async function replaceWithWinnerIntegration( + organizationId: string, + actorUserId: string, + losingIntegrationId: string +) { + const winnerIntegrationId = '55555555-5555-4555-8555-555555555555'; + await db.transaction(async tx => { + await tx.delete(platform_integrations).where(eq(platform_integrations.id, losingIntegrationId)); + await tx.insert(platform_integrations).values({ + id: winnerIntegrationId, + owned_by_organization_id: organizationId, + owned_by_user_id: null, + created_by_user_id: actorUserId, + platform: 'bitbucket', + integration_type: 'workspace_access_token', + platform_account_id: WORKSPACE_UUID, + platform_account_login: 'acme', + platform_installation_id: null, + repository_access: 'all', + repositories: [ + { + id: REFRESHED_REPOSITORY.id, + name: REFRESHED_REPOSITORY.name, + full_name: REFRESHED_REPOSITORY.fullName, + private: REFRESHED_REPOSITORY.private, + }, + ], + repositories_synced_at: WINNER_CACHED_AT, + integration_status: 'active', + metadata: { displayName: 'Acme Workspace' }, + }); + await tx.insert(platform_access_token_credentials).values({ + id: '66666666-6666-4666-8666-666666666666', + platform_integration_id: winnerIntegrationId, + owned_by_organization_id: organizationId, + platform: 'bitbucket', + integration_type: 'workspace_access_token', + token_encrypted: 'reconnected-ciphertext', + expires_at: '2030-01-01T23:59:59.999Z', + provider_credential_type: 'workspace_access_token', + provider_scopes: ['account', 'pullrequest', 'repository', 'repository:write', 'webhook'], + provider_verified_at: WINNER_CACHED_AT, + credential_version: 1, + last_validated_at: WINNER_CACHED_AT, + }); + }); + return winnerIntegrationId; +} + +describe('Bitbucket Workspace Access Token repository cache', () => { + let user: User; + let organization: Organization; + + beforeAll(async () => { + ({ + getBitbucketWorkspaceAccessTokenStatus, + readCachedBitbucketWorkspaceAccessTokenRepositories, + refreshBitbucketWorkspaceAccessTokenRepositories, + } = await import('./workspace-access-token-repository-cache')); + ({ fetchBitbucketRepositoriesForOrganization } = + await import('@/lib/cloud-agent/bitbucket-integration-helpers')); + user = await insertTestUser(); + organization = await createTestOrganization('Static Bitbucket Cache Org', user.id, 0); + }); + + afterEach(async () => { + jest.clearAllMocks(); + await db.delete(platform_access_token_credentials); + await db.delete(platform_integrations); + }); + + afterAll(async () => { + await db.delete(organizations); + await db.delete(kilocode_users); + }); + + it('returns an initialized organization cache without calling the token service', async () => { + await insertStaticIntegration(organization.id, user.id); + + await expect( + readCachedBitbucketWorkspaceAccessTokenRepositories({ + organizationId: organization.id, + }) + ).resolves.toEqual({ + status: 'available', + repositories: [ + { + id: REPOSITORY_UUID, + workspaceUuid: WORKSPACE_UUID, + name: 'API', + fullName: 'acme/api', + private: true, + defaultBranch: 'main', + }, + ], + syncedAt: CACHED_AT, + }); + expect(mockFetchBitbucketRepositoriesFromTokenService).not.toHaveBeenCalled(); + }); + + it('reads status with a canonicalized uppercase organization UUID', async () => { + const integration = await insertStaticIntegration(organization.id, user.id); + + await expect( + getBitbucketWorkspaceAccessTokenStatus(organization.id.toUpperCase()) + ).resolves.toMatchObject({ + status: 'connected', + recoveryAction: null, + integrationId: integration.id, + workspace: { uuid: WORKSPACE_UUID, slug: 'acme' }, + }); + }); + + it('requires disconnect and reconnect when the credential is missing', async () => { + await insertStaticIntegration(organization.id, user.id); + await db.delete(platform_access_token_credentials); + + await expect(getBitbucketWorkspaceAccessTokenStatus(organization.id)).resolves.toMatchObject({ + status: 'reconnect_required', + recoveryAction: 'disconnect_and_connect', + }); + }); + + it.each([ + ['malformed workspace', { platform_account_id: 'not-a-workspace-uuid' }], + ['inactive parent', { integration_status: 'inactive' }], + ] as const)('requires disconnect and reconnect for an %s', async (_label, update) => { + const integration = await insertStaticIntegration(organization.id, user.id); + await db + .update(platform_integrations) + .set(update) + .where(eq(platform_integrations.id, integration.id)); + + await expect(getBitbucketWorkspaceAccessTokenStatus(organization.id)).resolves.toMatchObject({ + status: 'reconnect_required', + recoveryAction: 'disconnect_and_connect', + }); + }); + + it('allows token replacement for ordinary invalidation', async () => { + const integration = await insertStaticIntegration(organization.id, user.id); + await db + .update(platform_integrations) + .set({ + auth_invalid_at: '2026-06-24T09:00:00.000Z', + auth_invalid_reason: 'provider_rejected', + }) + .where(eq(platform_integrations.id, integration.id)); + + await expect(getBitbucketWorkspaceAccessTokenStatus(organization.id)).resolves.toMatchObject({ + status: 'reconnect_required', + recoveryAction: 'replace_token', + }); + }); + + it('ignores a legacy recorded expiry when provider evidence is valid', async () => { + const integration = await insertStaticIntegration(organization.id, user.id); + await db + .update(platform_access_token_credentials) + .set({ expires_at: '2020-01-01T23:59:59.999Z' }) + .where(eq(platform_access_token_credentials.platform_integration_id, integration.id)); + + await expect(getBitbucketWorkspaceAccessTokenStatus(organization.id)).resolves.toMatchObject({ + status: 'connected', + recoveryAction: null, + }); + }); + + it('canonicalizes uppercase owner and integration UUIDs before a forced refresh', async () => { + const integration = await insertStaticIntegration(organization.id, user.id); + mockFetchBitbucketRepositoriesFromTokenService.mockResolvedValue({ + status: 'available', + repositories: [REFRESHED_REPOSITORY], + }); + + await expect( + refreshBitbucketWorkspaceAccessTokenRepositories({ + organizationId: organization.id.toUpperCase(), + kiloUserId: user.id, + expectedIntegrationId: integration.id.toUpperCase(), + }) + ).resolves.toMatchObject({ + status: 'available', + repositories: [REFRESHED_REPOSITORY], + }); + expect(mockFetchBitbucketRepositoriesFromTokenService).toHaveBeenCalledWith( + user.id, + organization.id + ); + + const [updated] = await db + .select({ repositories: platform_integrations.repositories }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(updated?.repositories).toEqual([ + { + id: REFRESHED_REPOSITORY.id, + name: REFRESHED_REPOSITORY.name, + full_name: REFRESHED_REPOSITORY.fullName, + private: REFRESHED_REPOSITORY.private, + }, + ]); + }); + + it('does not initialize an absent member cache through the token service', async () => { + const integration = await insertStaticIntegration(organization.id, user.id, { + repositories: null, + syncedAt: null, + }); + mockFetchBitbucketRepositoriesFromTokenService.mockResolvedValue({ + status: 'available', + repositories: [REFRESHED_REPOSITORY], + }); + + await expect( + fetchBitbucketRepositoriesForOrganization(organization.id, user.id) + ).resolves.toEqual({ + status: 'temporarily_unavailable', + }); + expect(mockFetchBitbucketRepositoriesFromTokenService).not.toHaveBeenCalled(); + + const [unchanged] = await db + .select({ + repositories: platform_integrations.repositories, + syncedAt: platform_integrations.repositories_synced_at, + }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(unchanged).toEqual({ repositories: null, syncedAt: null }); + }); + + it('does not replace a malformed member cache through the token service', async () => { + const integration = await insertStaticIntegration(organization.id, user.id, { + repositories: null, + syncedAt: CACHED_AT, + }); + const malformedRepositories = [{ id: 'not-a-repository-uuid' }]; + await db + .update(platform_integrations) + .set({ repositories: malformedRepositories as never }) + .where(eq(platform_integrations.id, integration.id)); + mockFetchBitbucketRepositoriesFromTokenService.mockResolvedValue({ + status: 'available', + repositories: [REFRESHED_REPOSITORY], + }); + + await expect( + fetchBitbucketRepositoriesForOrganization(organization.id, user.id) + ).resolves.toEqual({ + status: 'temporarily_unavailable', + }); + expect(mockFetchBitbucketRepositoriesFromTokenService).not.toHaveBeenCalled(); + + const [unchanged] = await db + .select({ + repositories: platform_integrations.repositories, + syncedAt: platform_integrations.repositories_synced_at, + }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(unchanged?.repositories).toEqual(malformedRepositories); + expect(new Date(unchanged?.syncedAt ?? '').toISOString()).toBe(CACHED_AT); + }); + + it('initializes an absent cache through an explicit forced refresh', async () => { + const integration = await insertStaticIntegration(organization.id, user.id, { + repositories: null, + syncedAt: null, + }); + mockFetchBitbucketRepositoriesFromTokenService.mockResolvedValue({ + status: 'available', + repositories: [ + { + id: REPOSITORY_UUID, + workspaceUuid: WORKSPACE_UUID, + name: 'API', + fullName: 'acme/api', + private: true, + defaultBranch: 'main', + }, + ], + }); + + await expect( + refreshBitbucketWorkspaceAccessTokenRepositories({ + organizationId: organization.id, + kiloUserId: user.id, + expectedIntegrationId: integration.id, + }) + ).resolves.toMatchObject({ + status: 'available', + repositories: [expect.objectContaining({ id: REPOSITORY_UUID })], + syncedAt: expect.any(String), + }); + expect(mockFetchBitbucketRepositoriesFromTokenService).toHaveBeenCalledWith( + user.id, + organization.id + ); + + const [updated] = await db + .select({ + repositories: platform_integrations.repositories, + syncedAt: platform_integrations.repositories_synced_at, + }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(updated?.repositories).toEqual([CACHED_REPOSITORY]); + expect(updated?.syncedAt).toBeTruthy(); + }); + + it('returns a current malformed available result as invalid_request without mutation', async () => { + const integration = await insertStaticIntegration(organization.id, user.id); + mockFetchBitbucketRepositoriesFromTokenService.mockResolvedValue({ + status: 'available', + repositories: [{ ...REFRESHED_REPOSITORY, workspaceUuid: REPOSITORY_UUID }], + }); + + await expect( + refreshBitbucketWorkspaceAccessTokenRepositories({ + organizationId: organization.id, + kiloUserId: user.id, + expectedIntegrationId: integration.id, + }) + ).resolves.toEqual({ status: 'invalid_request' }); + + const [unchanged] = await db + .select({ + repositories: platform_integrations.repositories, + syncedAt: platform_integrations.repositories_synced_at, + }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(unchanged?.repositories).toEqual([CACHED_REPOSITORY]); + expect(new Date(unchanged?.syncedAt ?? '').toISOString()).toBe(CACHED_AT); + }); + + it.each([ + 'insufficient_permissions', + 'reconnect_required', + 'temporarily_unavailable', + 'invalid_request', + ] as const)('propagates %s and preserves the last successful cache', async status => { + const integration = await insertStaticIntegration(organization.id, user.id); + mockFetchBitbucketRepositoriesFromTokenService.mockResolvedValue({ status }); + + await expect( + refreshBitbucketWorkspaceAccessTokenRepositories({ + organizationId: organization.id, + kiloUserId: user.id, + expectedIntegrationId: integration.id, + }) + ).resolves.toEqual({ status }); + + const [unchanged] = await db + .select({ + repositories: platform_integrations.repositories, + syncedAt: platform_integrations.repositories_synced_at, + }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(unchanged?.repositories).toEqual([CACHED_REPOSITORY]); + expect(new Date(unchanged?.syncedAt ?? '').toISOString()).toBe(CACHED_AT); + }); + + it('preserves a newer same-generation cache when an older refresh completes later', async () => { + const integration = await insertStaticIntegration(organization.id, user.id); + mockFetchBitbucketRepositoriesFromTokenService.mockImplementation(async () => { + await db + .update(platform_integrations) + .set({ + repositories: [ + { + id: REFRESHED_REPOSITORY.id, + name: REFRESHED_REPOSITORY.name, + full_name: REFRESHED_REPOSITORY.fullName, + private: REFRESHED_REPOSITORY.private, + }, + ], + repositories_synced_at: WINNER_CACHED_AT, + }) + .where(eq(platform_integrations.id, integration.id)); + return { + status: 'available', + repositories: [ + { + id: '77777777-7777-4777-8777-777777777777', + workspaceUuid: WORKSPACE_UUID, + name: 'Older', + fullName: 'acme/older', + private: true, + }, + ], + }; + }); + + await expect( + refreshBitbucketWorkspaceAccessTokenRepositories({ + organizationId: organization.id, + kiloUserId: user.id, + expectedIntegrationId: integration.id, + }) + ).resolves.toEqual({ + status: 'available', + repositories: [REFRESHED_REPOSITORY], + syncedAt: WINNER_CACHED_AT, + }); + + const [winner] = await db + .select({ + repositories: platform_integrations.repositories, + syncedAt: platform_integrations.repositories_synced_at, + }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(winner?.repositories).toEqual([ + { + id: REFRESHED_REPOSITORY.id, + name: REFRESHED_REPOSITORY.name, + full_name: REFRESHED_REPOSITORY.fullName, + private: REFRESHED_REPOSITORY.private, + }, + ]); + expect(new Date(winner?.syncedAt ?? '').toISOString()).toBe(WINNER_CACHED_AT); + }); + + it.each(['reconnect_required', 'insufficient_permissions', 'temporarily_unavailable'] as const)( + 'ignores stale %s after a newer credential generation wins', + async status => { + const integration = await insertStaticIntegration(organization.id, user.id); + mockFetchBitbucketRepositoriesFromTokenService.mockImplementation(async () => { + await db.transaction(async tx => { + await tx + .update(platform_access_token_credentials) + .set({ credential_version: 2, token_encrypted: 'winner-ciphertext' }) + .where(eq(platform_access_token_credentials.platform_integration_id, integration.id)); + await tx + .update(platform_integrations) + .set({ + repositories: [ + { + id: REFRESHED_REPOSITORY.id, + name: REFRESHED_REPOSITORY.name, + full_name: REFRESHED_REPOSITORY.fullName, + private: REFRESHED_REPOSITORY.private, + }, + ], + repositories_synced_at: WINNER_CACHED_AT, + }) + .where(eq(platform_integrations.id, integration.id)); + }); + return { status }; + }); + + await expect( + refreshBitbucketWorkspaceAccessTokenRepositories({ + organizationId: organization.id, + kiloUserId: user.id, + expectedIntegrationId: integration.id, + }) + ).resolves.toEqual({ + status: 'available', + repositories: [REFRESHED_REPOSITORY], + syncedAt: WINNER_CACHED_AT, + }); + + const [winner] = await db + .select({ + repositories: platform_integrations.repositories, + syncedAt: platform_integrations.repositories_synced_at, + }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(winner?.repositories).toEqual([ + { + id: REFRESHED_REPOSITORY.id, + name: REFRESHED_REPOSITORY.name, + full_name: REFRESHED_REPOSITORY.fullName, + private: REFRESHED_REPOSITORY.private, + }, + ]); + expect(new Date(winner?.syncedAt ?? '').toISOString()).toBe(WINNER_CACHED_AT); + } + ); + + it.each([ + ['provider failure', { status: 'reconnect_required' } as const], + [ + 'malformed available result', + { + status: 'available' as const, + repositories: [{ ...REFRESHED_REPOSITORY, workspaceUuid: REPOSITORY_UUID }], + }, + ], + [ + 'valid available result', + { status: 'available' as const, repositories: [REFRESHED_REPOSITORY] }, + ], + ])('returns the reconnected winner cache after a stale %s', async (_label, providerResult) => { + const integration = await insertStaticIntegration(organization.id, user.id); + mockFetchBitbucketRepositoriesFromTokenService.mockImplementation(async () => { + await replaceWithWinnerIntegration(organization.id, user.id, integration.id); + return providerResult; + }); + + await expect( + refreshBitbucketWorkspaceAccessTokenRepositories({ + organizationId: organization.id, + kiloUserId: user.id, + expectedIntegrationId: integration.id, + }) + ).resolves.toEqual({ + status: 'available', + repositories: [REFRESHED_REPOSITORY], + syncedAt: WINNER_CACHED_AT, + }); + }); + + it('keeps the successful cache visible in status after refresh failure', async () => { + const integration = await insertStaticIntegration(organization.id, user.id); + mockFetchBitbucketRepositoriesFromTokenService.mockResolvedValue({ + status: 'temporarily_unavailable', + }); + + await refreshBitbucketWorkspaceAccessTokenRepositories({ + organizationId: organization.id, + kiloUserId: user.id, + expectedIntegrationId: integration.id, + }); + + await expect(getBitbucketWorkspaceAccessTokenStatus(organization.id)).resolves.toMatchObject({ + status: 'connected', + repositoryCache: { + status: 'available', + repositories: [expect.objectContaining({ id: REPOSITORY_UUID })], + syncedAt: CACHED_AT, + }, + }); + }); + + it('fails closed when the joined credential evidence no longer satisfies the profile', async () => { + const integration = await insertStaticIntegration(organization.id, user.id); + await db + .update(platform_access_token_credentials) + .set({ provider_scopes: ['account'] }) + .where(eq(platform_access_token_credentials.platform_integration_id, integration.id)); + + await expect( + readCachedBitbucketWorkspaceAccessTokenRepositories({ + organizationId: organization.id, + }) + ).resolves.toEqual({ status: 'reconnect_required' }); + expect(mockFetchBitbucketRepositoriesFromTokenService).not.toHaveBeenCalled(); + }); + + it('rejects a forced refresh when management authority is lost during provider I/O', async () => { + const manager = await insertTestUser(); + await db.insert(organization_memberships).values({ + organization_id: organization.id, + kilo_user_id: manager.id, + role: 'billing_manager', + }); + const integration = await insertStaticIntegration(organization.id, user.id); + mockFetchBitbucketRepositoriesFromTokenService.mockImplementation(async () => { + await db + .delete(organization_memberships) + .where(eq(organization_memberships.kilo_user_id, manager.id)); + return { status: 'available', repositories: [REFRESHED_REPOSITORY] }; + }); + + await expect( + refreshBitbucketWorkspaceAccessTokenRepositories({ + organizationId: organization.id, + kiloUserId: manager.id, + expectedIntegrationId: integration.id, + }) + ).rejects.toMatchObject({ + name: 'BitbucketWorkspaceAccessTokenRepositoryCacheAuthorizationError', + message: 'The current user cannot refresh this organization integration', + }); + + const [unchanged] = await db + .select({ + repositories: platform_integrations.repositories, + syncedAt: platform_integrations.repositories_synced_at, + }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(unchanged?.repositories).toEqual([CACHED_REPOSITORY]); + expect(new Date(unchanged?.syncedAt ?? '').toISOString()).toBe(CACHED_AT); + }); + + it('does not replace the cache after the credential generation changes', async () => { + const integration = await insertStaticIntegration(organization.id, user.id); + mockFetchBitbucketRepositoriesFromTokenService.mockImplementation(async () => { + await db + .update(platform_access_token_credentials) + .set({ credential_version: 2, token_encrypted: 'rotated-ciphertext' }) + .where(eq(platform_access_token_credentials.platform_integration_id, integration.id)); + return { status: 'available', repositories: [REFRESHED_REPOSITORY] }; + }); + + await expect( + refreshBitbucketWorkspaceAccessTokenRepositories({ + organizationId: organization.id, + kiloUserId: user.id, + expectedIntegrationId: integration.id, + }) + ).resolves.toEqual({ + status: 'available', + repositories: [ + { + id: REPOSITORY_UUID, + workspaceUuid: WORKSPACE_UUID, + name: 'API', + fullName: 'acme/api', + private: true, + defaultBranch: 'main', + }, + ], + syncedAt: CACHED_AT, + }); + + const [unchanged] = await db + .select({ repositories: platform_integrations.repositories }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(unchanged?.repositories).toEqual([CACHED_REPOSITORY]); + }); + + it('never clears parent invalidation while a refresh is in flight', async () => { + const integration = await insertStaticIntegration(organization.id, user.id); + mockFetchBitbucketRepositoriesFromTokenService.mockImplementation(async () => { + await db + .update(platform_integrations) + .set({ + auth_invalid_at: '2026-06-24T09:00:00.000Z', + auth_invalid_reason: 'provider_rejected', + }) + .where(eq(platform_integrations.id, integration.id)); + return { status: 'available', repositories: [REFRESHED_REPOSITORY] }; + }); + + await expect( + refreshBitbucketWorkspaceAccessTokenRepositories({ + organizationId: organization.id, + kiloUserId: user.id, + expectedIntegrationId: integration.id, + }) + ).resolves.toEqual({ status: 'reconnect_required' }); + + const [invalidated] = await db + .select({ + repositories: platform_integrations.repositories, + invalidAt: platform_integrations.auth_invalid_at, + invalidReason: platform_integrations.auth_invalid_reason, + }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(invalidated?.repositories).toEqual([CACHED_REPOSITORY]); + expect(new Date(invalidated?.invalidAt ?? '').toISOString()).toBe('2026-06-24T09:00:00.000Z'); + expect(invalidated?.invalidReason).toBe('provider_rejected'); + }); +}); diff --git a/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-repository-cache.ts b/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-repository-cache.ts new file mode 100644 index 0000000000..4c50168eca --- /dev/null +++ b/apps/web/src/lib/integrations/platforms/bitbucket/workspace-access-token-repository-cache.ts @@ -0,0 +1,517 @@ +import 'server-only'; + +import { + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INVALIDATION_REASONS, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_PROVIDER_CREDENTIAL_TYPE, + hasRequiredBitbucketWorkspaceAccessTokenScopes, +} from '@kilocode/worker-utils/bitbucket-workspace-access-token'; +import { platform_access_token_credentials, platform_integrations } from '@kilocode/db/schema'; +import { and, eq, exists, isNull } from 'drizzle-orm'; +import { z } from 'zod'; +import { db, type DrizzleTransaction } from '@/lib/drizzle'; +import { INTEGRATION_STATUS } from '@/lib/integrations/core/constants'; +import { BitbucketWorkspaceAccessTokenMetadataSchema } from './metadata'; +import { + BitbucketRepositorySchema, + fetchBitbucketWorkspaceAccessTokenRepositoriesFromTokenService, +} from './token-service-client'; +import { + BitbucketWorkspaceAccessTokenOrganizationAuthorizationError, + lockBitbucketWorkspaceAccessTokenOrganization, + requireBitbucketWorkspaceAccessTokenOrganizationManager, +} from './workspace-access-token-organization-authorization'; + +const WorkspaceSlugSchema = z.string().regex(/^[a-z0-9][a-z0-9_.-]*$/); +const InvalidationReasonSchema = z.enum(BITBUCKET_WORKSPACE_ACCESS_TOKEN_INVALIDATION_REASONS); + +const CachedRepositorySchema = z + .object({ + id: z.uuid(), + name: z.string().min(1), + full_name: z.string().min(3), + private: z.boolean(), + default_branch: z.string().min(1).optional(), + }) + .strict(); + +export const BitbucketWorkspaceAccessTokenRepositoryListResultSchema = z.discriminatedUnion( + 'status', + [ + z + .object({ + status: z.literal('available'), + repositories: z.array(BitbucketRepositorySchema), + syncedAt: z.iso.datetime(), + }) + .strict(), + z.object({ status: z.literal('not_connected') }).strict(), + z.object({ status: z.literal('reconnect_required') }).strict(), + z.object({ status: z.literal('insufficient_permissions') }).strict(), + z.object({ status: z.literal('temporarily_unavailable') }).strict(), + z.object({ status: z.literal('invalid_request') }).strict(), + ] +); + +export type BitbucketWorkspaceAccessTokenRepositoryListResult = z.infer< + typeof BitbucketWorkspaceAccessTokenRepositoryListResultSchema +>; + +type AvailableRepositoryCache = Extract< + BitbucketWorkspaceAccessTokenRepositoryListResult, + { status: 'available' } +>; + +type ReadCachedRepositoriesInput = { + organizationId: string; + expectedIntegrationId?: string; +}; + +type RefreshRepositoriesInput = { + organizationId: string; + kiloUserId: string; + expectedIntegrationId: string; +}; + +type WorkspaceIdentity = { + uuid: string; + slug: string; +}; + +export class BitbucketWorkspaceAccessTokenRepositoryCacheAuthorizationError extends Error { + constructor() { + super('The current user cannot refresh this organization integration'); + this.name = 'BitbucketWorkspaceAccessTokenRepositoryCacheAuthorizationError'; + } +} + +function canonicalizeUuid(value: string): string | null { + const canonical = value.toLowerCase(); + return value.trim() === value && z.uuid().safeParse(canonical).success ? canonical : null; +} + +async function loadIntegration(organizationId: string) { + const [row] = await db + .select({ + integrationId: platform_integrations.id, + integrationStatus: platform_integrations.integration_status, + installationId: platform_integrations.platform_installation_id, + workspaceUuid: platform_integrations.platform_account_id, + workspaceSlug: platform_integrations.platform_account_login, + metadata: platform_integrations.metadata, + repositories: platform_integrations.repositories, + repositoriesSyncedAt: platform_integrations.repositories_synced_at, + authInvalidAt: platform_integrations.auth_invalid_at, + authInvalidReason: platform_integrations.auth_invalid_reason, + credentialId: platform_access_token_credentials.id, + credentialOrganizationId: platform_access_token_credentials.owned_by_organization_id, + credentialPlatform: platform_access_token_credentials.platform, + credentialIntegrationType: platform_access_token_credentials.integration_type, + providerCredentialType: platform_access_token_credentials.provider_credential_type, + providerScopes: platform_access_token_credentials.provider_scopes, + providerVerifiedAt: platform_access_token_credentials.provider_verified_at, + credentialVersion: platform_access_token_credentials.credential_version, + lastValidatedAt: platform_access_token_credentials.last_validated_at, + }) + .from(platform_integrations) + .leftJoin( + platform_access_token_credentials, + and( + eq(platform_access_token_credentials.platform_integration_id, platform_integrations.id), + eq(platform_access_token_credentials.owned_by_organization_id, organizationId), + eq(platform_access_token_credentials.platform, BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM), + eq( + platform_access_token_credentials.integration_type, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE + ) + ) + ) + .where( + and( + eq(platform_integrations.owned_by_organization_id, organizationId), + isNull(platform_integrations.owned_by_user_id), + eq(platform_integrations.platform, BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM), + eq( + platform_integrations.integration_type, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE + ) + ) + ) + .limit(1); + return row ?? null; +} + +type LoadedIntegration = NonNullable>>; + +function belongsToWorkspace(fullName: string, workspaceSlug: string): boolean { + const segments = fullName.split('/'); + return segments.length === 2 && segments[0] === workspaceSlug && segments[1].length > 0; +} + +function repositoriesHaveUniqueIdentity( + repositories: Array<{ id: string; fullName: string }> +): boolean { + return ( + new Set(repositories.map(repository => repository.id)).size === repositories.length && + new Set(repositories.map(repository => repository.fullName)).size === repositories.length + ); +} + +function parseCachedRepositories( + repositoriesValue: unknown, + repositoriesSyncedAt: string | null, + workspace: WorkspaceIdentity +): AvailableRepositoryCache | null { + if (repositoriesValue === null || repositoriesSyncedAt === null) return null; + const repositories = z.array(CachedRepositorySchema).safeParse(repositoriesValue); + if (!repositories.success) return null; + + const projected = repositories.data.map(repository => ({ + id: repository.id, + workspaceUuid: workspace.uuid, + name: repository.name, + fullName: repository.full_name, + private: repository.private, + defaultBranch: repository.default_branch, + })); + if ( + projected.some(repository => !belongsToWorkspace(repository.fullName, workspace.slug)) || + !repositoriesHaveUniqueIdentity(projected) + ) { + return null; + } + + return { + status: 'available', + repositories: projected, + syncedAt: new Date(repositoriesSyncedAt).toISOString(), + }; +} + +function toIsoTimestamp(value: string | null): string | null { + if (value === null) return null; + const parsed = new Date(value); + return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : null; +} + +function parseIntegration(row: LoadedIntegration, organizationId: string) { + const metadata = BitbucketWorkspaceAccessTokenMetadataSchema.safeParse(row.metadata); + const workspaceUuid = z.uuid().safeParse(row.workspaceUuid); + const workspaceSlug = WorkspaceSlugSchema.safeParse(row.workspaceSlug); + const workspaceIdentity: WorkspaceIdentity | null = + workspaceUuid.success && workspaceSlug.success + ? { uuid: workspaceUuid.data, slug: workspaceSlug.data } + : null; + const workspace = + metadata.success && workspaceIdentity + ? { ...workspaceIdentity, displayName: metadata.data.displayName } + : null; + const cache = workspace + ? parseCachedRepositories(row.repositories, row.repositoriesSyncedAt, workspace) + : null; + const credential = + row.credentialId !== null && + row.credentialOrganizationId === organizationId && + row.credentialPlatform === BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM && + row.credentialIntegrationType === BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE && + row.credentialVersion !== null && + row.credentialVersion > 0 + ? { id: row.credentialId, version: row.credentialVersion } + : null; + const hasValidCredentialEvidence = + credential !== null && + row.providerCredentialType === BITBUCKET_WORKSPACE_ACCESS_TOKEN_PROVIDER_CREDENTIAL_TYPE && + row.providerScopes !== null && + hasRequiredBitbucketWorkspaceAccessTokenScopes(row.providerScopes) && + toIsoTimestamp(row.providerVerifiedAt) !== null && + toIsoTimestamp(row.lastValidatedAt) !== null; + const usable = + workspace !== null && + row.installationId === null && + hasValidCredentialEvidence && + row.integrationStatus === INTEGRATION_STATUS.ACTIVE && + row.authInvalidAt === null; + const rotatable = + row.integrationStatus === INTEGRATION_STATUS.ACTIVE && + row.installationId === null && + workspaceIdentity !== null && + credential !== null && + credential.version < 2_147_483_647; + + return { + row, + workspaceIdentity, + workspace, + cache, + credential, + state: usable ? ('usable' as const) : ('reconnect_required' as const), + recoveryAction: usable + ? null + : rotatable + ? ('replace_token' as const) + : ('disconnect_and_connect' as const), + }; +} + +async function loadParsedIntegration(organizationId: string) { + const row = await loadIntegration(organizationId); + return row ? parseIntegration(row, organizationId) : null; +} + +function notConnectedStatus() { + return { + status: 'not_connected' as const, + recoveryAction: null, + method: BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE, + integrationId: null, + integrationStatus: null, + workspace: null, + invalidatedAt: null, + invalidationReason: null, + lastValidatedAt: null, + repositoryCache: { + status: 'uninitialized' as const, + repositories: [], + syncedAt: null, + }, + }; +} + +export async function getBitbucketWorkspaceAccessTokenStatus(organizationId: string) { + const canonicalOrganizationId = canonicalizeUuid(organizationId); + if (!canonicalOrganizationId) return notConnectedStatus(); + const integration = await loadParsedIntegration(canonicalOrganizationId); + if (!integration) return notConnectedStatus(); + + const invalidationReason = InvalidationReasonSchema.safeParse(integration.row.authInvalidReason); + const statusDetails = { + method: BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE, + integrationId: integration.row.integrationId, + integrationStatus: integration.row.integrationStatus, + workspace: integration.workspace, + invalidatedAt: toIsoTimestamp(integration.row.authInvalidAt), + invalidationReason: invalidationReason.success ? invalidationReason.data : null, + lastValidatedAt: toIsoTimestamp(integration.row.lastValidatedAt), + repositoryCache: integration.cache ?? { + status: 'uninitialized' as const, + repositories: [], + syncedAt: null, + }, + }; + if (integration.state === 'usable') { + return { status: 'connected' as const, recoveryAction: null, ...statusDetails }; + } + return { + status: 'reconnect_required' as const, + recoveryAction: integration.recoveryAction, + ...statusDetails, + }; +} + +export async function readCachedBitbucketWorkspaceAccessTokenRepositories({ + organizationId, + expectedIntegrationId, +}: ReadCachedRepositoriesInput): Promise { + const canonicalOrganizationId = canonicalizeUuid(organizationId); + const canonicalExpectedIntegrationId = expectedIntegrationId + ? canonicalizeUuid(expectedIntegrationId) + : undefined; + if (!canonicalOrganizationId || (expectedIntegrationId && !canonicalExpectedIntegrationId)) { + return { status: 'invalid_request' }; + } + + const integration = await loadParsedIntegration(canonicalOrganizationId); + if (!integration) return { status: 'not_connected' }; + if ( + canonicalExpectedIntegrationId && + integration.row.integrationId !== canonicalExpectedIntegrationId + ) { + return { status: 'invalid_request' }; + } + if (integration.state === 'reconnect_required') { + return { status: 'reconnect_required' }; + } + return integration.cache ?? { status: 'temporarily_unavailable' }; +} + +async function isObservedCredentialGenerationCurrent( + tx: DrizzleTransaction, + organizationId: string, + observed: { integrationId: string; credentialId: string; credentialVersion: number } +): Promise { + const [current] = await tx + .select({ + integrationId: platform_integrations.id, + credentialId: platform_access_token_credentials.id, + credentialVersion: platform_access_token_credentials.credential_version, + }) + .from(platform_integrations) + .innerJoin( + platform_access_token_credentials, + and( + eq(platform_access_token_credentials.platform_integration_id, platform_integrations.id), + eq(platform_access_token_credentials.owned_by_organization_id, organizationId), + eq(platform_access_token_credentials.platform, BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM), + eq( + platform_access_token_credentials.integration_type, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE + ) + ) + ) + .where( + and( + eq(platform_integrations.owned_by_organization_id, organizationId), + isNull(platform_integrations.owned_by_user_id), + eq(platform_integrations.platform, BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM), + eq( + platform_integrations.integration_type, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE + ) + ) + ); + return ( + current?.integrationId === observed.integrationId && + current.credentialId === observed.credentialId && + current.credentialVersion === observed.credentialVersion + ); +} + +export async function refreshBitbucketWorkspaceAccessTokenRepositories({ + organizationId, + kiloUserId, + expectedIntegrationId, +}: RefreshRepositoriesInput): Promise { + const canonicalOrganizationId = canonicalizeUuid(organizationId); + const canonicalExpectedIntegrationId = canonicalizeUuid(expectedIntegrationId); + if (!canonicalOrganizationId || !canonicalExpectedIntegrationId) { + return { status: 'invalid_request' }; + } + + const integration = await loadParsedIntegration(canonicalOrganizationId); + if (!integration) return { status: 'not_connected' }; + if (integration.row.integrationId !== canonicalExpectedIntegrationId) { + return { status: 'invalid_request' }; + } + if ( + integration.state === 'reconnect_required' || + !integration.workspaceIdentity || + !integration.credential + ) { + return { status: 'reconnect_required' }; + } + const workspaceIdentity = integration.workspaceIdentity; + const credential = integration.credential; + + const providerResult = await fetchBitbucketWorkspaceAccessTokenRepositoriesFromTokenService( + kiloUserId, + canonicalOrganizationId + ); + const stillCurrent = await db.transaction(async tx => { + await lockBitbucketWorkspaceAccessTokenOrganization(tx, canonicalOrganizationId); + return isObservedCredentialGenerationCurrent(tx, canonicalOrganizationId, { + integrationId: integration.row.integrationId, + credentialId: credential.id, + credentialVersion: credential.version, + }); + }); + if (!stillCurrent) { + return readCachedBitbucketWorkspaceAccessTokenRepositories({ + organizationId: canonicalOrganizationId, + }); + } + if (providerResult.status !== 'available') return providerResult; + if ( + providerResult.repositories.some( + repository => + repository.workspaceUuid !== workspaceIdentity.uuid || + !belongsToWorkspace(repository.fullName, workspaceIdentity.slug) + ) || + !repositoriesHaveUniqueIdentity(providerResult.repositories) + ) { + return { status: 'invalid_request' }; + } + + const repositories = providerResult.repositories.map(repository => ({ + id: repository.id, + name: repository.name, + full_name: repository.fullName, + private: repository.private, + default_branch: repository.defaultBranch, + })); + const previousSyncedAtMs = integration.row.repositoriesSyncedAt + ? new Date(integration.row.repositoriesSyncedAt).getTime() + : 0; + const syncedAt = new Date( + Math.max(Date.now(), Number.isFinite(previousSyncedAtMs) ? previousSyncedAtMs + 1 : 0) + ).toISOString(); + const previousCacheCondition = integration.row.repositoriesSyncedAt + ? eq(platform_integrations.repositories_synced_at, integration.row.repositoriesSyncedAt) + : isNull(platform_integrations.repositories_synced_at); + + let updated: boolean; + try { + updated = await db.transaction(async tx => { + await lockBitbucketWorkspaceAccessTokenOrganization(tx, canonicalOrganizationId); + await requireBitbucketWorkspaceAccessTokenOrganizationManager( + tx, + canonicalOrganizationId, + kiloUserId + ); + const currentCredential = tx + .select({ id: platform_access_token_credentials.id }) + .from(platform_access_token_credentials) + .where( + and( + eq(platform_access_token_credentials.id, credential.id), + eq( + platform_access_token_credentials.platform_integration_id, + integration.row.integrationId + ), + eq(platform_access_token_credentials.owned_by_organization_id, canonicalOrganizationId), + eq(platform_access_token_credentials.credential_version, credential.version) + ) + ); + const [updatedRow] = await tx + .update(platform_integrations) + .set({ + repositories, + repositories_synced_at: syncedAt, + updated_at: syncedAt, + }) + .where( + and( + eq(platform_integrations.id, integration.row.integrationId), + eq(platform_integrations.owned_by_organization_id, canonicalOrganizationId), + isNull(platform_integrations.owned_by_user_id), + eq(platform_integrations.platform, BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM), + eq( + platform_integrations.integration_type, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE + ), + eq(platform_integrations.integration_status, INTEGRATION_STATUS.ACTIVE), + isNull(platform_integrations.auth_invalid_at), + isNull(platform_integrations.platform_installation_id), + eq(platform_integrations.platform_account_id, workspaceIdentity.uuid), + eq(platform_integrations.platform_account_login, workspaceIdentity.slug), + previousCacheCondition, + exists(currentCredential) + ) + ) + .returning({ id: platform_integrations.id }); + return Boolean(updatedRow); + }); + } catch (error) { + if (error instanceof BitbucketWorkspaceAccessTokenOrganizationAuthorizationError) { + throw new BitbucketWorkspaceAccessTokenRepositoryCacheAuthorizationError(); + } + throw error; + } + + if (!updated) { + return readCachedBitbucketWorkspaceAccessTokenRepositories({ + organizationId: canonicalOrganizationId, + }); + } + return { ...providerResult, syncedAt }; +} 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 5237df8cdb..515bf918d3 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 @@ -1,5 +1,8 @@ import { NextResponse } from 'next/server'; -import type { PlatformRepository } from '@/lib/integrations/core/types'; +import { + requireNumericPlatformRepositories, + type PlatformRepository, +} from '@/lib/integrations/core/types'; import { findIntegrationByInstallationId, updateIntegrationRepositories, @@ -27,7 +30,7 @@ export async function handleInstallationRepositories(payload: InstallationReposi } // Get current repositories - const currentRepos = integration.repositories || []; + const currentRepos = requireNumericPlatformRepositories(integration.repositories) ?? []; let updatedRepos: PlatformRepository[] = currentRepos; if (action === GITHUB_ACTION.ADDED && repositories_added) { 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 ffd238c210..b8c476ad99 100644 --- a/apps/web/src/lib/integrations/validate-return-path.test.ts +++ b/apps/web/src/lib/integrations/validate-return-path.test.ts @@ -31,6 +31,14 @@ describe('validateReturnPath', () => { expect(validateReturnPath('/foo\nbar')).toBeNull(); }); + it('rejects paths with tabs that URL parsing would treat as an external redirect', () => { + expect(validateReturnPath('/\t/evil.example/path')).toBeNull(); + }); + + it('rejects paths that normalize to a protocol-relative URL', () => { + expect(validateReturnPath('/..//evil.example/path')).toBeNull(); + }); + it('rejects paths without leading slash', () => { expect(validateReturnPath('foo/bar')).toBeNull(); }); diff --git a/apps/web/src/lib/integrations/validate-return-path.ts b/apps/web/src/lib/integrations/validate-return-path.ts index f426648510..91af1c5d37 100644 --- a/apps/web/src/lib/integrations/validate-return-path.ts +++ b/apps/web/src/lib/integrations/validate-return-path.ts @@ -1,10 +1,23 @@ -const RETURN_PATH_RE = /^\/(?![/\\])[^\r\n]*$/; +const RETURN_PATH_BASE = 'https://return-path.invalid'; + +function containsUnsafeReturnPathCharacter(candidate: string): boolean { + return [...candidate].some(character => { + const codePoint = character.charCodeAt(0); + return character === '\\' || codePoint <= 0x1f || codePoint === 0x7f; + }); +} export function validateReturnPath(candidate: string): string | null { - if (!RETURN_PATH_RE.test(candidate) || candidate.startsWith('//')) { + if (!candidate.startsWith('/') || containsUnsafeReturnPathCharacter(candidate)) return null; + + try { + const resolved = new URL(candidate, RETURN_PATH_BASE); + const normalizedPath = `${resolved.pathname}${resolved.search}${resolved.hash}`; + if (resolved.origin !== RETURN_PATH_BASE || normalizedPath.startsWith('//')) return null; + return normalizedPath; + } catch { return null; } - return candidate; } export function parseStateReturn(rawState: string | null): { diff --git a/apps/web/src/lib/security-agent/router/shared-handlers.ts b/apps/web/src/lib/security-agent/router/shared-handlers.ts index 7ccb758316..1bbb0cfe10 100644 --- a/apps/web/src/lib/security-agent/router/shared-handlers.ts +++ b/apps/web/src/lib/security-agent/router/shared-handlers.ts @@ -5,6 +5,7 @@ import { updateRepositoriesForIntegration, } from '@/lib/integrations/db/platform-integrations'; import { fetchGitHubRepositories } from '@/lib/integrations/platforms/github/adapter'; +import { requireNumericPlatformRepositories } from '@/lib/integrations/core/types'; import { getSecurityAgentConfigWithStatus, upsertSecurityAgentConfig, @@ -140,7 +141,7 @@ function getRepoFullNamesInScope( integration: Integration, config: { repository_selection_mode?: 'all' | 'selected'; selected_repository_ids?: number[] } ): string[] { - const repositories = integration?.repositories ?? []; + const repositories = requireNumericPlatformRepositories(integration?.repositories ?? null) ?? []; if (config.repository_selection_mode === 'all') { return repositories.map(repo => repo.full_name).filter((name): name is string => !!name); } @@ -729,7 +730,7 @@ export function createSecurityAgentHandlers(deps: SecurityAgentDeps if (input.isEnabled && integration) { const installationId = integration.platform_installation_id; if (installationId) { - const allRepos = integration.repositories || []; + const allRepos = requireNumericPlatformRepositories(integration.repositories) ?? []; let repositoriesToSync: string[]; @@ -834,7 +835,7 @@ export function createSecurityAgentHandlers(deps: SecurityAgentDeps } // Auto-fetch repositories from GitHub if not cached - let repos = integration.repositories || []; + let repos = requireNumericPlatformRepositories(integration.repositories) ?? []; if (repos.length === 0 && integration.platform_installation_id) { const appType = integration.github_app_type || 'standard'; const fetchedRepos = await fetchGitHubRepositories( @@ -1020,7 +1021,7 @@ export function createSecurityAgentHandlers(deps: SecurityAgentDeps }); } - const allRepos = integration.repositories || []; + const allRepos = requireNumericPlatformRepositories(integration.repositories) ?? []; // If a specific repo is provided, sync only that one if (input.repoFullName) { @@ -1521,7 +1522,7 @@ export function createSecurityAgentHandlers(deps: SecurityAgentDeps // Get list of accessible repository full names const accessibleRepoFullNames: string[] = []; if (integration && integration.integration_status === 'active') { - const repos = integration.repositories || []; + const repos = requireNumericPlatformRepositories(integration.repositories) ?? []; for (const repo of repos) { if (repo.full_name) { accessibleRepoFullNames.push(repo.full_name); diff --git a/apps/web/src/lib/slack-bot/github-repository-context.ts b/apps/web/src/lib/slack-bot/github-repository-context.ts index 1d59ceb733..cb0305dd30 100644 --- a/apps/web/src/lib/slack-bot/github-repository-context.ts +++ b/apps/web/src/lib/slack-bot/github-repository-context.ts @@ -1,4 +1,8 @@ -import type { Owner, PlatformRepository } from '@/lib/integrations/core/types'; +import { + requireNumericPlatformRepositories, + type Owner, + type PlatformRepository, +} from '@/lib/integrations/core/types'; import { PLATFORM } from '@/lib/integrations/core/constants'; import { getIntegrationForOwner } from '@/lib/integrations/db/platform-integrations'; @@ -24,7 +28,7 @@ export async function getGitHubRepositoryContext(owner: Owner): Promise { + describe('generateInternalServiceToken', () => { + it('generates a generic service token without an audience', () => { + const token = generateInternalServiceToken(mockUser.id); + const decoded = jwt.decode(token) as jwt.JwtPayload; + + expect(decoded.kiloUserId).toBe(mockUser.id); + expect(decoded.aud).toBeUndefined(); + }); + }); + describe('generateApiToken', () => { it('should generate a valid JWT token for a user', () => { const token = generateApiToken(mockUser); diff --git a/apps/web/src/lib/tokens.ts b/apps/web/src/lib/tokens.ts index 5eada683aa..e38c1f847e 100644 --- a/apps/web/src/lib/tokens.ts +++ b/apps/web/src/lib/tokens.ts @@ -4,7 +4,10 @@ import jwt from 'jsonwebtoken'; import { warnExceptInTest } from '@/lib/utils.server'; import { NEXTAUTH_SECRET } from '@/lib/config.server'; +export { BITBUCKET_REPOSITORY_LIST_AUDIENCE } from '@kilocode/worker-utils/internal-service-token-audiences'; + export const JWT_TOKEN_VERSION = 3; + const jwtSigningAlgorithm = 'HS256'; export type JWTTokenExtraPayload = { @@ -39,12 +42,21 @@ export const TOKEN_EXPIRY = { */ export function generateInternalServiceToken( userId: string, - options?: { expiresIn?: number } + options?: { expiresIn?: number; audience?: string; organizationId?: string } ): string { - return jwt.sign({ kiloUserId: userId, version: JWT_TOKEN_VERSION }, NEXTAUTH_SECRET, { - algorithm: jwtSigningAlgorithm, - expiresIn: options?.expiresIn ?? ONE_HOUR_IN_SECONDS, - }); + return jwt.sign( + { + kiloUserId: userId, + version: JWT_TOKEN_VERSION, + ...(options?.organizationId ? { organizationId: options.organizationId } : {}), + }, + NEXTAUTH_SECRET, + { + algorithm: jwtSigningAlgorithm, + expiresIn: options?.expiresIn ?? ONE_HOUR_IN_SECONDS, + ...(options?.audience ? { audience: options.audience } : {}), + } + ); } export function generateApiToken( diff --git a/apps/web/src/lib/user/index.test.ts b/apps/web/src/lib/user/index.test.ts index d4f176619e..64876ace42 100644 --- a/apps/web/src/lib/user/index.test.ts +++ b/apps/web/src/lib/user/index.test.ts @@ -76,6 +76,9 @@ import { code_review_feedback_events, code_review_memory_proposals, user_github_app_tokens, + platform_oauth_credentials, + platform_access_token_credentials, + platform_integrations, model_eval_ingestions, microdollar_usage, model_experiment, @@ -243,6 +246,9 @@ describe('User', () => { await db.delete(code_review_memory_proposals); await db.delete(code_review_feedback_events); await db.delete(user_github_app_tokens); + await db.delete(platform_oauth_credentials); + await db.delete(platform_access_token_credentials); + await db.delete(platform_integrations); await db.delete(organizations); await db.delete(kilocode_users); }); @@ -524,6 +530,200 @@ describe('User', () => { expect(retainedDismissal?.dismissed_by_user_id).toBeNull(); }); + it('deletes personal integrations and every OAuth credential authorized by the user', async () => { + const user = await insertTestUser(); + const otherUser = await insertTestUser(); + const organization = await createTestOrganization( + 'OAuth Credential Cleanup Org', + otherUser.id, + 0 + ); + const [integration, otherIntegration, organizationIntegration] = await db + .insert(platform_integrations) + .values([ + { + owned_by_user_id: user.id, + created_by_user_id: user.id, + platform: 'bitbucket', + integration_type: 'oauth', + platform_installation_id: '{workspace-user}', + platform_account_id: '{workspace-user}', + platform_account_login: 'user-workspace', + integration_status: 'active', + }, + { + owned_by_user_id: otherUser.id, + created_by_user_id: otherUser.id, + platform: 'bitbucket', + integration_type: 'oauth', + platform_installation_id: '{workspace-other}', + platform_account_id: '{workspace-other}', + platform_account_login: 'other-workspace', + integration_status: 'active', + }, + { + owned_by_organization_id: organization.id, + created_by_user_id: user.id, + platform: 'bitbucket', + integration_type: 'oauth', + platform_installation_id: '{workspace-organization}', + platform_account_id: '{workspace-organization}', + platform_account_login: 'organization-workspace', + integration_status: 'active', + }, + ]) + .returning(); + if (!integration || !otherIntegration || !organizationIntegration) { + throw new Error('Failed to create Bitbucket integrations'); + } + + const [credential, otherCredential, organizationCredential] = await db + .insert(platform_oauth_credentials) + .values([ + { + platform_integration_id: integration.id, + platform: 'bitbucket', + authorized_by_user_id: user.id, + provider_subject_id: '{bitbucket-user}', + provider_subject_login: 'bitbucket-user', + access_token_encrypted: 'encrypted-access-token', + access_token_expires_at: '2026-06-22T14:00:00.000Z', + refresh_token_encrypted: 'encrypted-refresh-token', + }, + { + platform_integration_id: otherIntegration.id, + platform: 'bitbucket', + authorized_by_user_id: otherUser.id, + provider_subject_id: '{bitbucket-other-user}', + provider_subject_login: 'bitbucket-other-user', + access_token_encrypted: 'other-encrypted-access-token', + access_token_expires_at: '2026-06-22T14:00:00.000Z', + refresh_token_encrypted: 'other-encrypted-refresh-token', + }, + { + platform_integration_id: organizationIntegration.id, + platform: 'bitbucket', + authorized_by_user_id: user.id, + provider_subject_id: '{bitbucket-organization-authorizer}', + provider_subject_login: 'bitbucket-organization-authorizer', + access_token_encrypted: 'organization-encrypted-access-token', + access_token_expires_at: '2026-06-22T14:00:00.000Z', + refresh_token_encrypted: 'organization-encrypted-refresh-token', + }, + ]) + .returning(); + if (!credential || !otherCredential || !organizationCredential) { + throw new Error('Failed to create Bitbucket OAuth credentials'); + } + + await softDeleteUser(user.id); + + expect( + await db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.id, credential.id)) + ).toHaveLength(0); + expect( + await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)) + ).toHaveLength(0); + expect( + await db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.id, organizationCredential.id)) + ).toHaveLength(0); + expect( + await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, organizationIntegration.id)) + ).toEqual([ + expect.objectContaining({ + integration_status: 'suspended', + auth_invalid_reason: 'authorizing_user_deleted', + }), + ]); + expect( + await db + .select() + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.id, otherCredential.id)) + ).toHaveLength(1); + expect( + await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, otherIntegration.id)) + ).toHaveLength(1); + }); + + it('preserves an organization Workspace Access Token when its setup actor is deleted', async () => { + const setupActor = await insertTestUser(); + const organization = await createTestOrganization( + 'Workspace Access Token Setup Actor Org', + setupActor.id, + 0 + ); + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_organization_id: organization.id, + created_by_user_id: setupActor.id, + platform: 'bitbucket', + integration_type: 'workspace_access_token', + platform_account_id: '{workspace-organization}', + platform_account_login: 'organization-workspace', + repository_access: 'all', + integration_status: 'active', + }) + .returning(); + if (!integration) { + throw new Error('Failed to create Bitbucket Workspace Access Token integration'); + } + const [credential] = await db + .insert(platform_access_token_credentials) + .values({ + platform_integration_id: integration.id, + owned_by_organization_id: organization.id, + platform: 'bitbucket', + integration_type: 'workspace_access_token', + token_encrypted: 'encrypted-workspace-access-token', + provider_credential_type: 'workspace_access_token', + provider_scopes: ['account', 'repository', 'repository:write'], + provider_verified_at: '2026-06-24T10:00:00.000Z', + last_validated_at: '2026-06-24T10:00:00.000Z', + }) + .returning(); + if (!credential) { + throw new Error('Failed to create Bitbucket Workspace Access Token credential'); + } + + await softDeleteUser(setupActor.id); + + expect( + await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)) + ).toEqual([ + expect.objectContaining({ + integration_status: 'active', + auth_invalid_at: null, + auth_invalid_reason: null, + }), + ]); + expect( + await db + .select() + .from(platform_access_token_credentials) + .where(eq(platform_access_token_credentials.id, credential.id)) + ).toHaveLength(1); + }); + it('deletes personal Security Agent notifications through finding cleanup', async () => { const user = await insertTestUser(); const [finding] = await db diff --git a/apps/web/src/lib/user/index.ts b/apps/web/src/lib/user/index.ts index 19483d3bbb..0beb9815da 100644 --- a/apps/web/src/lib/user/index.ts +++ b/apps/web/src/lib/user/index.ts @@ -37,6 +37,7 @@ import { device_auth_requests, auto_top_up_configs, platform_integrations, + platform_oauth_credentials, byok_api_keys, agent_configs, webhook_events, @@ -858,6 +859,8 @@ export class SoftDeletePreconditionError extends Error { * - deployments_ephemeral ownership link and cleanup claims (FK nulled; * immediate cleanup scheduled) * - Recommendation dismissal actor references (nulled) + * - platform_oauth_credentials (encrypted OAuth tokens and provider identity; + * authorizations created by the user are removed, including organization grants) * - Various user-owned resources (platform_integrations, byok_api_keys, * agent_configs, webhook_events, code_indexing_*, source_embeddings, * cloud_agent_webhook_triggers, agent_environment_profiles, @@ -1085,6 +1088,21 @@ export async function softDeleteUser(userId: string) { .delete(agent_environment_profiles) .where(eq(agent_environment_profiles.owned_by_user_id, userId)); + const authorizedOAuthIntegrationIds = tx + .select({ id: platform_oauth_credentials.platform_integration_id }) + .from(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.authorized_by_user_id, userId)); + await tx + .update(platform_integrations) + .set({ + integration_status: 'suspended', + auth_invalid_at: new Date().toISOString(), + auth_invalid_reason: 'authorizing_user_deleted', + }) + .where(inArray(platform_integrations.id, authorizedOAuthIntegrationIds)); + await tx + .delete(platform_oauth_credentials) + .where(eq(platform_oauth_credentials.authorized_by_user_id, userId)); await tx .delete(platform_integrations) .where(eq(platform_integrations.owned_by_user_id, userId)); diff --git a/apps/web/src/routers/bitbucket-router.test.ts b/apps/web/src/routers/bitbucket-router.test.ts new file mode 100644 index 0000000000..ebba6fc2cb --- /dev/null +++ b/apps/web/src/routers/bitbucket-router.test.ts @@ -0,0 +1,196 @@ +/* eslint-disable drizzle/enforce-delete-with-where */ +import { afterAll, afterEach, beforeAll, describe, expect, it, jest } from '@jest/globals'; +import type { Organization, User } from '@kilocode/db/schema'; +import { + kilocode_users, + organization_memberships, + organizations, + platform_integrations, + platform_oauth_credentials, +} from '@kilocode/db/schema'; +import { db } from '@/lib/drizzle'; +import type * as BitbucketRepositoryCacheModule from '@/lib/integrations/platforms/bitbucket/repository-cache'; +import { createTestOrganization } from '@/tests/helpers/organization.helper'; +import { insertTestUser } from '@/tests/helpers/user.helper'; + +const mockScheduleBitbucketRepositoryCachePrime = + jest.fn< + (input: { + owner: { type: 'user' | 'org'; id: string }; + kiloUserId: string; + integrationId: string; + }) => void + >(); + +jest.mock('@/lib/integrations/platforms/bitbucket/repository-cache', () => ({ + ...jest.requireActual( + '@/lib/integrations/platforms/bitbucket/repository-cache' + ), + scheduleBitbucketRepositoryCachePrime: mockScheduleBitbucketRepositoryCachePrime, +})); + +type DirectBitbucketCaller = { + bitbucket: { + getInstallation(input?: { organizationId?: string }): Promise; + selectWorkspace(input: { + organizationId?: string; + workspaceUuid: string; + workspaceSlug: string; + }): Promise; + disconnect(input?: { organizationId?: string }): Promise; + }; +}; + +let createCallerForUser: (userId: string) => Promise; + +async function insertPendingIntegration(organizationId: string, authorizedByUserId: string) { + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_organization_id: organizationId, + created_by_user_id: authorizedByUserId, + platform: 'bitbucket', + integration_type: 'oauth', + scopes: ['account', 'repository', 'repository:write', 'webhook'], + repository_access: 'all', + integration_status: 'pending', + metadata: { + state: 'workspace_selection_required', + availableWorkspaces: [ + { + uuid: '123e4567-e89b-12d3-a456-426614174020', + slug: 'acme', + name: 'Acme', + }, + { + uuid: '123e4567-e89b-12d3-a456-426614174021', + slug: 'example', + name: 'Example', + }, + ], + }, + }) + .returning(); + if (!integration) throw new Error('Expected Bitbucket integration'); + await db.insert(platform_oauth_credentials).values({ + platform_integration_id: integration.id, + platform: 'bitbucket', + authorized_by_user_id: authorizedByUserId, + provider_subject_id: '123e4567-e89b-12d3-a456-426614174010', + provider_subject_login: 'bucket-admin', + access_token_encrypted: 'access-envelope', + access_token_expires_at: '2030-01-01T00:00:00.000Z', + refresh_token_encrypted: 'refresh-envelope', + }); + return integration; +} + +describe('bitbucketRouter organization ownership', () => { + let owner: User; + let billingManager: User; + let member: User; + let organization: Organization; + + beforeAll(async () => { + const [{ bitbucketRouter }, { createCallerFactory }, { findUserById }] = await Promise.all([ + import('./bitbucket-router'), + import('@/lib/trpc/init'), + import('@/lib/user'), + ]); + const createDirectCaller = createCallerFactory(bitbucketRouter); + createCallerForUser = async userId => { + const user = await findUserById(userId); + if (!user) throw new Error(`Test user not found: ${userId}`); + return { bitbucket: createDirectCaller({ user }) }; + }; + owner = await insertTestUser(); + billingManager = await insertTestUser(); + member = await insertTestUser(); + organization = await createTestOrganization('Bitbucket Router Org', owner.id, 0); + await db.insert(organization_memberships).values([ + { + organization_id: organization.id, + kilo_user_id: billingManager.id, + role: 'billing_manager', + }, + { organization_id: organization.id, kilo_user_id: member.id, role: 'member' }, + ]); + }); + + afterEach(async () => { + jest.clearAllMocks(); + await db.delete(platform_oauth_credentials); + await db.delete(platform_integrations); + }); + + afterAll(async () => { + await db.delete(organizations); + await db.delete(kilocode_users); + }); + + it('lets members see pending status without exposing the authorizer or workspace candidates', async () => { + await insertPendingIntegration(organization.id, owner.id); + + const ownerCaller = await createCallerForUser(owner.id); + const memberCaller = await createCallerForUser(member.id); + + await expect( + ownerCaller.bitbucket.getInstallation({ organizationId: organization.id }) + ).resolves.toMatchObject({ + status: 'workspace_selection_required', + authorizingNickname: 'bucket-admin', + availableWorkspaces: expect.arrayContaining([expect.objectContaining({ slug: 'acme' })]), + canManage: true, + }); + await expect( + memberCaller.bitbucket.getInstallation({ organizationId: organization.id }) + ).resolves.toEqual({ status: 'workspace_selection_required', canManage: false }); + }); + + it('rejects workspace selection by an ordinary member', async () => { + await insertPendingIntegration(organization.id, owner.id); + const caller = await createCallerForUser(member.id); + + await expect( + caller.bitbucket.selectWorkspace({ + organizationId: organization.id, + workspaceUuid: '123e4567-e89b-12d3-a456-426614174020', + workspaceSlug: 'acme', + }) + ).rejects.toMatchObject({ code: 'UNAUTHORIZED' }); + }); + + it('allows a billing manager to activate the organization workspace and primes its repository cache', async () => { + const integration = await insertPendingIntegration(organization.id, owner.id); + const caller = await createCallerForUser(billingManager.id); + + await expect( + caller.bitbucket.selectWorkspace({ + organizationId: organization.id, + workspaceUuid: '123e4567-e89b-12d3-a456-426614174020', + workspaceSlug: 'acme', + }) + ).resolves.toMatchObject({ success: true, workspace: { slug: 'acme' } }); + await expect( + caller.bitbucket.getInstallation({ organizationId: organization.id }) + ).resolves.toMatchObject({ status: 'connected', canManage: true }); + expect(mockScheduleBitbucketRepositoryCachePrime).toHaveBeenCalledWith({ + owner: { type: 'org', id: organization.id }, + kiloUserId: billingManager.id, + integrationId: integration.id, + }); + }); + + it('rejects disconnect by an ordinary member and allows the owner', async () => { + await insertPendingIntegration(organization.id, owner.id); + const memberCaller = await createCallerForUser(member.id); + const ownerCaller = await createCallerForUser(owner.id); + + await expect( + memberCaller.bitbucket.disconnect({ organizationId: organization.id }) + ).rejects.toMatchObject({ code: 'UNAUTHORIZED' }); + await expect( + ownerCaller.bitbucket.disconnect({ organizationId: organization.id }) + ).resolves.toEqual({ success: true }); + }); +}); diff --git a/apps/web/src/routers/bitbucket-router.ts b/apps/web/src/routers/bitbucket-router.ts new file mode 100644 index 0000000000..4b51a4fc93 --- /dev/null +++ b/apps/web/src/routers/bitbucket-router.ts @@ -0,0 +1,95 @@ +import 'server-only'; + +import { z } from 'zod'; +import { + disconnectBitbucketOAuthIntegration, + getBitbucketOAuthIntegrationStatus, + selectBitbucketOAuthWorkspace, +} from '@/lib/integrations/platforms/bitbucket/oauth-integration'; +import { + optionalOrgInput, + resolveAuthorizedOwner, + resolveOwner, +} from '@/lib/integrations/resolve-owner'; +import { baseProcedure, createTRPCRouter } from '@/lib/trpc/init'; +import { ensureOrganizationAccess } from '@/routers/organizations/utils'; + +const SelectWorkspaceInputSchema = z + .object({ + organizationId: z.uuid().optional(), + workspaceUuid: z.string().min(1), + workspaceSlug: z.string().min(1), + }) + .strict(); + +export const bitbucketRouter = createTRPCRouter({ + getInstallation: baseProcedure.input(optionalOrgInput).query(async ({ ctx, input }) => { + let canManage = true; + if (input?.organizationId) { + const role = await ensureOrganizationAccess(ctx, input.organizationId); + canManage = role === 'owner' || role === 'billing_manager'; + } + const owner = resolveOwner(ctx, input?.organizationId); + const status = await getBitbucketOAuthIntegrationStatus(owner, canManage); + if (!status) return { status: 'not_connected' as const, canManage }; + + if ( + status.status === 'reconnect_required' && + 'authorizingNickname' in status && + status.authorizingNickname + ) { + return { + status: 'reconnect_required' as const, + authorizingNickname: status.authorizingNickname, + canManage, + }; + } + if (status.status === 'reconnect_required') { + return { status: 'reconnect_required' as const, canManage }; + } + if (status.status === 'workspace_selection_required') { + if (!canManage) { + return { status: 'workspace_selection_required' as const, canManage: false as const }; + } + return { + status: 'workspace_selection_required' as const, + authorizingNickname: status.authorizingNickname, + availableWorkspaces: status.availableWorkspaces, + canManage: true as const, + }; + } + if (status.status === 'connected') { + return { + status: 'connected' as const, + authorizingNickname: status.authorizingNickname, + workspace: status.workspace + ? { + uuid: status.workspace.uuid, + slug: status.workspace.slug, + name: status.workspace.displayName, + } + : undefined, + canManage, + }; + } + return { status: 'reconnect_required' as const, canManage }; + }), + + selectWorkspace: baseProcedure + .input(SelectWorkspaceInputSchema) + .mutation(async ({ ctx, input }) => { + const owner = await resolveAuthorizedOwner(ctx, input.organizationId); + return selectBitbucketOAuthWorkspace({ + owner, + kiloUserId: ctx.user.id, + workspaceUuid: input.workspaceUuid, + workspaceSlug: input.workspaceSlug, + }); + }), + + disconnect: baseProcedure.input(optionalOrgInput).mutation(async ({ ctx, input }) => { + const owner = await resolveAuthorizedOwner(ctx, input?.organizationId); + await disconnectBitbucketOAuthIntegration({ owner }); + return { success: true }; + }), +}); diff --git a/apps/web/src/routers/cloud-agent-next-router.test.ts b/apps/web/src/routers/cloud-agent-next-router.test.ts index e978d5ebdd..fbb11293d2 100644 --- a/apps/web/src/routers/cloud-agent-next-router.test.ts +++ b/apps/web/src/routers/cloud-agent-next-router.test.ts @@ -79,7 +79,12 @@ let createCaller: (ctx: { user: User }) => { prompt: string; mode: string; model: string; - githubRepo: string; + githubRepo?: string; + bitbucketRepo?: { + fullName: string; + workspaceUuid: string; + repositoryUuid: string; + }; autoInitiate: boolean; devcontainer: boolean; images?: { path: string; files: string[] }; @@ -237,6 +242,29 @@ describe('cloudAgentNextRouter.prepareSession', () => { expect(mockPrepareSession).not.toHaveBeenCalledWith(expect.objectContaining({ images })); }); + it('rejects personal Bitbucket sessions before constructing a Cloud Agent client', async () => { + const caller = createCaller({ + user: { id: 'user-1', is_admin: false } as User, + }); + + await expect( + caller.prepareSession({ + prompt: 'Inspect the repository', + mode: 'code', + model: 'kilo/test-model', + bitbucketRepo: { + fullName: 'acme/api', + workspaceUuid: '11111111-1111-4111-8111-111111111111', + repositoryUuid: '22222222-2222-4222-8222-222222222222', + }, + autoInitiate: true, + devcontainer: false, + }) + ).rejects.toMatchObject({ code: 'BAD_REQUEST' }); + expect(mockCreateCloudAgentNextClient).not.toHaveBeenCalled(); + expect(mockPrepareSession).not.toHaveBeenCalled(); + }); + it('forwards devcontainer sessions when the feature flag is enabled', async () => { mockIsFeatureFlagEnabledOrDevelopment.mockResolvedValue(true); const caller = createCaller({ diff --git a/apps/web/src/routers/cloud-agent-next-router.ts b/apps/web/src/routers/cloud-agent-next-router.ts index 6964cff5b6..14744d90a9 100644 --- a/apps/web/src/routers/cloud-agent-next-router.ts +++ b/apps/web/src/routers/cloud-agent-next-router.ts @@ -14,7 +14,7 @@ import { fetchGitLabRepositoriesForUser, } from '@/lib/cloud-agent/gitlab-integration-helpers'; import { - basePrepareSessionNextSchema, + personalPrepareSessionNextSchema, basePrepareSessionNextOutputSchema, baseInitiateFromPreparedSessionNextSchema, baseInitiateSessionNextOutputSchema, @@ -119,7 +119,7 @@ export const cloudAgentNextRouter = createTRPCRouter({ * initiateFromPreparedSession. */ prepareSession: baseProcedure - .input(basePrepareSessionNextSchema) + .input(personalPrepareSessionNextSchema) .output(basePrepareSessionNextOutputSchema) .mutation(async ({ ctx, input }) => { if ( diff --git a/apps/web/src/routers/cloud-agent-next-schemas.test.ts b/apps/web/src/routers/cloud-agent-next-schemas.test.ts index 8f1a35d2a2..27b5146317 100644 --- a/apps/web/src/routers/cloud-agent-next-schemas.test.ts +++ b/apps/web/src/routers/cloud-agent-next-schemas.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from '@jest/globals'; import { basePrepareSessionNextSchema, baseSendMessageNextSchema, + personalPrepareSessionNextSchema, cloudAgentAttachmentsSchema, cloudAgentGetAttachmentUploadUrlSchema, } from './cloud-agent-next-schemas'; @@ -111,6 +112,23 @@ describe('basePrepareSessionNextSchema', () => { }); }); +describe('personalPrepareSessionNextSchema', () => { + it('rejects Bitbucket repository identity in personal context', () => { + expect( + personalPrepareSessionNextSchema.safeParse({ + bitbucketRepo: { + fullName: 'acme/api', + workspaceUuid: '11111111-1111-4111-8111-111111111111', + repositoryUuid: '22222222-2222-4222-8222-222222222222', + }, + prompt: 'Inspect the repository', + mode: 'code', + model: 'anthropic/claude-sonnet', + }).success + ).toBe(false); + }); +}); + describe('baseSendMessageNextSchema', () => { it('accepts canonical attachment references and rejects both attachment fields', () => { const send = { diff --git a/apps/web/src/routers/cloud-agent-next-schemas.ts b/apps/web/src/routers/cloud-agent-next-schemas.ts index dcfe919fdf..8477e60953 100644 --- a/apps/web/src/routers/cloud-agent-next-schemas.ts +++ b/apps/web/src/routers/cloud-agent-next-schemas.ts @@ -240,6 +240,16 @@ export const basePrepareSessionNextSchema = z ) .optional() .describe('GitLab project path (e.g., group/project or group/subgroup/project)'), + bitbucketRepo: z + .object({ + fullName: z + .string() + .regex(/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/, 'Invalid Bitbucket repository'), + workspaceUuid: z.uuid(), + repositoryUuid: z.uuid(), + }) + .strict() + .optional(), // Execution params (required) prompt: z.string().min(1).max(100_000), @@ -273,9 +283,12 @@ export const basePrepareSessionNextSchema = z devcontainer: z.boolean().optional(), }) .refine( - data => (data.githubRepo || data.gitlabProject) && !(data.githubRepo && data.gitlabProject), + data => + [data.githubRepo, data.gitlabProject, data.bitbucketRepo].filter( + repository => repository !== undefined + ).length === 1, { - message: 'Must provide either githubRepo or gitlabProject, but not both', + message: 'Must provide exactly one repository source', path: ['githubRepo'], } ) @@ -284,6 +297,14 @@ export const basePrepareSessionNextSchema = z path: ['attachments'], }); +export const personalPrepareSessionNextSchema = basePrepareSessionNextSchema.refine( + data => data.bitbucketRepo === undefined, + { + message: 'Bitbucket repositories require an organization', + path: ['bitbucketRepo'], + } +); + // Output schema for prepareSession export const basePrepareSessionNextOutputSchema = z.object({ kiloSessionId: z.string().startsWith('ses_').length(30), @@ -416,7 +437,9 @@ export const baseGetSessionNextOutputSchema = z.object({ // Repository info (no tokens) githubRepo: z.string().optional(), gitUrl: z.string().optional(), - platform: z.enum(['github', 'gitlab']).optional(), + platform: z.enum(['github', 'gitlab', 'bitbucket']).optional(), + bitbucketWorkspaceUuid: z.uuid().optional(), + bitbucketRepositoryUuid: z.uuid().optional(), // Execution params prompt: z.string().optional(), diff --git a/apps/web/src/routers/github-apps-router.ts b/apps/web/src/routers/github-apps-router.ts index 65e2395646..af30e38823 100644 --- a/apps/web/src/routers/github-apps-router.ts +++ b/apps/web/src/routers/github-apps-router.ts @@ -21,6 +21,7 @@ import { ensureOrganizationAccess } from '@/routers/organizations/utils'; import { createAuditLog } from '@/lib/organizations/organization-audit-logs'; import { APP_URL } from '@/lib/constants'; import { getGitHubAppCredentials } from '@/lib/integrations/platforms/github/app-selector'; +import { requireNumericPlatformRepositories } from '@/lib/integrations/core/types'; import { createGitHubUserAuthorizationState } from '@/lib/integrations/platforms/github/user-authorization-state'; import { disconnectGitHubUserAuthorization, @@ -108,7 +109,7 @@ export const githubAppsRouter = createTRPCRouter({ permissions: integration.permissions, events: integration.scopes, repositorySelection: integration.repository_access, - repositories: integration.repositories, + repositories: requireNumericPlatformRepositories(integration.repositories), suspendedAt: integration.suspended_at, suspendedBy: integration.suspended_by, installedAt: integration.installed_at, diff --git a/apps/web/src/routers/gitlab-router.ts b/apps/web/src/routers/gitlab-router.ts index 4d4ff3f3a7..451d30709b 100644 --- a/apps/web/src/routers/gitlab-router.ts +++ b/apps/web/src/routers/gitlab-router.ts @@ -10,6 +10,7 @@ import { } from '@/lib/integrations/resolve-owner'; import { validateGitLabInstance } from '@/lib/integrations/platforms/gitlab/adapter'; import { validatePersonalAccessToken } from '@/lib/integrations/platforms/gitlab/adapter'; +import { requireNumericPlatformRepositories } from '@/lib/integrations/core/types'; export const gitlabRouter = createTRPCRouter({ /** @@ -94,7 +95,7 @@ export const gitlabRouter = createTRPCRouter({ accountId: integration.platform_account_id, accountLogin: integration.platform_account_login, instanceUrl: metadata?.gitlab_instance_url || 'https://gitlab.com', - repositories: integration.repositories, + repositories: requireNumericPlatformRepositories(integration.repositories), repositoriesSyncedAt: integration.repositories_synced_at, installedAt: integration.installed_at, tokenExpiresAt: metadata?.token_expires_at ?? null, diff --git a/apps/web/src/routers/organizations/organization-bitbucket-router.test.ts b/apps/web/src/routers/organizations/organization-bitbucket-router.test.ts new file mode 100644 index 0000000000..e93d6d9239 --- /dev/null +++ b/apps/web/src/routers/organizations/organization-bitbucket-router.test.ts @@ -0,0 +1,554 @@ +/* eslint-disable drizzle/enforce-delete-with-where */ +import { afterAll, afterEach, beforeAll, describe, expect, it, jest } from '@jest/globals'; +import { + kilocode_users, + organization_memberships, + organizations, + platform_access_token_credentials, + platform_integrations, + platform_oauth_credentials, + type Organization, + type User, +} from '@kilocode/db/schema'; +import { db } from '@/lib/drizzle'; +import { eq } from 'drizzle-orm'; +import type { + BitbucketWorkspaceAccessTokenMutationResult, + ConnectBitbucketWorkspaceAccessTokenInput, + DisconnectBitbucketWorkspaceAccessTokenInput, + RotateBitbucketWorkspaceAccessTokenInput, +} from '@/lib/integrations/platforms/bitbucket/workspace-access-token-credentials'; +import type { createCallerForUser as CreateCallerForUser } from '@/routers/test-utils'; +import { createTestOrganization } from '@/tests/helpers/organization.helper'; +import { insertTestUser } from '@/tests/helpers/user.helper'; +import type { BitbucketRepositoryListResult } from '@/lib/integrations/platforms/bitbucket/token-service-client'; +import type * as TokenServiceClientModule from '@/lib/integrations/platforms/bitbucket/token-service-client'; +import type * as BitbucketRepositoryCacheModule from '@/lib/integrations/platforms/bitbucket/repository-cache'; + +const mockFetchBitbucketRepositoriesFromTokenService = + jest.fn<(kiloUserId: string, organizationId: string) => Promise>(); +const mockScheduleBitbucketRepositoryCachePrime = + jest.fn< + (input: { + owner: { type: 'user' | 'org'; id: string }; + kiloUserId: string; + integrationId: string; + }) => void + >(); + +jest.mock('@/lib/integrations/platforms/bitbucket/token-service-client', () => ({ + BitbucketRepositorySchema: jest.requireActual( + '@/lib/integrations/platforms/bitbucket/token-service-client' + ).BitbucketRepositorySchema, + fetchBitbucketWorkspaceAccessTokenRepositoriesFromTokenService: + mockFetchBitbucketRepositoriesFromTokenService, +})); + +jest.mock('@/lib/integrations/platforms/bitbucket/repository-cache', () => ({ + ...jest.requireActual( + '@/lib/integrations/platforms/bitbucket/repository-cache' + ), + scheduleBitbucketRepositoryCachePrime: mockScheduleBitbucketRepositoryCachePrime, +})); + +const mockConnect = + jest.fn< + ( + input: ConnectBitbucketWorkspaceAccessTokenInput + ) => Promise + >(); +const mockDisconnect = + jest.fn< + (input: DisconnectBitbucketWorkspaceAccessTokenInput) => Promise<{ integrationId: string }> + >(); +const mockRotate = + jest.fn< + ( + input: RotateBitbucketWorkspaceAccessTokenInput + ) => Promise + >(); + +jest.mock('@/lib/integrations/platforms/bitbucket/workspace-access-token-credentials', () => ({ + connectBitbucketWorkspaceAccessToken: mockConnect, + disconnectBitbucketWorkspaceAccessToken: mockDisconnect, + rotateBitbucketWorkspaceAccessToken: mockRotate, + BitbucketWorkspaceAccessTokenCredentialError: class extends Error {}, +})); + +let createCallerForUser: typeof CreateCallerForUser; + +const WORKSPACE_UUID = '11111111-1111-4111-8111-111111111111'; +const REPOSITORY_UUID = '22222222-2222-4222-8222-222222222222'; +const VALIDATED_AT = '2026-06-24T08:00:00.000Z'; + +async function insertStaticIntegration(organizationId: string, actorUserId: string) { + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_organization_id: organizationId, + owned_by_user_id: null, + created_by_user_id: actorUserId, + platform: 'bitbucket', + integration_type: 'workspace_access_token', + platform_account_id: WORKSPACE_UUID, + platform_account_login: 'acme', + platform_installation_id: null, + repository_access: 'all', + repositories: [ + { + id: REPOSITORY_UUID, + name: 'API', + full_name: 'acme/api', + private: true, + default_branch: 'main', + }, + ], + repositories_synced_at: VALIDATED_AT, + integration_status: 'active', + metadata: { displayName: 'Acme Workspace' }, + }) + .returning(); + if (!integration) throw new Error('Expected static Bitbucket integration'); + + await db.insert(platform_access_token_credentials).values({ + platform_integration_id: integration.id, + owned_by_organization_id: organizationId, + platform: 'bitbucket', + integration_type: 'workspace_access_token', + token_encrypted: 'secret-ciphertext-must-not-be-returned', + expires_at: null, + provider_credential_type: 'workspace_access_token', + provider_scopes: ['account', 'pullrequest', 'repository', 'repository:write', 'webhook'], + provider_verified_at: VALIDATED_AT, + credential_version: 1, + last_validated_at: VALIDATED_AT, + }); + + return integration; +} + +async function insertPendingOAuthIntegration(organizationId: string, actorUserId: string) { + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_organization_id: organizationId, + owned_by_user_id: null, + created_by_user_id: actorUserId, + platform: 'bitbucket', + integration_type: 'oauth', + scopes: ['account', 'email', 'repository', 'repository:write', 'webhook'], + repository_access: 'all', + integration_status: 'pending', + metadata: { + state: 'workspace_selection_required', + availableWorkspaces: [ + { + uuid: WORKSPACE_UUID, + slug: 'acme', + name: 'Acme Workspace', + }, + { + uuid: '33333333-3333-4333-8333-333333333333', + slug: 'example', + name: 'Example Workspace', + }, + ], + }, + }) + .returning(); + if (!integration) throw new Error('Expected pending Bitbucket OAuth integration'); + + await db.insert(platform_oauth_credentials).values({ + platform_integration_id: integration.id, + platform: 'bitbucket', + authorized_by_user_id: actorUserId, + provider_subject_id: '44444444-4444-4444-8444-444444444444', + provider_subject_login: 'bucket-admin', + access_token_encrypted: 'access-envelope', + access_token_expires_at: '2030-01-01T00:00:00.000Z', + refresh_token_encrypted: 'refresh-envelope', + }); + + return integration; +} + +async function insertActiveOAuthIntegration(organizationId: string, actorUserId: string) { + const [integration] = await db + .insert(platform_integrations) + .values({ + owned_by_organization_id: organizationId, + owned_by_user_id: null, + created_by_user_id: actorUserId, + platform: 'bitbucket', + integration_type: 'oauth', + platform_installation_id: WORKSPACE_UUID, + platform_account_id: WORKSPACE_UUID, + platform_account_login: 'acme', + scopes: ['account', 'email', 'repository', 'repository:write', 'webhook'], + repository_access: 'all', + repositories: [ + { + id: REPOSITORY_UUID, + name: 'API', + full_name: 'acme/api', + private: true, + default_branch: 'main', + }, + ], + repositories_synced_at: VALIDATED_AT, + integration_status: 'active', + metadata: { + state: 'active', + workspace: { uuid: WORKSPACE_UUID, slug: 'acme', name: 'Acme Workspace' }, + }, + }) + .returning(); + if (!integration) throw new Error('Expected active Bitbucket OAuth integration'); + + await db.insert(platform_oauth_credentials).values({ + platform_integration_id: integration.id, + platform: 'bitbucket', + authorized_by_user_id: actorUserId, + provider_subject_id: '44444444-4444-4444-8444-444444444444', + provider_subject_login: 'bucket-admin', + access_token_encrypted: 'access-envelope', + access_token_expires_at: '2030-01-01T00:00:00.000Z', + refresh_token_encrypted: 'refresh-envelope', + }); + + return integration; +} + +describe('organization Bitbucket router', () => { + let owner: User; + let billingManager: User; + let member: User; + let organization: Organization; + + beforeAll(async () => { + ({ createCallerForUser } = await import('@/routers/test-utils')); + owner = await insertTestUser(); + billingManager = await insertTestUser(); + member = await insertTestUser(); + organization = await createTestOrganization('Organization Bitbucket Router', owner.id, 0); + await db.insert(organization_memberships).values([ + { + organization_id: organization.id, + kilo_user_id: billingManager.id, + role: 'billing_manager', + }, + { organization_id: organization.id, kilo_user_id: member.id, role: 'member' }, + ]); + }); + + afterEach(async () => { + jest.clearAllMocks(); + await db.delete(platform_oauth_credentials); + await db.delete(platform_access_token_credentials); + await db.delete(platform_integrations); + }); + + afterAll(async () => { + await db.delete(organizations); + await db.delete(kilocode_users); + }); + + it('returns a sanitized initialized status to an ordinary member', async () => { + const integration = await insertStaticIntegration(organization.id, owner.id); + const caller = await createCallerForUser(member.id); + + const result = await caller.organizations.bitbucket.getStatus({ + organizationId: organization.id, + }); + + expect(result).toEqual({ + status: 'connected', + recoveryAction: null, + method: 'workspace_access_token', + integrationId: integration.id, + integrationStatus: 'active', + workspace: { + uuid: WORKSPACE_UUID, + slug: 'acme', + displayName: 'Acme Workspace', + }, + invalidatedAt: null, + invalidationReason: null, + lastValidatedAt: VALIDATED_AT, + repositoryCache: { + status: 'available', + repositories: [ + { + id: REPOSITORY_UUID, + workspaceUuid: WORKSPACE_UUID, + name: 'API', + fullName: 'acme/api', + private: true, + defaultBranch: 'main', + }, + ], + syncedAt: VALIDATED_AT, + }, + canManage: false, + }); + expect(JSON.stringify(result)).not.toContain('ciphertext'); + expect(result).not.toHaveProperty('token'); + expect(result).not.toHaveProperty('authorization'); + }); + + it('preserves the last successful cache in an invalidated status projection', async () => { + const integration = await insertStaticIntegration(organization.id, owner.id); + await db + .update(platform_integrations) + .set({ + auth_invalid_at: '2026-06-24T09:00:00.000Z', + auth_invalid_reason: 'provider_rejected', + }) + .where(eq(platform_integrations.id, integration.id)); + const caller = await createCallerForUser(member.id); + + const result = await caller.organizations.bitbucket.getStatus({ + organizationId: organization.id, + }); + + expect(result).toMatchObject({ + status: 'reconnect_required', + recoveryAction: 'replace_token', + invalidatedAt: '2026-06-24T09:00:00.000Z', + invalidationReason: 'provider_rejected', + repositoryCache: { + status: 'available', + repositories: [expect.objectContaining({ id: REPOSITORY_UUID })], + syncedAt: VALIDATED_AT, + }, + canManage: false, + }); + }); + + it('returns no recovery action when the organization is not connected', async () => { + const caller = await createCallerForUser(member.id); + + await expect( + caller.organizations.bitbucket.getStatus({ organizationId: organization.id }) + ).resolves.toMatchObject({ + status: 'not_connected', + recoveryAction: null, + canManage: false, + }); + }); + + it('returns OAuth workspace selection details only to organization managers', async () => { + const integration = await insertPendingOAuthIntegration(organization.id, owner.id); + const ownerCaller = await createCallerForUser(owner.id); + const memberCaller = await createCallerForUser(member.id); + + await expect( + ownerCaller.organizations.bitbucket.getStatus({ organizationId: organization.id }) + ).resolves.toMatchObject({ + status: 'workspace_selection_required', + recoveryAction: null, + method: 'oauth', + integrationId: integration.id, + authorizingNickname: 'bucket-admin', + availableWorkspaces: expect.arrayContaining([expect.objectContaining({ slug: 'acme' })]), + canManage: true, + }); + await expect( + memberCaller.organizations.bitbucket.getStatus({ organizationId: organization.id }) + ).resolves.toMatchObject({ + status: 'workspace_selection_required', + method: 'oauth', + authorizingNickname: null, + availableWorkspaces: [], + canManage: false, + }); + }); + + it('lets a billing manager select the OAuth workspace and primes the repository cache', async () => { + const integration = await insertPendingOAuthIntegration(organization.id, owner.id); + const caller = await createCallerForUser(billingManager.id); + + await expect( + caller.organizations.bitbucket.selectWorkspace({ + organizationId: organization.id, + workspaceUuid: WORKSPACE_UUID, + workspaceSlug: 'acme', + }) + ).resolves.toMatchObject({ success: true, workspace: { slug: 'acme' } }); + await expect( + caller.organizations.bitbucket.getStatus({ organizationId: organization.id }) + ).resolves.toMatchObject({ + status: 'connected', + method: 'oauth', + workspace: { slug: 'acme', displayName: 'Acme Workspace' }, + canManage: true, + }); + expect(mockScheduleBitbucketRepositoryCachePrime).toHaveBeenCalledWith({ + owner: { type: 'org', id: organization.id }, + kiloUserId: billingManager.id, + integrationId: integration.id, + }); + }); + + it('returns OAuth repository cache and disconnects OAuth integrations by id', async () => { + const integration = await insertActiveOAuthIntegration(organization.id, owner.id); + const caller = await createCallerForUser(owner.id); + + await expect( + caller.organizations.bitbucket.getStatus({ organizationId: organization.id }) + ).resolves.toMatchObject({ + status: 'connected', + method: 'oauth', + repositoryCache: { + status: 'available', + repositories: [expect.objectContaining({ fullName: 'acme/api' })], + syncedAt: VALIDATED_AT, + }, + canManage: true, + }); + await expect( + caller.organizations.bitbucket.disconnect({ + organizationId: organization.id, + integrationId: integration.id, + }) + ).resolves.toEqual({ integrationId: integration.id }); + }); + + it('requires disconnect and reconnect when the credential is missing', async () => { + await insertStaticIntegration(organization.id, owner.id); + await db.delete(platform_access_token_credentials); + const caller = await createCallerForUser(member.id); + + await expect( + caller.organizations.bitbucket.getStatus({ organizationId: organization.id }) + ).resolves.toMatchObject({ + status: 'reconnect_required', + recoveryAction: 'disconnect_and_connect', + canManage: false, + }); + }); + + it('rejects every integration mutation for an ordinary member', async () => { + const integration = await insertStaticIntegration(organization.id, owner.id); + const caller = await createCallerForUser(member.id); + + await expect( + caller.organizations.bitbucket.connect({ + organizationId: organization.id, + accessToken: 'ATCT-member-secret', + }) + ).rejects.toMatchObject({ code: 'UNAUTHORIZED' }); + await expect( + caller.organizations.bitbucket.replaceToken({ + organizationId: organization.id, + integrationId: integration.id, + accessToken: 'ATCT-member-secret', + }) + ).rejects.toMatchObject({ code: 'UNAUTHORIZED' }); + await expect( + caller.organizations.bitbucket.refreshRepositories({ + organizationId: organization.id, + integrationId: integration.id, + }) + ).rejects.toMatchObject({ code: 'UNAUTHORIZED' }); + await expect( + caller.organizations.bitbucket.disconnect({ + organizationId: organization.id, + integrationId: integration.id, + }) + ).rejects.toMatchObject({ code: 'UNAUTHORIZED' }); + await expect( + caller.organizations.bitbucket.selectWorkspace({ + organizationId: organization.id, + workspaceUuid: WORKSPACE_UUID, + workspaceSlug: 'acme', + }) + ).rejects.toMatchObject({ code: 'UNAUTHORIZED' }); + + expect(mockConnect).not.toHaveBeenCalled(); + expect(mockRotate).not.toHaveBeenCalled(); + expect(mockDisconnect).not.toHaveBeenCalled(); + }); + + it('returns sanitized authorization loss when a manager is demoted during refresh', async () => { + const refreshManager = await insertTestUser(); + await db.insert(organization_memberships).values({ + organization_id: organization.id, + kilo_user_id: refreshManager.id, + role: 'billing_manager', + }); + const integration = await insertStaticIntegration(organization.id, owner.id); + mockFetchBitbucketRepositoriesFromTokenService.mockImplementation(async () => { + await db + .delete(organization_memberships) + .where(eq(organization_memberships.kilo_user_id, refreshManager.id)); + return { + status: 'available', + repositories: [ + { + id: '33333333-3333-4333-8333-333333333333', + workspaceUuid: WORKSPACE_UUID, + name: 'Web', + fullName: 'acme/web', + private: false, + }, + ], + }; + }); + const caller = await createCallerForUser(refreshManager.id); + + await expect( + caller.organizations.bitbucket.refreshRepositories({ + organizationId: organization.id, + integrationId: integration.id, + }) + ).rejects.toMatchObject({ + code: 'UNAUTHORIZED', + message: 'The current user cannot refresh this organization integration', + }); + + const [unchanged] = await db + .select({ + repositories: platform_integrations.repositories, + syncedAt: platform_integrations.repositories_synced_at, + }) + .from(platform_integrations) + .where(eq(platform_integrations.id, integration.id)); + expect(unchanged?.repositories).toEqual([ + { + id: REPOSITORY_UUID, + name: 'API', + full_name: 'acme/api', + private: true, + default_branch: 'main', + }, + ]); + expect(new Date(unchanged?.syncedAt ?? '').toISOString()).toBe(VALIDATED_AT); + }); + + it('lets a billing manager connect with a token-only Workspace Access Token payload', async () => { + mockConnect.mockResolvedValue({ + integrationId: '33333333-3333-4333-8333-333333333333', + workspace: { + uuid: WORKSPACE_UUID, + slug: 'acme', + displayName: 'Acme Workspace', + }, + credentialVersion: 1, + repositoryCount: 1, + validatedAt: VALIDATED_AT, + }); + const caller = await createCallerForUser(billingManager.id); + + await caller.organizations.bitbucket.connect({ + organizationId: organization.id, + accessToken: 'ATCT-manager-secret', + }); + + expect(mockConnect).toHaveBeenCalledWith({ + organizationId: organization.id, + actorUserId: billingManager.id, + accessToken: 'ATCT-manager-secret', + }); + }); +}); diff --git a/apps/web/src/routers/organizations/organization-bitbucket-router.ts b/apps/web/src/routers/organizations/organization-bitbucket-router.ts new file mode 100644 index 0000000000..a41d9e61a9 --- /dev/null +++ b/apps/web/src/routers/organizations/organization-bitbucket-router.ts @@ -0,0 +1,227 @@ +import 'server-only'; + +import { TRPCError } from '@trpc/server'; +import { and, eq } from 'drizzle-orm'; +import { z } from 'zod'; +import { db } from '@/lib/drizzle'; +import { PLATFORM } from '@/lib/integrations/core/constants'; +import { + disconnectBitbucketOAuthIntegration, + getBitbucketOAuthIntegrationStatus, + refreshBitbucketOAuthRepositories, + selectBitbucketOAuthWorkspace, +} from '@/lib/integrations/platforms/bitbucket/oauth-integration'; +import { + BitbucketWorkspaceAccessTokenRepositoryCacheAuthorizationError, + getBitbucketWorkspaceAccessTokenStatus, + refreshBitbucketWorkspaceAccessTokenRepositories, +} from '@/lib/integrations/platforms/bitbucket/workspace-access-token-repository-cache'; +import { + BitbucketWorkspaceAccessTokenCredentialError, + connectBitbucketWorkspaceAccessToken, + disconnectBitbucketWorkspaceAccessToken, + rotateBitbucketWorkspaceAccessToken, +} from '@/lib/integrations/platforms/bitbucket/workspace-access-token-credentials'; +import { BitbucketWorkspaceAccessTokenError } from '@/lib/integrations/platforms/bitbucket/workspace-access-token-adapter'; +import { baseProcedure, createTRPCRouter } from '@/lib/trpc/init'; +import { + OrganizationIdInputSchema, + ensureOrganizationAccess, + organizationBillingProcedure, +} from '@/routers/organizations/utils'; +import { platform_integrations } from '@kilocode/db/schema'; + +const AccessTokenSchema = z.string().min(1).max(8_192); +const IntegrationIdSchema = z.uuid(); + +const ConnectInputSchema = OrganizationIdInputSchema.extend({ + accessToken: AccessTokenSchema, +}).strict(); + +const ReplaceTokenInputSchema = OrganizationIdInputSchema.extend({ + integrationId: IntegrationIdSchema, + accessToken: AccessTokenSchema, +}).strict(); + +const ExistingIntegrationInputSchema = OrganizationIdInputSchema.extend({ + integrationId: IntegrationIdSchema, +}).strict(); + +const SelectWorkspaceInputSchema = OrganizationIdInputSchema.extend({ + workspaceUuid: z.string().min(1), + workspaceSlug: z.string().min(1), +}).strict(); + +async function findOrganizationBitbucketIntegrationType(input: { + organizationId: string; + integrationId: string; +}): Promise<'workspace_access_token' | 'oauth'> { + const [integration] = await db + .select({ integrationType: platform_integrations.integration_type }) + .from(platform_integrations) + .where( + and( + eq(platform_integrations.id, input.integrationId), + eq(platform_integrations.owned_by_organization_id, input.organizationId), + eq(platform_integrations.platform, PLATFORM.BITBUCKET) + ) + ) + .limit(1); + + if ( + integration?.integrationType !== 'workspace_access_token' && + integration?.integrationType !== 'oauth' + ) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'The Bitbucket integration was not found' }); + } + return integration.integrationType; +} + +function rethrowBitbucketMutationError(error: unknown): never { + if (error instanceof BitbucketWorkspaceAccessTokenRepositoryCacheAuthorizationError) { + throw new TRPCError({ code: 'UNAUTHORIZED', message: error.message }); + } + + if (error instanceof BitbucketWorkspaceAccessTokenCredentialError) { + switch (error.code) { + case 'unauthorized': + throw new TRPCError({ code: 'UNAUTHORIZED', message: error.message }); + case 'organization_not_found': + case 'not_connected': + throw new TRPCError({ code: 'NOT_FOUND', message: error.message }); + case 'credential_conflict': + throw new TRPCError({ code: 'CONFLICT', message: error.message }); + case 'invalid_organization_id': + throw new TRPCError({ code: 'BAD_REQUEST', message: error.message }); + case 'encryption_failed': + throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: error.message }); + } + } + + if (error instanceof BitbucketWorkspaceAccessTokenError) { + switch (error.code) { + case 'permission_denied': + case 'insufficient_scopes': + throw new TRPCError({ code: 'FORBIDDEN', message: error.message }); + case 'rate_limited': + throw new TRPCError({ code: 'TOO_MANY_REQUESTS', message: error.message }); + case 'request_timeout': + throw new TRPCError({ code: 'TIMEOUT', message: error.message }); + case 'provider_unavailable': + case 'request_failed': + throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: error.message }); + default: + throw new TRPCError({ code: 'BAD_REQUEST', message: error.message }); + } + } + + throw error; +} + +export const organizationBitbucketRouter = createTRPCRouter({ + getStatus: baseProcedure.input(OrganizationIdInputSchema).query(async ({ ctx, input }) => { + const role = await ensureOrganizationAccess(ctx, input.organizationId); + const canManage = role === 'owner' || role === 'billing_manager'; + const workspaceAccessTokenStatus = await getBitbucketWorkspaceAccessTokenStatus( + input.organizationId + ); + if (workspaceAccessTokenStatus.integrationId) { + return { + ...workspaceAccessTokenStatus, + canManage, + }; + } + + const oauthStatus = await getBitbucketOAuthIntegrationStatus( + { type: 'org', id: input.organizationId }, + canManage + ); + return oauthStatus ?? { ...workspaceAccessTokenStatus, canManage }; + }), + + connect: organizationBillingProcedure + .input(ConnectInputSchema) + .mutation(async ({ ctx, input }) => { + try { + return await connectBitbucketWorkspaceAccessToken({ + organizationId: input.organizationId, + actorUserId: ctx.user.id, + accessToken: input.accessToken, + }); + } catch (error) { + rethrowBitbucketMutationError(error); + } + }), + + replaceToken: organizationBillingProcedure + .input(ReplaceTokenInputSchema) + .mutation(async ({ ctx, input }) => { + try { + return await rotateBitbucketWorkspaceAccessToken({ + organizationId: input.organizationId, + actorUserId: ctx.user.id, + integrationId: input.integrationId, + accessToken: input.accessToken, + }); + } catch (error) { + rethrowBitbucketMutationError(error); + } + }), + + selectWorkspace: organizationBillingProcedure + .input(SelectWorkspaceInputSchema) + .mutation(async ({ ctx, input }) => { + return selectBitbucketOAuthWorkspace({ + owner: { type: 'org', id: input.organizationId }, + kiloUserId: ctx.user.id, + workspaceUuid: input.workspaceUuid, + workspaceSlug: input.workspaceSlug, + }); + }), + + refreshRepositories: organizationBillingProcedure + .input(ExistingIntegrationInputSchema) + .mutation(async ({ ctx, input }) => { + try { + const integrationType = await findOrganizationBitbucketIntegrationType(input); + if (integrationType === 'oauth') { + return await refreshBitbucketOAuthRepositories({ + owner: { type: 'org', id: input.organizationId }, + kiloUserId: ctx.user.id, + expectedIntegrationId: input.integrationId, + }); + } + + return await refreshBitbucketWorkspaceAccessTokenRepositories({ + organizationId: input.organizationId, + kiloUserId: ctx.user.id, + expectedIntegrationId: input.integrationId, + }); + } catch (error) { + rethrowBitbucketMutationError(error); + } + }), + + disconnect: organizationBillingProcedure + .input(ExistingIntegrationInputSchema) + .mutation(async ({ ctx, input }) => { + try { + const integrationType = await findOrganizationBitbucketIntegrationType(input); + if (integrationType === 'oauth') { + const disconnected = await disconnectBitbucketOAuthIntegration({ + owner: { type: 'org', id: input.organizationId }, + integrationId: input.integrationId, + }); + return { integrationId: disconnected.integrationId }; + } + + return await disconnectBitbucketWorkspaceAccessToken({ + organizationId: input.organizationId, + actorUserId: ctx.user.id, + integrationId: input.integrationId, + }); + } catch (error) { + rethrowBitbucketMutationError(error); + } + }), +}); diff --git a/apps/web/src/routers/organizations/organization-cloud-agent-next-router.test.ts b/apps/web/src/routers/organizations/organization-cloud-agent-next-router.test.ts index 850f04b0e7..5a8195cb62 100644 --- a/apps/web/src/routers/organizations/organization-cloud-agent-next-router.test.ts +++ b/apps/web/src/routers/organizations/organization-cloud-agent-next-router.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, jest, beforeAll, beforeEach } from '@jest/globals import { createCallerFactory } from '@/lib/trpc/init'; import type * as TrpcInitModule from '@/lib/trpc/init'; import type { User } from '@kilocode/db/schema'; +import type { BitbucketOrganizationRepositoryListResult } from '@/lib/cloud-agent/bitbucket-integration-helpers'; const ORGANIZATION_ID = '9a283301-b75d-4375-a1ba-e319a02e18b7'; @@ -10,6 +11,10 @@ type AttachmentReference = { path: string; files: string[] }; const mockPrepareSession = jest.fn< (input: { githubRepo?: string; + gitUrl?: string; + platform?: 'github' | 'gitlab' | 'bitbucket'; + bitbucketWorkspaceUuid?: string; + bitbucketRepositoryUuid?: string; devcontainer?: boolean; kilocodeOrganizationId?: string; attachments?: AttachmentReference; @@ -55,6 +60,13 @@ const mockIsFeatureFlagEnabledOrDevelopment = jest.fn<(flagName: string, distinctId: string) => Promise>(); const mockVerifyOrgOwnsSessionV2ByCloudAgentId = jest.fn<() => Promise<{ kiloSessionId: string } | null>>(); +const mockFetchBitbucketRepositoriesForOrganization = + jest.fn< + ( + organizationId: string, + kiloUserId: string + ) => Promise + >(); jest.mock('@/lib/tokens', () => ({ generateCloudAgentToken: jest.fn(() => 'cloud-agent-token'), @@ -69,6 +81,13 @@ jest.mock('@/lib/posthog-feature-flags', () => ({ isFeatureFlagEnabledOrDevelopment: mockIsFeatureFlagEnabledOrDevelopment, })); +jest.mock('@/lib/cloud-agent/bitbucket-integration-helpers', () => ({ + ...jest.requireActual( + '@/lib/cloud-agent/bitbucket-integration-helpers' + ), + fetchBitbucketRepositoriesForOrganization: mockFetchBitbucketRepositoriesForOrganization, +})); + jest.mock('@/lib/r2/cloud-agent-attachments', () => ({ generateImageUploadUrl: jest.fn(), generateCloudAgentAttachmentUploadUrl: mockGenerateCloudAgentAttachmentUploadUrl, @@ -93,7 +112,12 @@ let createCaller: (ctx: { user: User }) => { prompt: string; mode: string; model: string; - githubRepo: string; + githubRepo?: string; + bitbucketRepo?: { + fullName: string; + workspaceUuid: string; + repositoryUuid: string; + }; autoInitiate: boolean; devcontainer: boolean; images?: { path: string; files: string[] }; @@ -115,6 +139,9 @@ let createCaller: (ctx: { user: User }) => { contentType: 'text/markdown'; contentLength: number; }) => Promise; + listBitbucketRepositories: (input: { + organizationId: string; + }) => Promise; }; beforeAll(async () => { @@ -262,6 +289,38 @@ describe('organizationCloudAgentNextRouter.prepareSession', () => { expect(mockPrepareSession).not.toHaveBeenCalledWith(expect.objectContaining({ images })); }); + it('forwards stable Bitbucket identity for organization sessions', async () => { + mockIsFeatureFlagEnabledOrDevelopment.mockResolvedValue(true); + const caller = createCaller({ user: { id: 'user-2', is_admin: false } as User }); + + await caller.prepareSession({ + organizationId: ORGANIZATION_ID, + prompt: 'Test prompt', + mode: 'code', + model: 'kilo/test-model', + bitbucketRepo: { + fullName: 'acme/repo', + workspaceUuid: '123e4567-e89b-12d3-a456-426614174020', + repositoryUuid: '123e4567-e89b-12d3-a456-426614174021', + }, + autoInitiate: true, + devcontainer: false, + }); + + expect(mockPrepareSession).toHaveBeenCalledWith( + expect.objectContaining({ + gitUrl: 'https://bitbucket.org/acme/repo.git', + platform: 'bitbucket', + bitbucketWorkspaceUuid: '123e4567-e89b-12d3-a456-426614174020', + bitbucketRepositoryUuid: '123e4567-e89b-12d3-a456-426614174021', + kilocodeOrganizationId: ORGANIZATION_ID, + }) + ); + expect(mockPrepareSession).not.toHaveBeenCalledWith( + expect.objectContaining({ bitbucketRepo: expect.anything() }) + ); + }); + it('forwards devcontainer sessions when the feature flag is enabled', async () => { mockIsFeatureFlagEnabledOrDevelopment.mockResolvedValue(true); const caller = createCaller({ @@ -295,3 +354,41 @@ describe('organizationCloudAgentNextRouter.prepareSession', () => { ); }); }); + +describe('organizationCloudAgentNextRouter Bitbucket repository listing', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('forwards exact organization ownership without a provider-refresh control', async () => { + const result = { + status: 'available' as const, + repositories: [], + syncedAt: '2026-06-23T08:00:00.000Z', + }; + mockFetchBitbucketRepositoriesForOrganization.mockResolvedValue(result); + const caller = createCaller({ user: { id: 'member-1', is_admin: false } as User }); + + await expect( + caller.listBitbucketRepositories({ + organizationId: ORGANIZATION_ID, + }) + ).resolves.toEqual(result); + expect(mockFetchBitbucketRepositoriesForOrganization).toHaveBeenCalledWith( + ORGANIZATION_ID, + 'member-1' + ); + }); + + it('propagates temporary cache initialization failure distinctly', async () => { + const result = { status: 'temporarily_unavailable' as const }; + mockFetchBitbucketRepositoriesForOrganization.mockResolvedValue(result); + const caller = createCaller({ user: { id: 'member-1', is_admin: false } as User }); + + await expect( + caller.listBitbucketRepositories({ + organizationId: ORGANIZATION_ID, + }) + ).resolves.toEqual(result); + }); +}); diff --git a/apps/web/src/routers/organizations/organization-cloud-agent-next-router.ts b/apps/web/src/routers/organizations/organization-cloud-agent-next-router.ts index 8668ca5493..d42d307abf 100644 --- a/apps/web/src/routers/organizations/organization-cloud-agent-next-router.ts +++ b/apps/web/src/routers/organizations/organization-cloud-agent-next-router.ts @@ -12,6 +12,10 @@ import { organizationMemberMutationProcedure, } from '@/routers/organizations/utils'; import { fetchGitHubRepositoriesForOrganization } from '@/lib/cloud-agent/github-integration-helpers'; +import { + BitbucketOrganizationRepositoryListResultSchema, + fetchBitbucketRepositoriesForOrganization, +} from '@/lib/cloud-agent/bitbucket-integration-helpers'; import { getGitLabInstanceUrlForOrganization, buildGitLabCloneUrl, @@ -179,6 +183,10 @@ const ListGitLabRepositoriesInput = z.object({ forceRefresh: z.boolean().optional().default(false), }); +const ListBitbucketRepositoriesInput = z.object({ + organizationId: z.uuid(), +}); + /** * Cloud Agent Next Router (Organization Context) * @@ -215,8 +223,15 @@ export const organizationCloudAgentNextRouter = createTRPCRouter({ const authToken = generateCloudAgentToken(ctx.user); const client = createCloudAgentNextClient(authToken); - const { gitlabProject, githubRepo, organizationId, attachments, images, ...restInput } = - input; + const { + gitlabProject, + githubRepo, + bitbucketRepo, + organizationId, + attachments, + images, + ...restInput + } = input; // Profile resolution happens inside cloud-agent-next. Tokens are resolved // there as well via GIT_TOKEN_SERVICE. We forward profileId + inline @@ -224,13 +239,22 @@ export const organizationCloudAgentNextRouter = createTRPCRouter({ let gitParams: { githubRepo?: string; gitUrl?: string; - platform?: 'github' | 'gitlab'; + platform?: 'github' | 'gitlab' | 'bitbucket'; + bitbucketWorkspaceUuid?: string; + bitbucketRepositoryUuid?: string; }; if (gitlabProject) { const instanceUrl = await getGitLabInstanceUrlForOrganization(organizationId); const gitUrl = buildGitLabCloneUrl(gitlabProject, instanceUrl); gitParams = { gitUrl, platform: PLATFORM.GITLAB }; + } else if (bitbucketRepo) { + gitParams = { + gitUrl: `https://bitbucket.org/${bitbucketRepo.fullName}.git`, + platform: PLATFORM.BITBUCKET, + bitbucketWorkspaceUuid: bitbucketRepo.workspaceUuid, + bitbucketRepositoryUuid: bitbucketRepo.repositoryUuid, + }; } else { gitParams = { githubRepo, platform: PLATFORM.GITHUB }; } @@ -607,4 +631,11 @@ export const organizationCloudAgentNextRouter = createTRPCRouter({ errorMessage: result.errorMessage, }; }), + + listBitbucketRepositories: organizationMemberProcedure + .input(ListBitbucketRepositoriesInput) + .output(BitbucketOrganizationRepositoryListResultSchema) + .query(async ({ ctx, input }) => { + return fetchBitbucketRepositoriesForOrganization(input.organizationId, ctx.user.id); + }), }); diff --git a/apps/web/src/routers/organizations/organization-router.ts b/apps/web/src/routers/organizations/organization-router.ts index dd0d981919..4139765b91 100644 --- a/apps/web/src/routers/organizations/organization-router.ts +++ b/apps/web/src/routers/organizations/organization-router.ts @@ -62,6 +62,7 @@ import { organizationAutoTriageRouter } from '@/routers/organizations/organizati import { organizationAutoFixRouter } from '@/routers/organizations/organization-auto-fix-router'; import { organizationAutoTopUpRouter } from '@/routers/organizations/organization-auto-top-up-router'; import { organizationKiloclawRouter } from '@/routers/organizations/organization-kiloclaw-router'; +import { organizationBitbucketRouter } from '@/routers/organizations/organization-bitbucket-router'; const OrganizationUpdateSchema = OrganizationIdInputSchema.extend({ name: OrganizationNameSchema, @@ -117,6 +118,7 @@ export const organizationsRouter = createTRPCRouter({ autoFix: organizationAutoFixRouter, autoTopUp: organizationAutoTopUpRouter, kiloclaw: organizationKiloclawRouter, + bitbucket: organizationBitbucketRouter, list: baseProcedure.query(async opts => { const { user } = opts.ctx; diff --git a/apps/web/src/routers/platform-integrations-router.test.ts b/apps/web/src/routers/platform-integrations-router.test.ts index 4d0ca0fe14..aa91ba25dc 100644 --- a/apps/web/src/routers/platform-integrations-router.test.ts +++ b/apps/web/src/routers/platform-integrations-router.test.ts @@ -62,6 +62,12 @@ describe('platformIntegrationsRouter', () => { platformAccountLogin: 'kilocode', status: INTEGRATION_STATUS.ACTIVE, }); + await insertPlatformIntegration({ + userId: ownerUser.id, + platform: PLATFORM.BITBUCKET, + platformAccountLogin: 'personal-workspace', + status: INTEGRATION_STATUS.ACTIVE, + }); const caller = await createCallerForUser(ownerUser.id); const result = await caller.platformIntegrations.listSetupStatus(); @@ -92,6 +98,13 @@ describe('platformIntegrationsRouter', () => { platformAccountLogin: 'kilocode', status: INTEGRATION_STATUS.ACTIVE, }); + await insertPlatformIntegration({ + organizationId: organization.id, + platform: PLATFORM.BITBUCKET, + platformAccountLogin: 'acme', + status: INTEGRATION_STATUS.ACTIVE, + integrationType: 'workspace_access_token', + }); const caller = await createCallerForUser(memberUser.id); const result = await caller.platformIntegrations.listSetupStatus({ @@ -104,6 +117,34 @@ describe('platformIntegrationsRouter', () => { installed: true, installation: { accountLogin: 'kilocode' }, }, + { + platform: PLATFORM.BITBUCKET, + installed: true, + installation: { accountLogin: 'acme' }, + }, + ]); + }); + + test('includes organization Bitbucket OAuth setup status', async () => { + await insertPlatformIntegration({ + organizationId: organization.id, + platform: PLATFORM.BITBUCKET, + platformAccountLogin: 'oauth-workspace', + status: INTEGRATION_STATUS.ACTIVE, + integrationType: 'oauth', + }); + + const caller = await createCallerForUser(memberUser.id); + const result = await caller.platformIntegrations.listSetupStatus({ + organizationId: organization.id, + }); + + expect(result).toEqual([ + { + platform: PLATFORM.BITBUCKET, + installed: true, + installation: { accountLogin: 'oauth-workspace' }, + }, ]); }); @@ -122,18 +163,20 @@ async function insertPlatformIntegration({ platform, platformAccountLogin, status, + integrationType = 'app', }: { userId?: string; organizationId?: string; platform: string; platformAccountLogin: string; status: string; + integrationType?: string; }) { await db.insert(platform_integrations).values({ owned_by_user_id: userId ?? null, owned_by_organization_id: organizationId ?? null, platform, - integration_type: 'app', + integration_type: integrationType, platform_installation_id: `${platform}-${crypto.randomUUID()}`, platform_account_login: platformAccountLogin, repository_access: 'all', diff --git a/apps/web/src/routers/platform-integrations-router.ts b/apps/web/src/routers/platform-integrations-router.ts index b2cabfadb3..c40c258239 100644 --- a/apps/web/src/routers/platform-integrations-router.ts +++ b/apps/web/src/routers/platform-integrations-router.ts @@ -2,6 +2,7 @@ import 'server-only'; import { getAllIntegrationsForOwner } from '@/lib/integrations/db/platform-integrations'; import { optionalOrgInput, resolveOwner } from '@/lib/integrations/resolve-owner'; import { summarizePlatformIntegrationsForSetupStatus } from '@/lib/integrations/platform-integration-setup-status'; +import { PLATFORM } from '@/lib/integrations/core/constants'; import { baseProcedure, createTRPCRouter } from '@/lib/trpc/init'; import { ensureOrganizationAccess } from '@/routers/organizations/utils'; @@ -13,6 +14,14 @@ export const platformIntegrationsRouter = createTRPCRouter({ const owner = resolveOwner(ctx, input?.organizationId); const integrations = await getAllIntegrationsForOwner(owner); - return summarizePlatformIntegrationsForSetupStatus(integrations); + const visibleIntegrations = integrations.filter(integration => { + if (integration.platform !== PLATFORM.BITBUCKET) return true; + return ( + owner.type === 'org' && + (integration.integration_type === 'workspace_access_token' || + integration.integration_type === 'oauth') + ); + }); + return summarizePlatformIntegrationsForSetupStatus(visibleIntegrations); }), }); diff --git a/apps/web/src/routers/root-router.test.ts b/apps/web/src/routers/root-router.test.ts index a5ceb8658a..299ed3746e 100644 --- a/apps/web/src/routers/root-router.test.ts +++ b/apps/web/src/routers/root-router.test.ts @@ -1,6 +1,7 @@ import { createCallerForUser } from '@/routers/test-utils'; import { insertTestUser } from '@/tests/helpers/user.helper'; import type { User } from '@kilocode/db/schema'; +import { rootRouter } from './root-router'; // Test users will be created dynamically let regularUser: User; @@ -34,6 +35,17 @@ describe('trpc tests', () => { // Test cleanup is handled automatically by the test framework }); + describe('router composition', () => { + it('registers Bitbucket only under organizations', () => { + expect(rootRouter._def.record).not.toHaveProperty('bitbucket'); + expect(rootRouter._def.record).toHaveProperty('organizations.bitbucket'); + expect(rootRouter._def.record).not.toHaveProperty('cloudAgentNext.listBitbucketRepositories'); + expect(rootRouter._def.record).toHaveProperty( + 'organizations.cloudAgentNext.listBitbucketRepositories' + ); + }); + }); + describe('hello procedure', () => { it('should greet the user with custom text', async () => { const caller = await createCallerForUser(regularUser.id); diff --git a/packages/db/src/migrations/0173_wealthy_johnny_blaze.sql b/packages/db/src/migrations/0173_wealthy_johnny_blaze.sql new file mode 100644 index 0000000000..e6a3bc6c25 --- /dev/null +++ b/packages/db/src/migrations/0173_wealthy_johnny_blaze.sql @@ -0,0 +1,58 @@ +CREATE TABLE "platform_access_token_credentials" ( + "id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL, + "platform_integration_id" uuid NOT NULL, + "owned_by_organization_id" uuid NOT NULL, + "platform" text NOT NULL, + "integration_type" text NOT NULL, + "token_encrypted" text NOT NULL, + "expires_at" timestamp with time zone, + "provider_credential_type" text NOT NULL, + "provider_scopes" text[] NOT NULL, + "provider_verified_at" timestamp with time zone NOT NULL, + "credential_version" integer DEFAULT 1 NOT NULL, + "last_validated_at" timestamp with time zone NOT NULL, + "last_used_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "UQ_platform_access_token_credentials_platform_integration_id" UNIQUE("platform_integration_id"), + CONSTRAINT "platform_access_token_credentials_platform_check" CHECK ("platform_access_token_credentials"."platform" = 'bitbucket'), + CONSTRAINT "platform_access_token_credentials_integration_type_check" CHECK ("platform_access_token_credentials"."integration_type" = 'workspace_access_token'), + CONSTRAINT "platform_access_token_credentials_provider_type_check" CHECK ("platform_access_token_credentials"."provider_credential_type" = 'workspace_access_token'), + CONSTRAINT "platform_access_token_credentials_version_positive_check" CHECK ("platform_access_token_credentials"."credential_version" > 0) +); +--> statement-breakpoint +CREATE TABLE "platform_oauth_credentials" ( + "id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL, + "platform_integration_id" uuid NOT NULL, + "platform" text NOT NULL, + "authorized_by_user_id" text NOT NULL, + "provider_subject_id" text NOT NULL, + "provider_subject_login" text NOT NULL, + "access_token_encrypted" text NOT NULL, + "access_token_expires_at" timestamp with time zone, + "refresh_token_encrypted" text NOT NULL, + "refresh_token_expires_at" timestamp with time zone, + "credential_version" integer DEFAULT 1 NOT NULL, + "revoked_at" timestamp with time zone, + "revocation_reason" text, + "last_used_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "platform_access_token_credentials" ADD CONSTRAINT "FK_platform_access_token_credentials_parent" FOREIGN KEY ("platform_integration_id") REFERENCES "public"."platform_integrations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "platform_oauth_credentials" ADD CONSTRAINT "platform_oauth_credentials_platform_integration_id_platform_integrations_id_fk" FOREIGN KEY ("platform_integration_id") REFERENCES "public"."platform_integrations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "platform_oauth_credentials" ADD CONSTRAINT "platform_oauth_credentials_authorized_by_user_id_kilocode_users_id_fk" FOREIGN KEY ("authorized_by_user_id") REFERENCES "public"."kilocode_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_platform_oauth_credentials_platform_integration_id" ON "platform_oauth_credentials" USING btree ("platform_integration_id");--> statement-breakpoint +CREATE INDEX "IDX_platform_oauth_credentials_platform_subject" ON "platform_oauth_credentials" USING btree ("platform","provider_subject_id");--> statement-breakpoint +CREATE INDEX "IDX_platform_oauth_credentials_authorized_by_user_id" ON "platform_oauth_credentials" USING btree ("authorized_by_user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_platform_integrations_user_bitbucket" ON "platform_integrations" USING btree ("owned_by_user_id","platform") WHERE "platform_integrations"."platform" = 'bitbucket' AND "platform_integrations"."owned_by_user_id" IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_platform_integrations_org_bitbucket" ON "platform_integrations" USING btree ("owned_by_organization_id","platform") WHERE "platform_integrations"."platform" = 'bitbucket' AND "platform_integrations"."owned_by_organization_id" IS NOT NULL;--> statement-breakpoint +ALTER TABLE "platform_integrations" ADD CONSTRAINT "platform_integrations_access_token_auth_invalidation_pair_check" CHECK (( + "platform_integrations"."platform" <> 'bitbucket' OR + "platform_integrations"."integration_type" <> 'workspace_access_token' OR + ( + ("platform_integrations"."auth_invalid_at" IS NULL AND "platform_integrations"."auth_invalid_reason" IS NULL) OR + ("platform_integrations"."auth_invalid_at" IS NOT NULL AND "platform_integrations"."auth_invalid_reason" IS NOT NULL) + ) + )); \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0173_snapshot.json b/packages/db/src/migrations/meta/0173_snapshot.json new file mode 100644 index 0000000000..59c963f4bf --- /dev/null +++ b/packages/db/src/migrations/meta/0173_snapshot.json @@ -0,0 +1,32152 @@ +{ + "id": "f9ae3efa-e7e8-486d-8a23-06730f557a96", + "prevId": "969175e5-639f-49f5-beef-8d6033d50298", + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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_repo_pr_sha": { + "name": "UQ_cloud_agent_code_reviews_repo_pr_sha", + "columns": [ + { + "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, + "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 + }, + "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 + }, + "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_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_failure_classification_check": { + "name": "cloud_agent_session_runs_failure_classification_check", + "value": "(\"cloud_agent_session_runs\".\"failure_stage\" IS NULL AND \"cloud_agent_session_runs\".\"failure_code\" IS NULL) OR\n (\"cloud_agent_session_runs\".\"failure_stage\" = 'pre_dispatch' AND \"cloud_agent_session_runs\".\"failure_code\" IN ('sandbox_connect_failed', 'workspace_setup_failed', 'kilo_server_failed', 'wrapper_start_failed', 'invalid_delivery_request', 'session_metadata_missing', 'model_missing', 'delivery_failure_unknown')) OR\n (\"cloud_agent_session_runs\".\"failure_stage\" = 'post_dispatch_no_activity' AND \"cloud_agent_session_runs\".\"failure_code\" IN ('wrapper_disconnected', 'wrapper_no_output', 'wrapper_ping_timeout', 'wrapper_error_before_activity', 'missing_assistant_reply')) OR\n (\"cloud_agent_session_runs\".\"failure_stage\" = 'agent_activity' AND \"cloud_agent_session_runs\".\"failure_code\" IN ('assistant_error', 'wrapper_error_after_activity')) OR\n (\"cloud_agent_session_runs\".\"failure_stage\" = 'interruption' AND \"cloud_agent_session_runs\".\"failure_code\" IN ('user_interrupt', 'container_shutdown', 'system_interrupt')) OR\n (\"cloud_agent_session_runs\".\"failure_stage\" = 'unknown' AND \"cloud_agent_session_runs\".\"failure_code\" = 'unclassified')" + }, + "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 + }, + "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.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": {} + }, + "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.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 + }, + "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": {} + } + }, + "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.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.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_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 + }, + "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 + } + }, + "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_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()" + } + }, + "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": {} + } + }, + "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 + }, + "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_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" + } + }, + "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 + }, + "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": {} + } + }, + "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_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 + }, + "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_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_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_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 + }, + "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_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_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 + }, + "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_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_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_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.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_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": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "integration_type": { + "name": "integration_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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_scopes": { + "name": "provider_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "provider_verified_at": { + "name": "provider_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "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": true + }, + "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": {}, + "foreignKeys": { + "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": { + "UQ_platform_access_token_credentials_platform_integration_id": { + "name": "UQ_platform_access_token_credentials_platform_integration_id", + "nullsNotDistinct": false, + "columns": [ + "platform_integration_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "platform_access_token_credentials_platform_check": { + "name": "platform_access_token_credentials_platform_check", + "value": "\"platform_access_token_credentials\".\"platform\" = 'bitbucket'" + }, + "platform_access_token_credentials_integration_type_check": { + "name": "platform_access_token_credentials_integration_type_check", + "value": "\"platform_access_token_credentials\".\"integration_type\" = 'workspace_access_token'" + }, + "platform_access_token_credentials_provider_type_check": { + "name": "platform_access_token_credentials_provider_type_check", + "value": "\"platform_access_token_credentials\".\"provider_credential_type\" = 'workspace_access_token'" + }, + "platform_access_token_credentials_version_positive_check": { + "name": "platform_access_token_credentials_version_positive_check", + "value": "\"platform_access_token_credentials\".\"credential_version\" > 0" + } + }, + "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_user_bitbucket": { + "name": "UQ_platform_integrations_user_bitbucket", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "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" + }, + { + "expression": "platform", + "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 )" + }, + "platform_integrations_access_token_auth_invalidation_pair_check": { + "name": "platform_integrations_access_token_auth_invalidation_pair_check", + "value": "(\n \"platform_integrations\".\"platform\" <> 'bitbucket' OR\n \"platform_integrations\".\"integration_type\" <> 'workspace_access_token' OR\n (\n (\"platform_integrations\".\"auth_invalid_at\" IS NULL AND \"platform_integrations\".\"auth_invalid_reason\" IS NULL) OR\n (\"platform_integrations\".\"auth_invalid_at\" IS NOT NULL AND \"platform_integrations\".\"auth_invalid_reason\" IS NOT NULL)\n )\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": true + }, + "authorized_by_user_id": { + "name": "authorized_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "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": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "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_platform_subject": { + "name": "IDX_platform_oauth_credentials_platform_subject", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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": {}, + "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_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 da9af7c8d3..44af307c06 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -1212,6 +1212,13 @@ "when": 1782227993584, "tag": "0172_boring_ghost_rider", "breakpoints": true + }, + { + "idx": 173, + "version": "7", + "when": 1782335387926, + "tag": "0173_wealthy_johnny_blaze", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema-types.ts b/packages/db/src/schema-types.ts index 28e8bde258..feac35257b 100644 --- a/packages/db/src/schema-types.ts +++ b/packages/db/src/schema-types.ts @@ -969,11 +969,12 @@ export type GatewayApiKind = z.infer; export type IntegrationPermissions = Record; -export type PlatformRepository = { - id: number; +export type PlatformRepository = { + id: TId; name: string; full_name: string; private: boolean; + default_branch?: string; }; export const REVIEW_MEMORY_PLATFORMS = ['github'] as const; diff --git a/packages/db/src/schema.test.ts b/packages/db/src/schema.test.ts index f14479226f..cc38cb32c3 100644 --- a/packages/db/src/schema.test.ts +++ b/packages/db/src/schema.test.ts @@ -159,6 +159,118 @@ async function expectEphemeralConstraintViolation( }); } +type PlatformIntegrationInsert = typeof schema.platform_integrations.$inferInsert; +type PlatformAccessTokenCredentialInsert = + typeof schema.platform_access_token_credentials.$inferInsert; + +async function withPlatformAccessTokenTestData( + testFn: (params: { + userId: string; + organizationId: string; + otherOrganizationId: string; + }) => Promise +): Promise { + const userId = `schema-platform-token-${crypto.randomUUID()}`; + + await schemaTestDb.db.insert(schema.kilocode_users).values({ + id: userId, + google_user_email: `${userId}@example.com`, + google_user_name: 'Schema Platform Token User', + google_user_image_url: 'https://example.com/avatar.png', + stripe_customer_id: `cus_${crypto.randomUUID()}`, + }); + + const organizationRows = await schemaTestDb.db + .insert(schema.organizations) + .values([ + { name: `Schema Platform Token Org ${crypto.randomUUID()}` }, + { name: `Schema Platform Token Other Org ${crypto.randomUUID()}` }, + ]) + .returning({ id: schema.organizations.id }); + const organization = organizationRows[0]; + const otherOrganization = organizationRows[1]; + if (!organization || !otherOrganization) { + throw new Error('Failed to insert platform access token test organizations'); + } + + try { + await testFn({ + userId, + organizationId: organization.id, + otherOrganizationId: otherOrganization.id, + }); + } finally { + await schemaTestDb.db + .delete(schema.organizations) + .where(eq(schema.organizations.id, organization.id)); + await schemaTestDb.db + .delete(schema.organizations) + .where(eq(schema.organizations.id, otherOrganization.id)); + await schemaTestDb.db.delete(schema.kilocode_users).where(eq(schema.kilocode_users.id, userId)); + } +} + +async function insertPlatformIntegration( + organizationId: string, + overrides: Partial = {} +): Promise { + const [integration] = await schemaTestDb.db + .insert(schema.platform_integrations) + .values({ + owned_by_organization_id: organizationId, + platform: 'bitbucket', + integration_type: 'workspace_access_token', + integration_status: 'active', + ...overrides, + }) + .returning(); + + if (!integration) { + throw new Error('Failed to insert platform integration'); + } + + return integration; +} + +async function insertPlatformAccessTokenCredential( + integration: typeof schema.platform_integrations.$inferSelect, + overrides: Partial = {} +): Promise { + const now = '2026-06-24T10:00:00.000Z'; + const [credential] = await schemaTestDb.db + .insert(schema.platform_access_token_credentials) + .values({ + platform_integration_id: integration.id, + owned_by_organization_id: integration.owned_by_organization_id ?? crypto.randomUUID(), + platform: 'bitbucket', + integration_type: 'workspace_access_token', + token_encrypted: 'encrypted-workspace-access-token', + provider_credential_type: 'workspace_access_token', + provider_scopes: ['account', 'repository', 'repository:write'], + provider_verified_at: now, + last_validated_at: now, + ...overrides, + }) + .returning(); + + if (!credential) { + throw new Error('Failed to insert platform access token credential'); + } + + return credential; +} + +async function expectPlatformCredentialConstraintViolation( + insertPromise: Promise, + constraint: string +): Promise { + await expect(insertPromise).rejects.toMatchObject({ + cause: { + constraint, + }, + }); +} + describe('database schema', () => { it("should be up to date with migrations (run 'pnpm drizzle generate' if this fails)", async () => { const migrationsDir = path.join(__dirname, 'migrations'); @@ -541,6 +653,146 @@ describe('database schema', () => { expect(Object.hasOwn(schema, 'kilo_pass_store_purchases')).toBe(true); }); + it('exposes provider-neutral access token credentials', () => { + expect(Object.hasOwn(schema, 'platform_access_token_credentials')).toBe(true); + }); + + describe('platform access token credentials', () => { + it('stores one verified Bitbucket Workspace Access Token credential for an organization integration', async () => { + await withPlatformAccessTokenTestData(async ({ organizationId }) => { + const integration = await insertPlatformIntegration(organizationId); + + const credential = await insertPlatformAccessTokenCredential(integration); + + expect(credential).toEqual( + expect.objectContaining({ + platform_integration_id: integration.id, + owned_by_organization_id: organizationId, + platform: 'bitbucket', + integration_type: 'workspace_access_token', + provider_credential_type: 'workspace_access_token', + credential_version: 1, + }) + ); + expect(credential).not.toHaveProperty('capability_profile'); + }); + }); + + it('rejects a credential without a parent integration', async () => { + await withPlatformAccessTokenTestData(async ({ organizationId }) => { + const integration = await insertPlatformIntegration(organizationId); + + await expectPlatformCredentialConstraintViolation( + insertPlatformAccessTokenCredential(integration, { + platform_integration_id: crypto.randomUUID(), + }), + 'FK_platform_access_token_credentials_parent' + ); + }); + }); + + it('rejects a second credential for the same integration', async () => { + await withPlatformAccessTokenTestData(async ({ organizationId }) => { + const integration = await insertPlatformIntegration(organizationId); + await insertPlatformAccessTokenCredential(integration); + + await expectPlatformCredentialConstraintViolation( + insertPlatformAccessTokenCredential(integration), + 'UQ_platform_access_token_credentials_platform_integration_id' + ); + }); + }); + + it.each([ + { + label: 'platform', + overrides: { platform: 'github' as 'bitbucket' }, + constraint: 'platform_access_token_credentials_platform_check', + }, + { + label: 'integration type', + overrides: { integration_type: 'oauth' as 'workspace_access_token' }, + constraint: 'platform_access_token_credentials_integration_type_check', + }, + { + label: 'provider credential type', + overrides: { provider_credential_type: 'oauth2' as 'workspace_access_token' }, + constraint: 'platform_access_token_credentials_provider_type_check', + }, + { + label: 'credential version', + overrides: { credential_version: 0 }, + constraint: 'platform_access_token_credentials_version_positive_check', + }, + ] satisfies { + label: string; + overrides: Partial; + constraint: string; + }[])('rejects an invalid $label', async ({ overrides, constraint }) => { + await withPlatformAccessTokenTestData(async ({ organizationId }) => { + const integration = await insertPlatformIntegration(organizationId); + + await expectPlatformCredentialConstraintViolation( + insertPlatformAccessTokenCredential(integration, overrides), + constraint + ); + }); + }); + + it.each([ + { auth_invalid_at: '2026-06-24T10:00:00.000Z', auth_invalid_reason: null }, + { auth_invalid_at: null, auth_invalid_reason: 'provider_rejected' }, + ])('rejects unpaired Workspace Access Token invalidation state', async invalidation => { + await withPlatformAccessTokenTestData(async ({ organizationId }) => { + await expectPlatformCredentialConstraintViolation( + insertPlatformIntegration(organizationId, invalidation), + 'platform_integrations_access_token_auth_invalidation_pair_check' + ); + }); + }); + + it('allows paired Workspace Access Token invalidation state', async () => { + await withPlatformAccessTokenTestData(async ({ organizationId }) => { + const integration = await insertPlatformIntegration(organizationId, { + auth_invalid_at: '2026-06-24T10:00:00.000Z', + auth_invalid_reason: 'provider_rejected', + }); + + expect(integration.auth_invalid_reason).toBe('provider_rejected'); + }); + }); + + it('does not apply the invalidation-pair check to OAuth integrations', async () => { + await withPlatformAccessTokenTestData(async ({ organizationId }) => { + const integration = await insertPlatformIntegration(organizationId, { + integration_type: 'oauth', + auth_invalid_reason: 'authorizing_user_deleted', + }); + + expect(integration.auth_invalid_at).toBeNull(); + expect(integration.auth_invalid_reason).toBe('authorizing_user_deleted'); + }); + }); + + it('cascades credential deletion with its parent integration', async () => { + await withPlatformAccessTokenTestData(async ({ organizationId }) => { + const integration = await insertPlatformIntegration(organizationId); + const credential = await insertPlatformAccessTokenCredential(integration); + + await schemaTestDb.db + .delete(schema.platform_integrations) + .where(eq(schema.platform_integrations.id, integration.id)); + + expect( + await schemaTestDb.db + .select() + .from(schema.platform_access_token_credentials) + .where(eq(schema.platform_access_token_credentials.id, credential.id)) + ).toHaveLength(0); + }); + }); + }); + describe('ephemeral deployments', () => { it('allows pending rows with null slug and expiry', async () => { await withEphemeralTestUser(async ({ userId }) => { diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index a7a9c33c2f..fc3ffb87c4 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -193,6 +193,8 @@ export const SCHEMA_CHECK_ENUMS = { SecurityAuditLogAction, SecurityAuditLogActorType, SecurityFindingAuditSourceContext, + SecurityFindingNotificationKind, + SecurityFindingNotificationStatus, KiloClawPlan, KiloClawScheduledPlan, KiloClawScheduledBy, @@ -3004,7 +3006,7 @@ export const platform_integrations = pgTable( // Repository access (GitHub's value: 'all' or 'selected') repository_access: text(), // nullable for pending installations - repositories: jsonb().$type(), + repositories: jsonb().$type[]>(), repositories_synced_at: timestamp({ withTimezone: true, mode: 'string' }), auth_invalid_at: timestamp({ withTimezone: true, mode: 'string' }), auth_invalid_reason: text(), @@ -3048,6 +3050,14 @@ 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_user_bitbucket') + .on(table.owned_by_user_id, table.platform) + .where(sql`${table.platform} = 'bitbucket' AND ${table.owned_by_user_id} IS NOT NULL`), + uniqueIndex('UQ_platform_integrations_org_bitbucket') + .on(table.owned_by_organization_id, table.platform) + .where( + sql`${table.platform} = 'bitbucket' AND ${table.owned_by_organization_id} IS NOT NULL` + ), index('IDX_platform_integrations_owned_by_org_id').on(table.owned_by_organization_id), index('IDX_platform_integrations_owned_by_user_id').on(table.owned_by_user_id), index('IDX_platform_integrations_platform_inst_id').on(table.platform_installation_id), @@ -3079,6 +3089,17 @@ export const platform_integrations = pgTable( (${table.owned_by_user_id} IS NULL AND ${table.owned_by_organization_id} IS NOT NULL) )` ), + check( + 'platform_integrations_access_token_auth_invalidation_pair_check', + sql`( + ${table.platform} <> 'bitbucket' OR + ${table.integration_type} <> 'workspace_access_token' OR + ( + (${table.auth_invalid_at} IS NULL AND ${table.auth_invalid_reason} IS NULL) OR + (${table.auth_invalid_at} IS NOT NULL AND ${table.auth_invalid_reason} IS NOT NULL) + ) + )` + ), ] ); @@ -3124,6 +3145,99 @@ export const user_github_app_tokens = pgTable( export type UserGitHubAppToken = typeof user_github_app_tokens.$inferSelect; export type NewUserGitHubAppToken = typeof user_github_app_tokens.$inferInsert; +export const platform_oauth_credentials = pgTable( + 'platform_oauth_credentials', + { + id: idPrimaryKeyColumn, + platform_integration_id: uuid() + .notNull() + .references(() => platform_integrations.id, { onDelete: 'cascade' }), + platform: text().notNull(), + authorized_by_user_id: text() + .notNull() + .references(() => kilocode_users.id, { onDelete: 'cascade' }), + provider_subject_id: text().notNull(), + provider_subject_login: text().notNull(), + access_token_encrypted: text().notNull(), + access_token_expires_at: timestamp({ withTimezone: true, mode: 'string' }), + refresh_token_encrypted: text().notNull(), + refresh_token_expires_at: timestamp({ withTimezone: true, mode: 'string' }), + credential_version: integer().notNull().default(1), + revoked_at: timestamp({ withTimezone: true, mode: 'string' }), + revocation_reason: text(), + last_used_at: timestamp({ withTimezone: true, mode: 'string' }), + created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + updated_at: timestamp({ withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull() + .$onUpdateFn(() => sql`now()`), + }, + table => [ + uniqueIndex('UQ_platform_oauth_credentials_platform_integration_id').on( + table.platform_integration_id + ), + index('IDX_platform_oauth_credentials_platform_subject').on( + table.platform, + table.provider_subject_id + ), + index('IDX_platform_oauth_credentials_authorized_by_user_id').on(table.authorized_by_user_id), + ] +); + +export type PlatformOAuthCredential = typeof platform_oauth_credentials.$inferSelect; +export type NewPlatformOAuthCredential = typeof platform_oauth_credentials.$inferInsert; + +export const platform_access_token_credentials = pgTable( + 'platform_access_token_credentials', + { + id: idPrimaryKeyColumn, + platform_integration_id: uuid().notNull(), + owned_by_organization_id: uuid().notNull(), + platform: text().notNull().$type<'bitbucket'>(), + integration_type: text().notNull().$type<'workspace_access_token'>(), + token_encrypted: text().notNull(), + expires_at: timestamp({ withTimezone: true, mode: 'string' }), + provider_credential_type: text().notNull().$type<'workspace_access_token'>(), + provider_scopes: text().array().notNull(), + provider_verified_at: timestamp({ withTimezone: true, mode: 'string' }).notNull(), + credential_version: integer().notNull().default(1), + last_validated_at: timestamp({ withTimezone: true, mode: 'string' }).notNull(), + last_used_at: timestamp({ withTimezone: true, mode: 'string' }), + created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + updated_at: timestamp({ withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull() + .$onUpdateFn(() => sql`now()`), + }, + table => [ + unique('UQ_platform_access_token_credentials_platform_integration_id').on( + table.platform_integration_id + ), + check('platform_access_token_credentials_platform_check', sql`${table.platform} = 'bitbucket'`), + check( + 'platform_access_token_credentials_integration_type_check', + sql`${table.integration_type} = 'workspace_access_token'` + ), + check( + 'platform_access_token_credentials_provider_type_check', + sql`${table.provider_credential_type} = 'workspace_access_token'` + ), + check( + 'platform_access_token_credentials_version_positive_check', + sql`${table.credential_version} > 0` + ), + foreignKey({ + columns: [table.platform_integration_id], + foreignColumns: [platform_integrations.id], + name: 'FK_platform_access_token_credentials_parent', + }).onDelete('cascade'), + ] +); + +export type PlatformAccessTokenCredential = typeof platform_access_token_credentials.$inferSelect; +export type NewPlatformAccessTokenCredential = + typeof platform_access_token_credentials.$inferInsert; + // User Deployments export const deployments = pgTable( diff --git a/packages/worker-utils/package.json b/packages/worker-utils/package.json index a1f1d5ae97..e1921a5c23 100644 --- a/packages/worker-utils/package.json +++ b/packages/worker-utils/package.json @@ -5,6 +5,8 @@ "type": "module", "exports": { ".": "./src/index.ts", + "./bitbucket-workspace-access-token": "./src/bitbucket-workspace-access-token.ts", + "./internal-service-token-audiences": "./src/internal-service-token-audiences.ts", "./instance-id": "./src/instance-id.ts", "./kilo-token-auth": "./src/kilo-token-auth.ts", "./sandbox-id": "./src/sandbox-id.ts", diff --git a/packages/worker-utils/src/bitbucket-workspace-access-token.test.ts b/packages/worker-utils/src/bitbucket-workspace-access-token.test.ts new file mode 100644 index 0000000000..7f7fe8331d --- /dev/null +++ b/packages/worker-utils/src/bitbucket-workspace-access-token.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, it } from 'vitest'; +import { + BITBUCKET_ACCESS_TOKEN_FAMILY_PREFIX, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INVALIDATION_REASONS, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_PROVIDER_CREDENTIAL_TYPE, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_REQUIRED_EFFECTIVE_SCOPES, + buildBitbucketOrganizationCredentialLockKey, + buildBitbucketWorkspaceAccessTokenAad, + hasBitbucketAccessTokenFamilyPrefix, + hasRequiredBitbucketWorkspaceAccessTokenScopes, + normalizeBitbucketWorkspaceAccessTokenScopes, +} from './bitbucket-workspace-access-token'; + +const aadInput = { + credentialId: 'credential-1', + integrationId: 'integration-1', + organizationId: 'organization-1', + credentialVersion: 3, +}; + +describe('Bitbucket Workspace Access Token contract', () => { + it('builds the compatible organization credential lock key', () => { + expect( + buildBitbucketOrganizationCredentialLockKey('123e4567-e89b-12d3-a456-426614174030') + ).toBe('bitbucket-oauth-owner:org:123e4567-e89b-12d3-a456-426614174030'); + }); + + it('builds deterministic organization-owned AAD without a Kilo user', () => { + const inputWithUser = { + ...aadInput, + userId: 'must-not-be-bound', + authorizedByUserId: 'must-also-not-be-bound', + }; + const aad = buildBitbucketWorkspaceAccessTokenAad(inputWithUser); + + expect(aad).toBe( + JSON.stringify({ + scheme: BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + version: 1, + platform: 'bitbucket', + credentialId: 'credential-1', + integrationId: 'integration-1', + owner: { type: 'org', id: 'organization-1' }, + integrationType: 'workspace_access_token', + credentialVersion: 3, + }) + ); + expect(aad).not.toContain('must-not-be-bound'); + expect(aad).not.toContain('must-also-not-be-bound'); + expect(buildBitbucketWorkspaceAccessTokenAad(aadInput)).toBe(aad); + }); + + it('normalizes observed scopes without materializing implied scopes', () => { + expect( + normalizeBitbucketWorkspaceAccessTokenScopes( + ' Repository:Write, ACCOUNT repository:write\trepository ' + ) + ).toEqual(['account', 'repository', 'repository:write']); + expect(normalizeBitbucketWorkspaceAccessTokenScopes('repository:write account')).toEqual([ + 'account', + 'repository:write', + ]); + expect( + normalizeBitbucketWorkspaceAccessTokenScopes('repository:write account pullrequest') + ).toEqual(['account', 'pullrequest', 'repository:write']); + expect( + normalizeBitbucketWorkspaceAccessTokenScopes('repository:write account webhook') + ).toEqual(['account', 'repository:write', 'webhook']); + expect(BITBUCKET_WORKSPACE_ACCESS_TOKEN_REQUIRED_EFFECTIVE_SCOPES).toEqual([ + 'account', + 'repository', + 'repository:write', + 'pullrequest', + 'webhook', + ]); + }); + + it('requires effective scopes without rejecting additional observed evidence', () => { + expect(hasRequiredBitbucketWorkspaceAccessTokenScopes(['account', 'repository:write'])).toBe( + false + ); + expect( + hasRequiredBitbucketWorkspaceAccessTokenScopes(['account', 'repository:write', 'webhook']) + ).toBe(false); + expect( + hasRequiredBitbucketWorkspaceAccessTokenScopes([ + 'account', + 'repository', + 'repository:write', + 'pullrequest', + 'webhook', + ]) + ).toBe(true); + + const observedScopes = normalizeBitbucketWorkspaceAccessTokenScopes( + 'pullrequest Repository:Write account webhook' + ); + expect(observedScopes).toEqual(['account', 'pullrequest', 'repository:write', 'webhook']); + expect(hasRequiredBitbucketWorkspaceAccessTokenScopes(observedScopes)).toBe(true); + + expect(hasRequiredBitbucketWorkspaceAccessTokenScopes(['account', 'repository'])).toBe(false); + expect( + hasRequiredBitbucketWorkspaceAccessTokenScopes(['pullrequest', 'repository:write']) + ).toBe(false); + }); + + it('exposes the approved credential and invalidation vocabulary', () => { + expect(BITBUCKET_WORKSPACE_ACCESS_TOKEN_PROVIDER_CREDENTIAL_TYPE).toBe( + 'workspace_access_token' + ); + expect(BITBUCKET_WORKSPACE_ACCESS_TOKEN_INVALIDATION_REASONS).toEqual([ + 'expired', + 'provider_rejected', + 'workspace_mismatch', + 'encryption_unreadable', + ]); + }); + + it('recognizes only the unmodified Bitbucket access-token family prefix', () => { + expect(BITBUCKET_ACCESS_TOKEN_FAMILY_PREFIX).toBe('ATCT'); + expect(hasBitbucketAccessTokenFamilyPrefix('ATCT-valid-looking-token')).toBe(true); + expect(hasBitbucketAccessTokenFamilyPrefix(' ATCT-valid-looking-token')).toBe(false); + expect(hasBitbucketAccessTokenFamilyPrefix('oauth-token')).toBe(false); + }); +}); diff --git a/packages/worker-utils/src/bitbucket-workspace-access-token.ts b/packages/worker-utils/src/bitbucket-workspace-access-token.ts new file mode 100644 index 0000000000..80b200c9af --- /dev/null +++ b/packages/worker-utils/src/bitbucket-workspace-access-token.ts @@ -0,0 +1,85 @@ +export const BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME = + 'bitbucket-workspace-access-token-rsa-aes-256-gcm'; +export const BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_VERSION = 1; +export const BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM = 'bitbucket'; +export const BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE = 'workspace_access_token'; +export const BITBUCKET_WORKSPACE_ACCESS_TOKEN_PROVIDER_CREDENTIAL_TYPE = + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE; +export const BITBUCKET_ACCESS_TOKEN_FAMILY_PREFIX = 'ATCT'; +export const BITBUCKET_WORKSPACE_ACCESS_TOKEN_INVALIDATION_REASONS = [ + 'expired', + 'provider_rejected', + 'workspace_mismatch', + 'encryption_unreadable', +] as const; +export const BITBUCKET_WORKSPACE_ACCESS_TOKEN_REQUIRED_EFFECTIVE_SCOPES = [ + 'account', + 'repository', + 'repository:write', + 'pullrequest', + 'webhook', +] as const; + +export type BitbucketWorkspaceAccessTokenInvalidationReason = + (typeof BITBUCKET_WORKSPACE_ACCESS_TOKEN_INVALIDATION_REASONS)[number]; +export type BitbucketWorkspaceAccessTokenRequiredScope = + (typeof BITBUCKET_WORKSPACE_ACCESS_TOKEN_REQUIRED_EFFECTIVE_SCOPES)[number]; + +export function buildBitbucketOrganizationCredentialLockKey(organizationId: string): string { + return `bitbucket-oauth-owner:org:${organizationId}`; +} + +export type BitbucketWorkspaceAccessTokenAadInput = { + credentialId: string; + integrationId: string; + organizationId: string; + credentialVersion: number; +}; + +export function buildBitbucketWorkspaceAccessTokenAad( + input: BitbucketWorkspaceAccessTokenAadInput +): string { + return JSON.stringify({ + scheme: BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + version: BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_VERSION, + platform: BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM, + credentialId: input.credentialId, + integrationId: input.integrationId, + owner: { type: 'org', id: input.organizationId }, + integrationType: BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE, + credentialVersion: input.credentialVersion, + }); +} + +export function hasBitbucketAccessTokenFamilyPrefix(token: string): boolean { + return token.startsWith(BITBUCKET_ACCESS_TOKEN_FAMILY_PREFIX); +} + +export function normalizeBitbucketWorkspaceAccessTokenScopes(scopeHeader: string): string[] { + return [ + ...new Set( + scopeHeader + .split(/[\s,]+/) + .map(scope => scope.trim().toLowerCase()) + .filter(Boolean) + ), + ].sort(); +} + +export function hasRequiredBitbucketWorkspaceAccessTokenScopes( + observedScopes: readonly string[] +): boolean { + const effectiveScopes = new Set( + observedScopes.map(scope => scope.trim().toLowerCase()).filter(Boolean) + ); + + // Bitbucket documents repository write as implying repository read. Keep the + // implication out of normalization so stored provider evidence stays exact. + if (effectiveScopes.has('repository:write')) { + effectiveScopes.add('repository'); + } + + return BITBUCKET_WORKSPACE_ACCESS_TOKEN_REQUIRED_EFFECTIVE_SCOPES.every(scope => + effectiveScopes.has(scope) + ); +} diff --git a/packages/worker-utils/src/index.ts b/packages/worker-utils/src/index.ts index 61ed9b6876..ee1740038c 100644 --- a/packages/worker-utils/src/index.ts +++ b/packages/worker-utils/src/index.ts @@ -51,6 +51,7 @@ export type { } from './cloud-agent-next-client.js'; export { CloudAgentNextBillingError, CloudAgentNextError } from './cloud-agent-next-client.js'; +export { BITBUCKET_REPOSITORY_LIST_AUDIENCE } from './internal-service-token-audiences.js'; export { signKiloToken, verifyKiloToken, diff --git a/packages/worker-utils/src/internal-service-token-audiences.ts b/packages/worker-utils/src/internal-service-token-audiences.ts new file mode 100644 index 0000000000..9fdc0f1cb4 --- /dev/null +++ b/packages/worker-utils/src/internal-service-token-audiences.ts @@ -0,0 +1 @@ +export const BITBUCKET_REPOSITORY_LIST_AUDIENCE = 'git-token-service:bitbucket-repositories'; diff --git a/packages/worker-utils/src/kilo-token.test.ts b/packages/worker-utils/src/kilo-token.test.ts index 989fd37a2b..ea81c678fd 100644 --- a/packages/worker-utils/src/kilo-token.test.ts +++ b/packages/worker-utils/src/kilo-token.test.ts @@ -158,6 +158,23 @@ describe('verifyKiloToken', () => { await expect(verifyKiloToken(token, SECRET)).rejects.toThrow(); }); + it('requires the expected audience when one is specified', async () => { + const token = await new SignJWT({ version: 3, kiloUserId: 'user-123' }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('1h') + .setAudience('git-token-service:bitbucket-repositories') + .sign(encode(SECRET)); + + await expect( + verifyKiloToken(token, SECRET, { + audience: 'git-token-service:bitbucket-repositories', + }) + ).resolves.toMatchObject({ kiloUserId: 'user-123' }); + await expect(verifyKiloToken(token, SECRET, { audience: 'another-service' })).rejects.toThrow(); + await expect(verifyKiloToken(token, SECRET)).rejects.toThrow(); + }); + it('rejects wrong secret', async () => { const token = await sign({ version: 3, kiloUserId: 'user-123' }); await expect( diff --git a/packages/worker-utils/src/kilo-token.ts b/packages/worker-utils/src/kilo-token.ts index c4264bd372..81d1ed2472 100644 --- a/packages/worker-utils/src/kilo-token.ts +++ b/packages/worker-utils/src/kilo-token.ts @@ -60,6 +60,7 @@ export async function signKiloToken(params: { pepper: string | null; secret: string; expiresInSeconds: number; + audience?: string; env?: string; extra?: SignKiloTokenExtra; }): Promise<{ token: string; expiresAt: string }> { @@ -79,11 +80,12 @@ export async function signKiloToken(params: { const validatedPayload = signKiloTokenPayload.parse(payload); - const token = await new SignJWT(validatedPayload) + let signer = new SignJWT(validatedPayload) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt(now) - .setExpirationTime(exp) - .sign(new TextEncoder().encode(params.secret)); + .setExpirationTime(exp); + if (params.audience) signer = signer.setAudience(params.audience); + const token = await signer.sign(new TextEncoder().encode(params.secret)); return { token, expiresAt: new Date(exp * 1000).toISOString() }; } @@ -96,10 +98,18 @@ export async function signKiloToken(params: { * * @throws if the token is invalid, expired, or fails schema validation. */ -export async function verifyKiloToken(token: string, secret: string): Promise { +export async function verifyKiloToken( + token: string, + secret: string, + options?: { audience?: string } +): Promise { const { payload } = await jwtVerify(token, new TextEncoder().encode(secret), { algorithms: ['HS256'], + audience: options?.audience, }); + if (options?.audience === undefined && payload.aud !== undefined) { + throw new Error('Unexpected token audience'); + } return kiloTokenPayload.parse(payload); } diff --git a/services/cloud-agent-next/src/persistence/CloudAgentSession.ts b/services/cloud-agent-next/src/persistence/CloudAgentSession.ts index 3c4e9fb5b9..cb68c2bd31 100644 --- a/services/cloud-agent-next/src/persistence/CloudAgentSession.ts +++ b/services/cloud-agent-next/src/persistence/CloudAgentSession.ts @@ -225,6 +225,13 @@ type GroupedRegisterSessionInput = { url: string; branch?: string; } + | { + type: 'bitbucket'; + url: string; + workspaceUuid: string; + repositoryUuid: string; + branch?: string; + } | { type: 'git'; url: string; @@ -1621,14 +1628,23 @@ export class CloudAgentSession extends DurableObject { platform: 'gitlab', upstreamBranch: input.repository.branch, } - : input.repository?.type === 'git' + : input.repository?.type === 'bitbucket' ? { - type: 'git', + type: 'bitbucket', url: input.repository.url, - token: input.repository.token, + platform: 'bitbucket', + workspaceUuid: input.repository.workspaceUuid, + repositoryUuid: input.repository.repositoryUuid, upstreamBranch: input.repository.branch, } - : undefined; + : input.repository?.type === 'git' + ? { + type: 'git', + url: input.repository.url, + token: input.repository.token, + upstreamBranch: input.repository.branch, + } + : undefined; const metadata: SessionMetadata = { metadataSchemaVersion: 2, @@ -1832,6 +1848,7 @@ export class CloudAgentSession extends DurableObject { githubAppType?: 'standard' | 'lite'; gitToken?: string; gitlabTokenManaged?: boolean; + bitbucketTokenManaged?: boolean; devcontainer?: SessionMetadata['devcontainer']; }): Promise> { const metadata = await this.getMetadata(); @@ -1855,7 +1872,13 @@ export class CloudAgentSession extends DurableObject { gitlabTokenManaged: input.gitlabTokenManaged ?? metadata.repository.gitlabTokenManaged, } - : metadata.repository; + : metadata.repository?.type === 'bitbucket' + ? { + ...metadata.repository, + bitbucketTokenManaged: + input.bitbucketTokenManaged ?? metadata.repository.bitbucketTokenManaged, + } + : metadata.repository; const updated: SessionMetadata = { ...metadata, diff --git a/services/cloud-agent-next/src/persistence/session-metadata.test.ts b/services/cloud-agent-next/src/persistence/session-metadata.test.ts index 9c4a28efa6..01f2c764ce 100644 --- a/services/cloud-agent-next/src/persistence/session-metadata.test.ts +++ b/services/cloud-agent-next/src/persistence/session-metadata.test.ts @@ -335,6 +335,34 @@ describe('session metadata boundary', () => { expect(serializeSessionMetadata(current)).toEqual(current); }); + it('persists Bitbucket identity and managed status without a token', () => { + const metadata = parseSessionMetadata({ + metadataSchemaVersion: 2, + identity: { sessionId: 'agent_bitbucket', userId: 'user_123' }, + auth: {}, + repository: { + type: 'bitbucket', + url: 'https://bitbucket.org/acme/repo.git', + platform: 'bitbucket', + workspaceUuid: '123e4567-e89b-12d3-a456-426614174020', + repositoryUuid: '123e4567-e89b-12d3-a456-426614174021', + bitbucketTokenManaged: true, + token: 'must-not-persist', + }, + lifecycle: { version: 1, timestamp: 1 }, + }); + + expect(metadata.repository).toEqual({ + type: 'bitbucket', + url: 'https://bitbucket.org/acme/repo.git', + platform: 'bitbucket', + workspaceUuid: '123e4567-e89b-12d3-a456-426614174020', + repositoryUuid: '123e4567-e89b-12d3-a456-426614174021', + bitbucketTokenManaged: true, + }); + expect(JSON.stringify(serializeSessionMetadata(metadata))).not.toContain('must-not-persist'); + }); + it('preserves legacy generic git tokens in grouped repository metadata', () => { const metadata = parseSessionMetadata({ version: 1, diff --git a/services/cloud-agent-next/src/persistence/session-metadata.ts b/services/cloud-agent-next/src/persistence/session-metadata.ts index 647a4e3ef5..20883f1535 100644 --- a/services/cloud-agent-next/src/persistence/session-metadata.ts +++ b/services/cloud-agent-next/src/persistence/session-metadata.ts @@ -62,6 +62,17 @@ const MetadataRepositorySchema = z.discriminatedUnion('type', [ ...RepositoryCommonSchema, }) .strip(), + z + .object({ + type: z.literal('bitbucket'), + url: z.string(), + platform: z.literal('bitbucket').optional(), + workspaceUuid: z.string().uuid(), + repositoryUuid: z.string().uuid(), + bitbucketTokenManaged: z.boolean().optional(), + upstreamBranch: branchNameSchema.optional(), + }) + .strip(), z .object({ type: z.literal('git'), diff --git a/services/cloud-agent-next/src/router/handlers/session-management.ts b/services/cloud-agent-next/src/router/handlers/session-management.ts index 9fa34c5419..cd7b530c37 100644 --- a/services/cloud-agent-next/src/router/handlers/session-management.ts +++ b/services/cloud-agent-next/src/router/handlers/session-management.ts @@ -32,17 +32,27 @@ import { requireCurrentSessionAccess } from '../../session-access.js'; function publicRepositoryFields(metadata: CloudAgentSessionState): { githubRepo?: string; gitUrl?: string; - platform?: 'github' | 'gitlab'; + platform?: 'github' | 'gitlab' | 'bitbucket'; + bitbucketWorkspaceUuid?: string; + bitbucketRepositoryUuid?: string; } { const repository = metadata.repository; if (!repository) return {}; - if (repository.type === 'github') { - return { githubRepo: repository.repo, platform: repository.platform ?? 'github' }; + switch (repository.type) { + case 'github': + return { githubRepo: repository.repo, platform: repository.platform ?? 'github' }; + case 'gitlab': + return { gitUrl: repository.url, platform: 'gitlab' }; + case 'bitbucket': + return { + gitUrl: repository.url, + platform: 'bitbucket', + bitbucketWorkspaceUuid: repository.workspaceUuid, + bitbucketRepositoryUuid: repository.repositoryUuid, + }; + case 'git': + return { gitUrl: repository.url, platform: repository.platform }; } - return { - gitUrl: repository.url, - platform: repository.platform ?? (repository.type === 'gitlab' ? 'gitlab' : undefined), - }; } async function deleteSessionResources( diff --git a/services/cloud-agent-next/src/router/handlers/session-prepare.ts b/services/cloud-agent-next/src/router/handlers/session-prepare.ts index 4f5ee18520..ea2c557c14 100644 --- a/services/cloud-agent-next/src/router/handlers/session-prepare.ts +++ b/services/cloud-agent-next/src/router/handlers/session-prepare.ts @@ -42,6 +42,7 @@ import type { Env } from '../../types.js'; import type { SessionProfileBundle } from '../../session-profile.js'; import type { SessionCreateRequest } from '../../session/session-requests.js'; import { assertKiloModelAvailable } from '../../model-validation.js'; +import { assertBitbucketRepositoryAccessBeforeSessionCreation } from '../../session/validate-repository-access.js'; type SessionPrepareHandlers = { prepareSession: typeof prepareSessionHandler; @@ -210,19 +211,32 @@ export function prepareInputToSessionCreateRequest(input: PrepareInput): Session message: 'Must provide either githubRepo or gitUrl', }); } - repository = - input.platform === 'gitlab' - ? { - type: 'gitlab', - url: gitUrl, - branch: input.upstreamBranch, - } - : { - type: 'git', - url: gitUrl, - token: input.gitToken, - branch: input.upstreamBranch, - }; + if (input.platform === 'gitlab') { + repository = { + type: 'gitlab', + url: gitUrl, + branch: input.upstreamBranch, + }; + } else if ( + input.platform === 'bitbucket' && + input.bitbucketWorkspaceUuid && + input.bitbucketRepositoryUuid + ) { + repository = { + type: 'bitbucket', + url: gitUrl, + workspaceUuid: input.bitbucketWorkspaceUuid, + repositoryUuid: input.bitbucketRepositoryUuid, + branch: input.upstreamBranch, + }; + } else { + repository = { + type: 'git', + url: gitUrl, + token: input.gitToken, + branch: input.upstreamBranch, + }; + } } const initialTurn: SessionCreateRequest['initialTurn'] = @@ -298,6 +312,12 @@ const prepareSessionHandler = internalApiProtectedProcedure .mutation(async ({ input, ctx }) => { return withLogTags({ source: 'prepareSession' }, async () => { const request = prepareInputToSessionCreateRequest(input); + await assertBitbucketRepositoryAccessBeforeSessionCreation({ + env: ctx.env, + userId: ctx.userId, + orgId: input.kilocodeOrganizationId, + repository: request.repository, + }); const policy = profileResolutionPolicyForSessionCreateOrigin(input.createdOnPlatform); const requestWithProfile = await resolveEffectiveSessionConfiguration(ctx, request, policy); assertModeAvailableForProfile( diff --git a/services/cloud-agent-next/src/router/handlers/session-start.ts b/services/cloud-agent-next/src/router/handlers/session-start.ts index c55a4b45ad..acadee6c97 100644 --- a/services/cloud-agent-next/src/router/handlers/session-start.ts +++ b/services/cloud-agent-next/src/router/handlers/session-start.ts @@ -27,6 +27,7 @@ import { } from './session-prepare.js'; import type { SessionCreateRequest } from '../../session/session-requests.js'; import { assertKiloModelAvailable } from '../../model-validation.js'; +import { assertBitbucketRepositoryAccessBeforeSessionCreation } from '../../session/validate-repository-access.js'; type SessionStartHandlers = { start: typeof startSessionHandler; @@ -42,6 +43,28 @@ function startInputToSessionCreateRequest( const repo = input.repository; const profile = input.profile; + let repository: SessionCreateRequest['repository']; + switch (repo.type) { + case 'github': + repository = { type: 'github', repo: repo.repo, branch: repo.branch }; + break; + case 'gitlab': + repository = { type: 'gitlab', url: repo.url, branch: repo.branch }; + break; + case 'bitbucket': + repository = { + type: 'bitbucket', + url: repo.url, + workspaceUuid: repo.workspaceUuid, + repositoryUuid: repo.repositoryUuid, + branch: repo.branch, + }; + break; + case 'git': + repository = { type: 'git', url: repo.url, token: repo.token, branch: repo.branch }; + break; + } + return { initialTurn: { type: 'prompt', @@ -50,12 +73,7 @@ function startInputToSessionCreateRequest( attachments: input.message.attachments ?? input.message.images, }, agent: input.agent, - repository: - repo.type === 'github' - ? { type: 'github', repo: repo.repo, branch: repo.branch } - : repo.type === 'gitlab' - ? { type: 'gitlab', url: repo.url, branch: repo.branch } - : { type: 'git', url: repo.url, token: repo.token, branch: repo.branch }, + repository, profile: profile ? { id: profile.id, @@ -108,6 +126,12 @@ const startSessionHandler = protectedProcedure db = getPgDb(ctx.env); await assertOrganizationMembership(db, ctx.userId, organizationId); } + await assertBitbucketRepositoryAccessBeforeSessionCreation({ + env: ctx.env, + userId: ctx.userId, + orgId: organizationId, + repository: request.repository, + }); const policy = profileResolutionPolicyForSessionCreateOrigin( input.options?.createdOnPlatform diff --git a/services/cloud-agent-next/src/router/schemas.test.ts b/services/cloud-agent-next/src/router/schemas.test.ts index 9b20c435c3..f2e17ed7ee 100644 --- a/services/cloud-agent-next/src/router/schemas.test.ts +++ b/services/cloud-agent-next/src/router/schemas.test.ts @@ -156,9 +156,42 @@ describe('grouped unified session input contracts', () => { expect(SendMessageInput.parse(input)).toEqual(input); }); + + it('requires stable workspace and repository UUIDs for Bitbucket starts', () => { + const repository = { + type: 'bitbucket' as const, + url: 'https://bitbucket.org/acme/repo.git', + workspaceUuid: '123e4567-e89b-12d3-a456-426614174020', + repositoryUuid: '123e4567-e89b-12d3-a456-426614174021', + }; + expect(StartSessionInput.safeParse({ ...baseStartInput, repository }).success).toBe(true); + expect( + StartSessionInput.safeParse({ + ...baseStartInput, + repository: { type: 'bitbucket', url: repository.url }, + }).success + ).toBe(false); + }); }); describe('legacy live attachment input compatibility', () => { + it('requires paired Bitbucket identity fields on prepareSession', () => { + const input = { + prompt: 'Update the repository', + mode: 'code', + model: 'claude-sonnet-4-5-20250929', + gitUrl: 'https://bitbucket.org/acme/repo.git', + platform: 'bitbucket', + bitbucketWorkspaceUuid: '123e4567-e89b-12d3-a456-426614174020', + bitbucketRepositoryUuid: '123e4567-e89b-12d3-a456-426614174021', + }; + expect(PrepareSessionInput.safeParse(input).success).toBe(true); + expect( + PrepareSessionInput.safeParse({ ...input, bitbucketRepositoryUuid: undefined }).success + ).toBe(false); + expect(PrepareSessionInput.safeParse({ ...input, platform: 'gitlab' }).success).toBe(false); + }); + it('accepts document attachments on prepareSession while retaining images', () => { const basePrepareInput = { prompt: 'Summarize this document', diff --git a/services/cloud-agent-next/src/router/schemas.ts b/services/cloud-agent-next/src/router/schemas.ts index 03e7428737..4af1781be7 100644 --- a/services/cloud-agent-next/src/router/schemas.ts +++ b/services/cloud-agent-next/src/router/schemas.ts @@ -388,9 +388,11 @@ export const PrepareSessionInput = z 'Git token for generic git repositories. Ignored when platform selects a managed provider.' ), platform: z - .enum(['github', 'gitlab']) + .enum(['github', 'gitlab', 'bitbucket']) .optional() .describe('Git platform type for correct token/env var handling'), + bitbucketWorkspaceUuid: z.string().uuid().optional(), + bitbucketRepositoryUuid: z.string().uuid().optional(), // Optional configuration envVars: envVarsSchema.optional().describe('Environment variables to inject into the session'), @@ -512,6 +514,27 @@ export const PrepareSessionInput = z message: 'Must provide either githubRepo or gitUrl, but not both', path: ['githubRepo'], }) + .superRefine((data, ctx) => { + const hasBitbucketIds = + data.bitbucketWorkspaceUuid !== undefined && data.bitbucketRepositoryUuid !== undefined; + if ( + (data.bitbucketWorkspaceUuid === undefined) !== + (data.bitbucketRepositoryUuid === undefined) + ) { + ctx.addIssue({ + code: 'custom', + path: ['bitbucketWorkspaceUuid'], + message: 'Bitbucket workspace and repository UUIDs must be provided together', + }); + } + if ((data.platform === 'bitbucket') !== hasBitbucketIds) { + ctx.addIssue({ + code: 'custom', + path: ['platform'], + message: 'Bitbucket identity is required only for Bitbucket repositories', + }); + } + }) .superRefine(rejectAmbiguousAttachments) .refine(requiresAppendSystemPrompt, { message: 'appendSystemPrompt is required when mode is custom', @@ -562,6 +585,13 @@ export const RepositoryInputSchema = z.discriminatedUnion('type', [ url: gitUrlSchema.describe('GitLab repository HTTPS URL'), branch: branchNameSchema.optional().describe('Branch to checkout'), }), + z.object({ + type: z.literal('bitbucket'), + url: gitUrlSchema.describe('Bitbucket Cloud repository HTTPS URL'), + workspaceUuid: z.string().uuid(), + repositoryUuid: z.string().uuid(), + branch: branchNameSchema.optional().describe('Branch to checkout'), + }), z.object({ type: z.literal('git'), url: gitUrlSchema.describe('Git repository HTTPS URL'), @@ -802,7 +832,9 @@ export const GetSessionOutput = z.object({ // Repository info (no tokens) githubRepo: z.string().optional().describe('GitHub repository in org/repo format'), gitUrl: z.string().optional().describe('Generic git URL'), - platform: z.enum(['github', 'gitlab']).optional().describe('Git platform type'), + platform: z.enum(['github', 'gitlab', 'bitbucket']).optional().describe('Git platform type'), + bitbucketWorkspaceUuid: z.string().uuid().optional(), + bitbucketRepositoryUuid: z.string().uuid().optional(), // Execution params prompt: z.string().optional().describe('Task prompt'), diff --git a/services/cloud-agent-next/src/services/git-token-service-client.test.ts b/services/cloud-agent-next/src/services/git-token-service-client.test.ts index a2d2a54f61..114bb87009 100644 --- a/services/cloud-agent-next/src/services/git-token-service-client.test.ts +++ b/services/cloud-agent-next/src/services/git-token-service-client.test.ts @@ -1,13 +1,17 @@ import { describe, expect, it, vi } from 'vitest'; +import { logger } from '../logger.js'; import type { GitTokenService } from '../types.js'; import { resolveCloudAgentGitHubAuthForRepo, + resolveManagedBitbucketToken, resolveManagedGitLabToken, } from './git-token-service-client.js'; vi.mock('../logger.js', () => ({ logger: { info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), withFields: vi.fn(() => ({ info: vi.fn(), error: vi.fn(), warn: vi.fn() })), }, })); @@ -24,6 +28,81 @@ function createEnv(service: Partial) { return { GIT_TOKEN_SERVICE: service as GitTokenService }; } +describe('resolveManagedBitbucketToken', () => { + const repositoryParams = { + userId: 'user_123', + workspaceUuid: '123e4567-e89b-12d3-a456-426614174020', + repositoryUuid: '123e4567-e89b-12d3-a456-426614174021', + repositoryUrl: 'https://bitbucket.org/acme/repo.git', + }; + + it('rejects a missing organization before invoking the service binding', async () => { + const getBitbucketToken = vi.fn().mockResolvedValue({ success: true, token: 'opaque-token' }); + + await expect( + resolveManagedBitbucketToken(createEnv({ getBitbucketToken }), repositoryParams as never) + ).resolves.toEqual({ success: false, reason: 'invalid_request' }); + expect(getBitbucketToken).not.toHaveBeenCalled(); + }); + + it('forwards exact organization and repository identity and returns the token unchanged', async () => { + const getBitbucketToken = vi.fn().mockResolvedValue({ + success: true, + token: 'opaque-workspace-token', + }); + const params = { + ...repositoryParams, + orgId: '123e4567-e89b-12d3-a456-426614174030', + }; + + await expect( + resolveManagedBitbucketToken(createEnv({ getBitbucketToken }), params) + ).resolves.toEqual({ success: true, token: 'opaque-workspace-token' }); + expect(getBitbucketToken).toHaveBeenCalledWith(params); + }); + + it.each(['insufficient_permissions', 'temporarily_unavailable'] as const)( + 'preserves the %s resolver failure', + async reason => { + const getBitbucketToken = vi.fn().mockResolvedValue({ success: false, reason }); + + await expect( + resolveManagedBitbucketToken(createEnv({ getBitbucketToken }), { + ...repositoryParams, + orgId: '123e4567-e89b-12d3-a456-426614174030', + }) + ).resolves.toEqual({ success: false, reason }); + } + ); + + it('normalizes a missing service binding to temporary unavailability', async () => { + await expect( + resolveManagedBitbucketToken( + {}, + { + ...repositoryParams, + orgId: '123e4567-e89b-12d3-a456-426614174030', + } + ) + ).resolves.toEqual({ success: false, reason: 'temporarily_unavailable' }); + expect(logger.warn).toHaveBeenCalledWith( + 'Bitbucket git-token-service binding is not configured' + ); + }); + + it('normalizes an RPC exception to temporary unavailability', async () => { + const getBitbucketToken = vi.fn().mockRejectedValue(new Error('binding unavailable')); + + await expect( + resolveManagedBitbucketToken(createEnv({ getBitbucketToken }), { + ...repositoryParams, + orgId: '123e4567-e89b-12d3-a456-426614174030', + }) + ).resolves.toEqual({ success: false, reason: 'temporarily_unavailable' }); + expect(logger.error).toHaveBeenCalledWith('Failed to call git-token-service getBitbucketToken'); + }); +}); + describe('resolveManagedGitLabToken', () => { const reviewParams = { userId: 'user_123', diff --git a/services/cloud-agent-next/src/services/git-token-service-client.ts b/services/cloud-agent-next/src/services/git-token-service-client.ts index 7199535f56..3e45cff481 100644 --- a/services/cloud-agent-next/src/services/git-token-service-client.ts +++ b/services/cloud-agent-next/src/services/git-token-service-client.ts @@ -1,5 +1,10 @@ import { logger } from '../logger.js'; -import type { GitAuthorConfig, GitTokenService, ManagedGitHubFallbackReason } from '../types.js'; +import type { + BitbucketTokenFailureReason, + GitAuthorConfig, + GitTokenService, + ManagedGitHubFallbackReason, +} from '../types.js'; type GitTokenServiceEnv = { GIT_TOKEN_SERVICE?: GitTokenService; @@ -180,6 +185,42 @@ export type ResolveManagedGitLabTokenResult = | { success: true; token: string; glabIsOAuth2: boolean } | { success: false; reason: string }; +export type ResolveManagedBitbucketTokenResult = + | { success: true; token: string } + | { success: false; reason: BitbucketTokenFailureReason }; + +export async function resolveManagedBitbucketToken( + env: GitTokenServiceEnv, + params: { + userId: string; + orgId: string; + workspaceUuid: string; + repositoryUuid: string; + repositoryUrl: string; + } +): Promise { + if (!params.orgId) { + return { success: false, reason: 'invalid_request' }; + } + + try { + if (!env.GIT_TOKEN_SERVICE?.getBitbucketToken) { + logger.warn('Bitbucket git-token-service binding is not configured'); + return { success: false, reason: 'temporarily_unavailable' }; + } + const result = await env.GIT_TOKEN_SERVICE.getBitbucketToken(params); + if (result.success) { + logger.info('Resolved Bitbucket token via git-token-service'); + return { success: true, token: result.token }; + } + logger.withFields({ reason: result.reason }).info('Bitbucket token lookup failed'); + return { success: false, reason: result.reason }; + } catch { + logger.error('Failed to call git-token-service getBitbucketToken'); + return { success: false, reason: 'temporarily_unavailable' }; + } +} + export async function resolveManagedGitLabToken( env: GitTokenServiceEnv, params: { diff --git a/services/cloud-agent-next/src/session-service.test.ts b/services/cloud-agent-next/src/session-service.test.ts index 808a504b41..a5b28306b9 100644 --- a/services/cloud-agent-next/src/session-service.test.ts +++ b/services/cloud-agent-next/src/session-service.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type * as DevContainerModule from './kilo/devcontainer.js'; +import type * as GitTokenServiceClientModule from './services/git-token-service-client.js'; vi.mock('./logger.js', () => ({ logger: { @@ -41,6 +42,7 @@ vi.mock('./workspace.js', () => ({ const tokenMocks = vi.hoisted(() => ({ resolveCloudAgentGitHubAuthForRepo: vi.fn(), + resolveManagedBitbucketToken: vi.fn(), resolveManagedGitLabToken: vi.fn(), })); const devcontainerMocks = vi.hoisted(() => ({ @@ -54,7 +56,10 @@ const attachmentMocks = vi.hoisted(() => ({ buildSignedPromptAttachments: vi.fn().mockResolvedValue([]), })); -vi.mock('./services/git-token-service-client.js', () => tokenMocks); +vi.mock('./services/git-token-service-client.js', async importActual => ({ + ...(await importActual()), + ...tokenMocks, +})); vi.mock('./kilo/devcontainer.js', async importActual => ({ ...(await importActual()), bringUpDevContainer: devcontainerMocks.bringUpDevContainer, @@ -281,6 +286,83 @@ function createGitLabCodeReviewMetadata(): CloudAgentSessionState { }); } +function createBitbucketMetadata(orgId?: string): CloudAgentSessionState { + return parseSessionMetadata({ + metadataSchemaVersion: 2, + identity: { + sessionId: 'agent_bitbucket', + userId: 'user_test', + ...(orgId ? { orgId } : {}), + }, + auth: {}, + repository: { + type: 'bitbucket', + url: 'https://bitbucket.org/acme/repo.git', + platform: 'bitbucket', + workspaceUuid: '123e4567-e89b-12d3-a456-426614174020', + repositoryUuid: '123e4567-e89b-12d3-a456-426614174021', + }, + lifecycle: { version: 1, timestamp: 1 }, + }); +} + +describe('SessionService.resolveWorkspaceTokens', () => { + beforeEach(() => { + vi.clearAllMocks(); + tokenMocks.resolveManagedBitbucketToken.mockResolvedValue({ + success: true, + token: 'opaque-workspace-token', + }); + }); + + it('fails closed for replayed Bitbucket metadata without an organization', async () => { + await expect( + new SessionService().resolveWorkspaceTokens(createEnv(), createBitbucketMetadata()) + ).rejects.toMatchObject({ + code: 'INVALID_REQUEST', + retryable: false, + message: 'Bitbucket repositories require an organization', + }); + expect(tokenMocks.resolveManagedBitbucketToken).not.toHaveBeenCalled(); + }); + + it('re-resolves an organization token with exact persisted repository identity', async () => { + const orgId = '123e4567-e89b-12d3-a456-426614174030'; + + await expect( + new SessionService().resolveWorkspaceTokens(createEnv(), createBitbucketMetadata(orgId)) + ).resolves.toMatchObject({ + gitToken: 'opaque-workspace-token', + bitbucketTokenManaged: true, + }); + expect(tokenMocks.resolveManagedBitbucketToken).toHaveBeenCalledWith(expect.any(Object), { + userId: 'user_test', + orgId, + workspaceUuid: '123e4567-e89b-12d3-a456-426614174020', + repositoryUuid: '123e4567-e89b-12d3-a456-426614174021', + repositoryUrl: 'https://bitbucket.org/acme/repo.git', + }); + }); + + it('keeps temporary runtime token resolution failures retryable', async () => { + tokenMocks.resolveManagedBitbucketToken.mockResolvedValue({ + success: false, + reason: 'temporarily_unavailable', + }); + + await expect( + new SessionService().resolveWorkspaceTokens( + createEnv(), + createBitbucketMetadata('123e4567-e89b-12d3-a456-426614174030') + ) + ).rejects.toMatchObject({ + code: 'WORKSPACE_SETUP_FAILED', + retryable: true, + message: 'Bitbucket repository authorization failed (temporarily_unavailable).', + }); + }); +}); + describe('writeGlobalRules', () => { it('writes the shared Cloud Agent rules for the session', async () => { const writeFile = vi.fn().mockResolvedValue(undefined); diff --git a/services/cloud-agent-next/src/session-service.ts b/services/cloud-agent-next/src/session-service.ts index a2773ed44a..5edec2c634 100644 --- a/services/cloud-agent-next/src/session-service.ts +++ b/services/cloud-agent-next/src/session-service.ts @@ -12,6 +12,7 @@ import { generateSandboxId } from './sandbox-id.js'; import { normalizeKilocodeModel } from './persistence/model-utils.js'; import { resolveCloudAgentGitHubAuthForRepo, + resolveManagedBitbucketToken, resolveManagedGitLabToken, } from './services/git-token-service-client.js'; import { ExecutionError } from './execution/errors.js'; @@ -472,6 +473,7 @@ export type ResolvedWorkspaceTokens = { githubFallbackReason?: ManagedGitHubFallbackReason; gitToken?: string; gitlabTokenManaged?: boolean; + bitbucketTokenManaged?: boolean; glabIsOAuth2?: boolean; }; @@ -933,15 +935,22 @@ function githubRepository(metadata: CloudAgentSessionState) { function gitRepository(metadata: CloudAgentSessionState) { const repository = metadata.repository; - return repository?.type === 'git' || repository?.type === 'gitlab' ? repository : undefined; + return repository?.type === 'git' || + repository?.type === 'gitlab' || + repository?.type === 'bitbucket' + ? repository + : undefined; } -function repositoryPlatform(metadata: CloudAgentSessionState): 'github' | 'gitlab' | undefined { +function repositoryPlatform( + metadata: CloudAgentSessionState +): 'github' | 'gitlab' | 'bitbucket' | undefined { const repository = metadata.repository; if (!repository) return undefined; if (repository.platform) return repository.platform; if (repository.type === 'github') return 'github'; if (repository.type === 'gitlab') return 'gitlab'; + if (repository.type === 'bitbucket') return 'bitbucket'; return undefined; } @@ -1022,12 +1031,15 @@ export class SessionService { gitUrl?: string; gitToken?: string; gitlabTokenManaged?: boolean; + bitbucketTokenManaged?: boolean; + bitbucketWorkspaceUuid?: string; + bitbucketRepositoryUuid?: string; glabIsOAuth2?: boolean; upstreamBranch?: string; branchName?: string; envVars?: Record; botId?: string; - platform?: 'github' | 'gitlab'; + platform?: 'github' | 'gitlab' | 'bitbucket'; }): SessionContext { const sessionHome = options.sessionHome ?? getSessionHomePath(options.sessionId); const workspacePath = @@ -1052,6 +1064,9 @@ export class SessionService { gitUrl: options.gitUrl, gitToken: options.gitToken, gitlabTokenManaged: options.gitlabTokenManaged, + bitbucketTokenManaged: options.bitbucketTokenManaged, + bitbucketWorkspaceUuid: options.bitbucketWorkspaceUuid, + bitbucketRepositoryUuid: options.bitbucketRepositoryUuid, glabIsOAuth2: options.glabIsOAuth2, platform: options.platform, envVars: options.envVars, @@ -1442,8 +1457,10 @@ export class SessionService { throw ExecutionError.invalidRequest('GitHub authentication required for this repository'); } - let gitToken = repositoryPlatform(metadata) === 'gitlab' ? undefined : git?.token; + const platform = repositoryPlatform(metadata); + let gitToken = git?.type === 'git' ? git.token : undefined; let gitlabTokenManaged = git?.type === 'gitlab' ? git.gitlabTokenManaged : undefined; + let bitbucketTokenManaged = git?.type === 'bitbucket' ? git.bitbucketTokenManaged : undefined; let glabIsOAuth2: boolean | undefined; if (git?.url && repositoryPlatform(metadata) === 'gitlab') { if (!env.GIT_TOKEN_SERVICE) { @@ -1465,12 +1482,40 @@ export class SessionService { } } - if (git?.url && repositoryPlatform(metadata) === 'gitlab' && !gitToken) { + if (git?.url && platform === 'gitlab' && !gitToken) { throw ExecutionError.invalidRequest( 'No GitLab integration found. Please connect your GitLab account first.' ); } + if (git?.type === 'bitbucket') { + if (!metadata.identity.orgId) { + throw ExecutionError.invalidRequest('Bitbucket repositories require an organization'); + } + + const result = await resolveManagedBitbucketToken(env, { + userId: metadata.identity.userId, + orgId: metadata.identity.orgId, + workspaceUuid: git.workspaceUuid, + repositoryUuid: git.repositoryUuid, + repositoryUrl: git.url, + }); + if (!result.success) { + const reconnect = result.reason === 'reconnect_required' ? ' Reconnect Bitbucket.' : ''; + const message = `Bitbucket repository authorization failed (${result.reason}).${reconnect}`; + if (result.reason === 'temporarily_unavailable') { + throw ExecutionError.workspaceSetupFailed(message); + } + throw ExecutionError.invalidRequest(message); + } + gitToken = result.token; + bitbucketTokenManaged = true; + } + + if (git?.type === 'bitbucket' && !gitToken) { + throw ExecutionError.invalidRequest('Bitbucket authentication required for this repository'); + } + return { githubToken, githubInstallationId, @@ -1481,6 +1526,7 @@ export class SessionService { githubFallbackReason, gitToken, gitlabTokenManaged, + bitbucketTokenManaged, glabIsOAuth2, }; } @@ -1533,6 +1579,9 @@ export class SessionService { gitUrl: git?.url, gitToken: resolvedTokens.gitToken, gitlabTokenManaged: resolvedTokens.gitlabTokenManaged, + bitbucketTokenManaged: resolvedTokens.bitbucketTokenManaged, + bitbucketWorkspaceUuid: git?.type === 'bitbucket' ? git.workspaceUuid : undefined, + bitbucketRepositoryUuid: git?.type === 'bitbucket' ? git.repositoryUuid : undefined, glabIsOAuth2: resolvedTokens.glabIsOAuth2, upstreamBranch: metadata.repository?.upstreamBranch, branchName, @@ -1570,6 +1619,7 @@ export class SessionService { githubAppType: resolvedTokens.githubAppType, gitToken: resolvedTokens.gitToken, gitlabTokenManaged: resolvedTokens.gitlabTokenManaged, + bitbucketTokenManaged: resolvedTokens.bitbucketTokenManaged, ...(metadata.devcontainer ? { devcontainer: metadata.devcontainer } : {}), } satisfies WrapperWorkspaceReady; @@ -1700,7 +1750,7 @@ export class SessionService { ...(repositoryShallow(metadata) !== undefined ? { shallow: repositoryShallow(metadata) } : {}), - refreshRemote: tokens.gitlabTokenManaged === true, + refreshRemote: tokens.gitlabTokenManaged === true || tokens.bitbucketTokenManaged === true, }; } @@ -1758,6 +1808,9 @@ export class SessionService { gitUrl: git?.url, gitToken: resolvedTokens.gitToken, gitlabTokenManaged: resolvedTokens.gitlabTokenManaged, + bitbucketTokenManaged: resolvedTokens.bitbucketTokenManaged, + bitbucketWorkspaceUuid: git?.type === 'bitbucket' ? git.workspaceUuid : undefined, + bitbucketRepositoryUuid: git?.type === 'bitbucket' ? git.repositoryUuid : undefined, glabIsOAuth2: resolvedTokens.glabIsOAuth2, upstreamBranch: metadata.repository?.upstreamBranch, branchName, @@ -1776,6 +1829,7 @@ export class SessionService { githubAppType: resolvedTokens.githubAppType, gitToken: resolvedTokens.gitToken, gitlabTokenManaged: resolvedTokens.gitlabTokenManaged, + bitbucketTokenManaged: resolvedTokens.bitbucketTokenManaged, devcontainer: metadata.devcontainer, } satisfies PreparedWorkspace['ready']; const runtimeEnv = this.buildRuntimeEnv({ @@ -2126,7 +2180,10 @@ export class SessionService { const git = gitRepository(metadata); if (git) { - if (tokens.gitToken !== undefined && tokens.gitlabTokenManaged === true) { + if ( + tokens.gitToken !== undefined && + (tokens.gitlabTokenManaged === true || tokens.bitbucketTokenManaged === true) + ) { await updateGitRemoteToken( session, context.workspacePath, @@ -2410,7 +2467,7 @@ type GetSaferEnvVarsOptions = { gitUrl?: string; gitToken?: string; glabIsOAuth2?: boolean; - platform?: 'github' | 'gitlab'; + platform?: 'github' | 'gitlab' | 'bitbucket'; profile?: SessionProfileBundle; }; @@ -2424,6 +2481,7 @@ export type WorkspaceReadyMetadata = { githubAppType?: 'standard' | 'lite'; gitToken?: string; gitlabTokenManaged?: boolean; + bitbucketTokenManaged?: boolean; devcontainer?: CloudAgentSessionState['devcontainer']; }; diff --git a/services/cloud-agent-next/src/session/session-requests.ts b/services/cloud-agent-next/src/session/session-requests.ts index 5d77246ee2..e5f0d5b1e0 100644 --- a/services/cloud-agent-next/src/session/session-requests.ts +++ b/services/cloud-agent-next/src/session/session-requests.ts @@ -27,6 +27,13 @@ export type SessionRepositoryRequest = url: string; branch?: string; } + | { + type: 'bitbucket'; + url: string; + workspaceUuid: string; + repositoryUuid: string; + branch?: string; + } | { type: 'git'; url: string; diff --git a/services/cloud-agent-next/src/session/validate-repository-access.test.ts b/services/cloud-agent-next/src/session/validate-repository-access.test.ts new file mode 100644 index 0000000000..9bab2e466f --- /dev/null +++ b/services/cloud-agent-next/src/session/validate-repository-access.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, it, vi } from 'vitest'; +import { assertBitbucketRepositoryAccessBeforeSessionCreation } from './validate-repository-access.js'; + +const repository = { + type: 'bitbucket' as const, + url: 'https://bitbucket.org/acme/repo.git', + workspaceUuid: '123e4567-e89b-12d3-a456-426614174020', + repositoryUuid: '123e4567-e89b-12d3-a456-426614174021', +}; + +describe('Bitbucket session creation preflight', () => { + it('validates organization sessions against the organization-owned integration', async () => { + const getBitbucketToken = vi.fn().mockResolvedValue({ success: true, token: 'token' }); + const orgId = '123e4567-e89b-12d3-a456-426614174030'; + + await expect( + assertBitbucketRepositoryAccessBeforeSessionCreation({ + env: { GIT_TOKEN_SERVICE: { getBitbucketToken } } as never, + userId: 'user-1', + orgId, + repository, + }) + ).resolves.toBeUndefined(); + expect(getBitbucketToken).toHaveBeenCalledWith({ + userId: 'user-1', + orgId, + workspaceUuid: repository.workspaceUuid, + repositoryUuid: repository.repositoryUuid, + repositoryUrl: repository.url, + }); + }); + + it('rejects personal Bitbucket sessions before invoking the service binding', async () => { + const getBitbucketToken = vi.fn().mockResolvedValue({ success: true, token: 'token' }); + + await expect( + assertBitbucketRepositoryAccessBeforeSessionCreation({ + env: { GIT_TOKEN_SERVICE: { getBitbucketToken } } as never, + userId: 'user-1', + repository, + }) + ).rejects.toMatchObject({ + code: 'BAD_REQUEST', + message: 'Bitbucket repositories require an organization', + }); + expect(getBitbucketToken).not.toHaveBeenCalled(); + }); + + it('keeps insufficient workspace permissions distinguishable', async () => { + const getBitbucketToken = vi.fn().mockResolvedValue({ + success: false, + reason: 'insufficient_permissions', + }); + + await expect( + assertBitbucketRepositoryAccessBeforeSessionCreation({ + env: { GIT_TOKEN_SERVICE: { getBitbucketToken } } as never, + userId: 'user-1', + orgId: '123e4567-e89b-12d3-a456-426614174030', + repository, + }) + ).rejects.toMatchObject({ + code: 'BAD_REQUEST', + message: 'Bitbucket repository authorization failed (insufficient_permissions)', + }); + }); + + it('reports temporary provider failures as service unavailable', async () => { + const getBitbucketToken = vi.fn().mockResolvedValue({ + success: false, + reason: 'temporarily_unavailable', + }); + + await expect( + assertBitbucketRepositoryAccessBeforeSessionCreation({ + env: { GIT_TOKEN_SERVICE: { getBitbucketToken } } as never, + userId: 'user-1', + orgId: '123e4567-e89b-12d3-a456-426614174030', + repository, + }) + ).rejects.toMatchObject({ + code: 'SERVICE_UNAVAILABLE', + message: 'Bitbucket repository authorization failed (temporarily_unavailable)', + }); + }); + + it('reports an unavailable token-service binding as service unavailable', async () => { + await expect( + assertBitbucketRepositoryAccessBeforeSessionCreation({ + env: {} as never, + userId: 'user-1', + orgId: '123e4567-e89b-12d3-a456-426614174030', + repository, + }) + ).rejects.toMatchObject({ + code: 'SERVICE_UNAVAILABLE', + message: 'Bitbucket repository authorization failed (temporarily_unavailable)', + }); + }); + + it('reports token-service RPC failures as service unavailable', async () => { + const getBitbucketToken = vi.fn().mockRejectedValue(new Error('binding unavailable')); + + await expect( + assertBitbucketRepositoryAccessBeforeSessionCreation({ + env: { GIT_TOKEN_SERVICE: { getBitbucketToken } } as never, + userId: 'user-1', + orgId: '123e4567-e89b-12d3-a456-426614174030', + repository, + }) + ).rejects.toMatchObject({ + code: 'SERVICE_UNAVAILABLE', + message: 'Bitbucket repository authorization failed (temporarily_unavailable)', + }); + }); +}); diff --git a/services/cloud-agent-next/src/session/validate-repository-access.ts b/services/cloud-agent-next/src/session/validate-repository-access.ts new file mode 100644 index 0000000000..bf773792bd --- /dev/null +++ b/services/cloud-agent-next/src/session/validate-repository-access.ts @@ -0,0 +1,33 @@ +import { TRPCError } from '@trpc/server'; +import type { PersistenceEnv } from '../persistence/types.js'; +import { resolveManagedBitbucketToken } from '../services/git-token-service-client.js'; +import type { SessionRepositoryRequest } from './session-requests.js'; + +export async function assertBitbucketRepositoryAccessBeforeSessionCreation(input: { + env: PersistenceEnv; + userId: string; + orgId?: string; + repository: SessionRepositoryRequest; +}): Promise { + if (input.repository.type !== 'bitbucket') return; + if (!input.orgId) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Bitbucket repositories require an organization', + }); + } + + const result = await resolveManagedBitbucketToken(input.env, { + userId: input.userId, + orgId: input.orgId, + workspaceUuid: input.repository.workspaceUuid, + repositoryUuid: input.repository.repositoryUuid, + repositoryUrl: input.repository.url, + }); + if (!result.success) { + throw new TRPCError({ + code: result.reason === 'temporarily_unavailable' ? 'SERVICE_UNAVAILABLE' : 'BAD_REQUEST', + message: `Bitbucket repository authorization failed (${result.reason})`, + }); + } +} diff --git a/services/cloud-agent-next/src/shared/wrapper-bootstrap.ts b/services/cloud-agent-next/src/shared/wrapper-bootstrap.ts index 0ca7176a71..475644413b 100644 --- a/services/cloud-agent-next/src/shared/wrapper-bootstrap.ts +++ b/services/cloud-agent-next/src/shared/wrapper-bootstrap.ts @@ -23,7 +23,7 @@ export type WrapperBootstrapRepoSource = kind: 'git'; url: string; token?: string; - platform?: 'github' | 'gitlab'; + platform?: 'github' | 'gitlab' | 'bitbucket'; shallow?: boolean; refreshRemote?: boolean; }; @@ -140,6 +140,7 @@ export type WrapperWorkspaceReady = { githubAppType?: 'standard' | 'lite'; gitToken?: string; gitlabTokenManaged?: boolean; + bitbucketTokenManaged?: boolean; devcontainer?: WrapperDevContainerMetadata; }; diff --git a/services/cloud-agent-next/src/types.ts b/services/cloud-agent-next/src/types.ts index c0faf154a8..063133e0d2 100644 --- a/services/cloud-agent-next/src/types.ts +++ b/services/cloud-agent-next/src/types.ts @@ -79,10 +79,14 @@ export type SessionContext = { gitToken?: string; /** Whether the GitLab token was resolved server-side and its remote should be refreshed. */ gitlabTokenManaged?: boolean; + /** Whether the Bitbucket token was resolved server-side and its remote should be refreshed. */ + bitbucketTokenManaged?: boolean; + bitbucketWorkspaceUuid?: string; + bitbucketRepositoryUuid?: string; /** GitLab CLI bearer-mode instruction returned with a server-resolved credential. */ glabIsOAuth2?: boolean; /** Git platform type for correct token/env var handling */ - platform?: 'github' | 'gitlab'; + platform?: 'github' | 'gitlab' | 'bitbucket'; envVars?: Record; }; /** Result of interrupting a session's running processes */ @@ -165,6 +169,20 @@ type GetGitLabTokenResult = | 'no_project_token'; }; +export type BitbucketTokenFailureReason = + | 'invalid_request' + | 'not_connected' + | 'reconnect_required' + | 'temporarily_unavailable' + | 'insufficient_permissions' + | 'workspace_mismatch' + | 'repository_not_found' + | 'repository_mismatch'; + +type GetBitbucketTokenResult = + | { success: true; token: string } + | { success: false; reason: BitbucketTokenFailureReason }; + export type GitTokenService = { getTokenForRepo(params: { githubRepo: string; @@ -184,6 +202,13 @@ export type GitTokenService = { repositoryUrl?: string; createdOnPlatform?: string; }): Promise; + getBitbucketToken?(params: { + userId: string; + orgId: string; + workspaceUuid: string; + repositoryUuid: string; + repositoryUrl: string; + }): Promise; }; export type Env = { diff --git a/services/cloud-agent-next/src/workspace.test.ts b/services/cloud-agent-next/src/workspace.test.ts index d7d06dec0f..ab238a3444 100644 --- a/services/cloud-agent-next/src/workspace.test.ts +++ b/services/cloud-agent-next/src/workspace.test.ts @@ -576,6 +576,27 @@ describe('disk space checking', () => { ); }); + it('should use x-token-auth username for bitbucket platform', async () => { + mockExec + .mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' }) + .mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' }); + mockGitCheckout.mockResolvedValue({ success: true, exitCode: 0 }); + + await cloneGitRepo( + fakeSession, + '/workspace', + 'https://bitbucket.org/acme/repo.git', + 'test-token', + undefined, + { platform: 'bitbucket' } + ); + + expect(mockGitCheckout).toHaveBeenCalledWith( + expect.stringContaining('x-token-auth:test-token'), + expect.any(Object) + ); + }); + it('should use x-access-token username for github platform', async () => { mockExec .mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' }) // git config user.name @@ -751,6 +772,23 @@ describe('disk space checking', () => { ); }); + it('should use x-token-auth username for bitbucket platform', async () => { + mockExec.mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' }); + + await updateGitRemoteToken( + fakeSession, + '/workspace', + 'https://bitbucket.org/acme/repo.git', + 'new-token', + 'bitbucket' + ); + + expect(mockExec).toHaveBeenCalledWith( + expect.stringContaining('x-token-auth:new-token'), + expect.any(Object) + ); + }); + it('should use x-access-token username for github platform', async () => { mockExec.mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' }); diff --git a/services/cloud-agent-next/src/workspace.ts b/services/cloud-agent-next/src/workspace.ts index 4a8d34b8dd..24510a2223 100644 --- a/services/cloud-agent-next/src/workspace.ts +++ b/services/cloud-agent-next/src/workspace.ts @@ -691,6 +691,20 @@ export async function createSandboxUsageEvent( }; } +type ManagedGitPlatform = 'github' | 'gitlab' | 'bitbucket'; + +function gitCredentialUsername(platform: ManagedGitPlatform | undefined): string { + switch (platform) { + case 'gitlab': + return 'oauth2'; + case 'bitbucket': + return 'x-token-auth'; + case 'github': + case undefined: + return 'x-access-token'; + } +} + export async function cloneGitHubRepo( session: ExecutionSession, workspacePath: string, @@ -709,14 +723,14 @@ export async function cloneGitRepo( gitUrl: string, gitToken?: string, gitAuthor?: GitAuthorConfig, - options?: { shallow?: boolean; platform?: 'github' | 'gitlab' } + options?: { shallow?: boolean; platform?: ManagedGitPlatform } ): Promise { // Build URL with token if available (for private repos) // GitLab OAuth tokens require username 'oauth2'; all other providers use 'x-access-token' let repoUrl = gitUrl; if (gitToken) { const url = new URL(gitUrl); - url.username = options?.platform === 'gitlab' ? 'oauth2' : 'x-access-token'; + url.username = gitCredentialUsername(options?.platform); url.password = gitToken; repoUrl = url.toString(); } @@ -803,7 +817,7 @@ export type RestoreWorkspaceOptions = { gitToken?: string; gitAuthor?: GitAuthorConfig; lastSeenBranch?: string; - platform?: 'github' | 'gitlab'; + platform?: ManagedGitPlatform; }; export async function restoreWorkspace( @@ -869,11 +883,10 @@ export async function updateGitRemoteToken( workspacePath: string, gitUrl: string, gitToken: string, - platform?: 'github' | 'gitlab' + platform?: ManagedGitPlatform ): Promise { - // Build new URL with token embedded (GitLab uses 'oauth2', others use 'x-access-token') const newUrl = new URL(gitUrl); - newUrl.username = platform === 'gitlab' ? 'oauth2' : 'x-access-token'; + newUrl.username = gitCredentialUsername(platform); newUrl.password = gitToken; const sanitizedGitUrl = sanitizeGitUrlForLogging(gitUrl); diff --git a/services/cloud-agent-next/wrapper/src/session-bootstrap.test.ts b/services/cloud-agent-next/wrapper/src/session-bootstrap.test.ts index aca68cb9c5..837fc635f3 100644 --- a/services/cloud-agent-next/wrapper/src/session-bootstrap.test.ts +++ b/services/cloud-agent-next/wrapper/src/session-bootstrap.test.ts @@ -944,6 +944,42 @@ describe('prepareWrapperBootstrapWorkspace', () => { ); }); + it('refreshes a warm Bitbucket remote with x-token-auth', async () => { + const request = makeRequest(tmpDir, { + workspace: { + workspacePath: path.join(tmpDir, 'workspace'), + sessionHome: path.join(tmpDir, 'home'), + branchName: 'main', + preferSnapshot: true, + }, + repo: { + kind: 'git', + url: 'https://bitbucket.org/acme/repo.git', + token: 'bitbucket-token', + platform: 'bitbucket', + refreshRemote: true, + }, + }); + await createCompleteGitWorkspace(request.workspace.workspacePath); + const gitCalls: string[][] = []; + + await prepareWrapperBootstrapWorkspace(request, undefined, { + git: async args => { + gitCalls.push(args); + return { stdout: '', stderr: '', exitCode: 0 }; + }, + }); + + expect(gitCalls).toEqual([ + [ + 'remote', + 'set-url', + 'origin', + 'https://x-token-auth:bitbucket-token@bitbucket.org/acme/repo.git', + ], + ]); + }); + it('refreshes a warm GitHub remote, author, and selected CLI credential', async () => { const request = makeRequest(tmpDir, { workspace: { diff --git a/services/cloud-agent-next/wrapper/src/session-bootstrap.ts b/services/cloud-agent-next/wrapper/src/session-bootstrap.ts index e2eef81540..90edaa0c62 100644 --- a/services/cloud-agent-next/wrapper/src/session-bootstrap.ts +++ b/services/cloud-agent-next/wrapper/src/session-bootstrap.ts @@ -154,11 +154,12 @@ function longGitOptions( function authenticatedUrl( gitUrl: string, token: string | undefined, - platform: 'github' | 'gitlab' | undefined + platform: 'github' | 'gitlab' | 'bitbucket' | undefined ): string { if (!token) return gitUrl; const url = new URL(gitUrl); - url.username = platform === 'gitlab' ? 'oauth2' : 'x-access-token'; + url.username = + platform === 'gitlab' ? 'oauth2' : platform === 'bitbucket' ? 'x-token-auth' : 'x-access-token'; url.password = token; return url.toString(); } diff --git a/services/git-token-service/.dev.vars.example b/services/git-token-service/.dev.vars.example index 3da8489b64..4080d69798 100644 --- a/services/git-token-service/.dev.vars.example +++ b/services/git-token-service/.dev.vars.example @@ -34,4 +34,18 @@ GITHUB_LITE_APP_PRIVATE_KEY= # GitLab App credentials (optional, used as fallback when metadata lacks client credentials) GITLAB_CLIENT_ID= -GITLAB_CLIENT_SECRET= \ No newline at end of file +GITLAB_CLIENT_SECRET= + +# Bitbucket Cloud OAuth consumer credentials shared with Web +BITBUCKET_CLIENT_ID= +BITBUCKET_CLIENT_SECRET= +# Shared RSA/AES envelope keypair for persisted Bitbucket OAuth and Workspace Access Token credentials. +# Generate outside the repository and never commit key material: +# pnpm exec tsx dev/generate-rsa-env-keypair.ts -- --out-dir \ +# --public-env BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY \ +# --private-env BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY +# Use the same key ID and public key in Web; keep the private key in this Worker only. +# Provision both key values as per-Worker secrets because they exceed Secrets Store's size limit. +BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID= +BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY= +BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY= \ No newline at end of file diff --git a/services/git-token-service/src/bitbucket-api.test.ts b/services/git-token-service/src/bitbucket-api.test.ts new file mode 100644 index 0000000000..24ec06e2b8 --- /dev/null +++ b/services/git-token-service/src/bitbucket-api.test.ts @@ -0,0 +1,710 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + findBitbucketWorkspaceRepositoryByUuid, + listBitbucketWorkspaceRepositories, +} from './bitbucket-api.js'; + +const REPOSITORY_PAGE_LENGTH = 50; +const MAX_REPOSITORY_PAGES = 20; +const MAX_REPOSITORY_ITEMS = 500; +const MAX_RESPONSE_BYTES = 1_000_000; +const accessToken = 'bitbucket-access-token-fixture'; +const workspaceUuid = 'a07d5c40-2d2d-4e79-a812-6a47824a77d6'; +const repositoryUuid = '38a47a32-cb87-4a9f-b75d-7224774bba77'; +const anotherRepositoryUuid = '671c0279-67a5-4d24-8b21-4d6acdfa04d3'; + +function jsonResponse(value: unknown, init?: ResponseInit): Response { + return new Response(JSON.stringify(value), { + ...init, + headers: { 'Content-Type': 'application/json', ...init?.headers }, + }); +} + +function repositoryPayload(overrides: Record = {}): Record { + return { + uuid: `{${repositoryUuid}}`, + name: 'Widgets', + slug: 'widgets', + full_name: 'acme/widgets', + is_private: true, + workspace: { uuid: `{${workspaceUuid}}`, slug: 'acme' }, + mainbranch: { name: 'main' }, + ...overrides, + }; +} + +function numberedUuid(index: number): string { + return `00000000-0000-0000-0000-${index.toString(16).padStart(12, '0')}`; +} + +describe('listBitbucketWorkspaceRepositories', () => { + it('lists normalized repositories from the selected workspace endpoint', async () => { + const fetchMock = vi.fn().mockResolvedValue( + jsonResponse({ + pagelen: 50, + values: [ + { + uuid: `{${repositoryUuid.toUpperCase()}}`, + name: 'Widgets', + slug: 'widgets', + full_name: 'acme/widgets', + is_private: true, + workspace: { + uuid: `{${workspaceUuid.toUpperCase()}}`, + slug: 'acme', + }, + mainbranch: { name: 'main' }, + }, + ], + }) + ); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: `{${workspaceUuid.toUpperCase()}}` }, + fetch: fetchMock, + }) + ).resolves.toEqual([ + { + id: repositoryUuid, + workspaceUuid, + name: 'Widgets', + fullName: 'acme/widgets', + private: true, + defaultBranch: 'main', + }, + ]); + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.bitbucket.org/2.0/repositories/acme?pagelen=50', + { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + redirect: 'manual', + signal: expect.any(AbortSignal), + } + ); + }); + + it.each([' token', 'token ', 'to ken', 'to\nken', 'töken'])( + 'rejects non-canonical access token %j', + async token => { + const fetchMock = vi.fn(); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken: token, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'invalid_request' }); + expect(fetchMock).not.toHaveBeenCalled(); + } + ); + + it.each([ + { slug: 'acme/other', uuid: workspaceUuid }, + { slug: '.', uuid: workspaceUuid }, + { slug: 'acme', uuid: 'not-a-uuid' }, + ])('rejects invalid selected workspace %#', async workspace => { + const fetchMock = vi.fn(); + + await expect( + listBitbucketWorkspaceRepositories({ accessToken, workspace, fetch: fetchMock }) + ).rejects.toMatchObject({ code: 'invalid_request' }); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('follows a validated opaque next link', async () => { + const next = + 'https://api.bitbucket.org/2.0/repositories/acme?cursor=opaque%2F%5C%2Evalue&pagelen=50'; + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + jsonResponse({ + pagelen: 50, + values: [ + { + uuid: `{${repositoryUuid}}`, + name: 'Widgets', + slug: 'widgets', + full_name: 'acme/widgets', + is_private: true, + workspace: { uuid: `{${workspaceUuid}}`, slug: 'acme' }, + mainbranch: { name: 'main' }, + }, + ], + next, + }) + ) + .mockResolvedValueOnce( + jsonResponse({ + pagelen: 50, + values: [ + { + uuid: anotherRepositoryUuid, + name: 'Tools', + slug: 'tools', + full_name: 'acme/tools', + is_private: false, + workspace: { uuid: workspaceUuid, slug: 'acme' }, + mainbranch: null, + }, + ], + }) + ); + + const repositories = await listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }); + + expect(repositories.map(repository => repository.id)).toEqual([ + repositoryUuid, + anotherRepositoryUuid, + ]); + expect(repositories[1]).not.toHaveProperty('defaultBranch'); + expect(fetchMock).toHaveBeenNthCalledWith(2, next, expect.any(Object)); + }); + + it.each([ + [401, 'authentication_rejected'], + [403, 'insufficient_permissions'], + [404, 'not_found'], + [429, 'rate_limited'], + [500, 'provider_unavailable'], + [503, 'provider_unavailable'], + ] as const)('classifies provider status %s as %s', async (status, code) => { + const fetchMock = vi + .fn() + .mockResolvedValue(jsonResponse({ error: { message: 'rejected' } }, { status })); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code }); + }); + + it('classifies transport failures without exposing provider details', async () => { + const fetchMock = vi.fn().mockRejectedValue(new Error(`Bearer ${accessToken}`)); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'transport_failed' }); + }); + + it('times out a provider request', async () => { + const fetchMock = vi.fn((_url: string | URL | Request, init?: RequestInit) => { + return new Promise((_resolve, reject) => { + init?.signal?.addEventListener('abort', () => reject(init.signal?.reason), { once: true }); + }); + }); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + requestTimeoutMs: 5, + }) + ).rejects.toMatchObject({ code: 'request_timed_out' }); + }); + + it('times out while reading a provider response body', async () => { + const fetchMock = vi.fn((_url: string | URL | Request, init?: RequestInit) => { + return Promise.resolve( + new Response( + new ReadableStream({ + start(controller) { + init?.signal?.addEventListener('abort', () => controller.error(init.signal?.reason), { + once: true, + }); + }, + }), + { headers: { 'Content-Type': 'application/json' } } + ) + ); + }); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + requestTimeoutMs: 5, + }) + ).rejects.toMatchObject({ code: 'request_timed_out' }); + }); + + it('rejects unexpected successful provider statuses', async () => { + const fetchMock = vi + .fn() + .mockResolvedValue(jsonResponse({ pagelen: 50, values: [] }, { status: 201 })); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'request_failed' }); + }); + + it('requires a JSON provider response', async () => { + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ pagelen: 50, values: [] }), { + headers: { 'Content-Type': 'text/plain' }, + }) + ); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'invalid_response' }); + }); + + it('rejects a response whose declared size exceeds the JSON response bound', async () => { + const fetchMock = vi + .fn() + .mockResolvedValue( + jsonResponse( + { pagelen: 50, values: [] }, + { headers: { 'Content-Length': String(MAX_RESPONSE_BYTES + 1) } } + ) + ); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'response_too_large' }); + }); + + it('stops reading when the streamed JSON body exceeds the response bound', async () => { + const fetchMock = vi.fn().mockResolvedValue( + new Response('x'.repeat(MAX_RESPONSE_BYTES + 1), { + headers: { + 'Content-Length': '10', + 'Content-Type': 'application/json', + }, + }) + ); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'response_too_large' }); + }); + + it.each([ + {}, + { pagelen: '50', values: [] }, + { pagelen: REPOSITORY_PAGE_LENGTH + 1, values: [] }, + { pagelen: 50, values: 'not-an-array' }, + { + pagelen: 1, + values: [ + repositoryPayload(), + repositoryPayload({ + uuid: `{${anotherRepositoryUuid}}`, + name: 'Tools', + slug: 'tools', + full_name: 'acme/tools', + }), + ], + }, + { pagelen: 50, values: [repositoryPayload({ uuid: 'not-a-uuid' })] }, + { pagelen: 50, values: [repositoryPayload({ is_private: 'true' })] }, + { pagelen: 50, values: [repositoryPayload({ mainbranch: { name: '' } })] }, + ])('rejects a malformed repository page %#', async payload => { + const fetchMock = vi.fn().mockResolvedValue(jsonResponse(payload)); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'invalid_response' }); + }); + + it.each([ + repositoryPayload({ + workspace: { uuid: '{00000000-0000-0000-0000-000000000001}', slug: 'acme' }, + }), + repositoryPayload({ workspace: { uuid: `{${workspaceUuid}}`, slug: 'other' } }), + repositoryPayload({ full_name: 'other/widgets' }), + ])('rejects a repository outside the selected workspace contract %#', async repository => { + const fetchMock = vi + .fn() + .mockResolvedValue(jsonResponse({ pagelen: 50, values: [repository] })); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'workspace_mismatch' }); + }); + + it('classifies malformed repository identity separately from workspace mismatch', async () => { + const fetchMock = vi.fn().mockResolvedValue( + jsonResponse({ + pagelen: 50, + values: [repositoryPayload({ slug: '../widgets', full_name: 'acme/../widgets' })], + }) + ); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'invalid_response' }); + }); + + it('rejects duplicate repository paths with inconsistent UUIDs', async () => { + const fetchMock = vi.fn().mockResolvedValue( + jsonResponse({ + pagelen: 50, + values: [repositoryPayload(), repositoryPayload({ uuid: `{${anotherRepositoryUuid}}` })], + }) + ); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'invalid_response' }); + }); + + it('discards earlier pages when a later page is invalid', async () => { + const next = 'https://api.bitbucket.org/2.0/repositories/acme?page=2&pagelen=50'; + const fetchMock = vi + .fn() + .mockResolvedValueOnce(jsonResponse({ pagelen: 50, values: [repositoryPayload()], next })) + .mockResolvedValueOnce(jsonResponse({ pagelen: 50, values: 'invalid' })); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'invalid_response' }); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + it('enforces the named page traversal cap', async () => { + let requestCount = 0; + const fetchMock = vi.fn(async () => { + requestCount += 1; + return jsonResponse({ + pagelen: 50, + values: [], + next: `https://api.bitbucket.org/2.0/repositories/acme?page=${requestCount + 1}&pagelen=50`, + }); + }); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'page_limit_exceeded' }); + expect(fetchMock).toHaveBeenCalledTimes(MAX_REPOSITORY_PAGES); + }); + + it('enforces the named repository item cap', async () => { + let requestCount = 0; + const fetchMock = vi.fn(async () => { + const firstItem = requestCount * REPOSITORY_PAGE_LENGTH + 1; + requestCount += 1; + return jsonResponse({ + pagelen: REPOSITORY_PAGE_LENGTH, + values: Array.from({ length: REPOSITORY_PAGE_LENGTH }, (_, offset) => { + const index = firstItem + offset; + return repositoryPayload({ + uuid: `{${numberedUuid(index)}}`, + name: `Repository ${index}`, + slug: `repository-${index}`, + full_name: `acme/repository-${index}`, + mainbranch: null, + }); + }), + next: `https://api.bitbucket.org/2.0/repositories/acme?page=${requestCount + 1}&pagelen=50`, + }); + }); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'item_limit_exceeded' }); + expect(fetchMock).toHaveBeenCalledTimes(MAX_REPOSITORY_ITEMS / REPOSITORY_PAGE_LENGTH); + }); + + it('rejects redirects without following their location', async () => { + const fetchMock = vi.fn().mockResolvedValue( + new Response(null, { + status: 302, + headers: { Location: 'https://evil.example/repositories' }, + }) + ); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'redirect_rejected' }); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it.each([ + { redirected: true, url: 'https://evil.example/repositories' }, + { + redirected: false, + url: 'https://api.bitbucket.org/2.0/repositories/other?pagelen=50', + }, + ])('rejects a successful response reached through another URL %#', async responseMetadata => { + const response = jsonResponse({ pagelen: 50, values: [] }); + Object.defineProperties(response, { + redirected: { value: responseMetadata.redirected }, + url: { value: responseMetadata.url }, + }); + const fetchMock = vi.fn().mockResolvedValue(response); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'redirect_rejected' }); + }); + + it.each([ + ['fixed-host', 'http://api.bitbucket.org/2.0/repositories/acme?pagelen=50'], + ['fixed-host', 'https://evil.example/2.0/repositories/acme?pagelen=50'], + ['fixed-host', 'https://api.bitbucket.org:443/2.0/repositories/acme?pagelen=50'], + ['fixed-host', 'https://api.bitbucket.org:8443/2.0/repositories/acme?pagelen=50'], + ['fixed-host', 'HTTPS://api.bitbucket.org/2.0/repositories/acme?pagelen=50'], + ['fixed-host', 'https://API.bitbucket.org/2.0/repositories/acme?pagelen=50'], + ['credentials', 'https://user@api.bitbucket.org/2.0/repositories/acme?pagelen=50'], + ['credentials', 'https://user:password@api.bitbucket.org/2.0/repositories/acme?pagelen=50'], + ['path', 'https://api.bitbucket.org/2.0/repositories/other?pagelen=50'], + ['path', 'https://api.bitbucket.org/2.0/repositories/acme/widgets?pagelen=50'], + ['path', 'https://api.bitbucket.org/2.0/repositories/acme%2Fother?pagelen=50'], + ['path', 'https://api.bitbucket.org/2.0/repositories/other/../acme?pagelen=50'], + ['path', 'https://api.bitbucket.org/2.0/repositories/other/%2e%2e/acme?pagelen=50'], + ['path', 'https://api.bitbucket.org/2.0/repositories/acme?pagelen=50#fragment'], + ['path', 'https://api.bitbucket.org/2.0/repositories/acme?pagelen=50#'], + ['query', 'https://api.bitbucket.org/2.0/repositories/acme?cursor=%ZZ'], + ['query', 'https://api.bitbucket.org/2.0/repositories/acme?cursor=opaque\tvalue&pagelen=50'], + [ + 'query', + 'https://api.bitbucket.org/2.0/repositories/acme?cursor=opaque\u00a0value&pagelen=50', + ], + ['page length', 'https://api.bitbucket.org/2.0/repositories/acme?pagelen=%35%30'], + ['page length', 'https://api.bitbucket.org/2.0/repositories/acme?pagelen=51'], + ['page length', 'https://api.bitbucket.org/2.0/repositories/acme?pagelen=0'], + ['page length', 'https://api.bitbucket.org/2.0/repositories/acme?pagelen=50&pagelen=10'], + ['case-variant query', 'https://api.bitbucket.org/2.0/repositories/acme?Pagelen=50'], + ['case-variant query', 'https://api.bitbucket.org/2.0/repositories/acme?PAGELEN=50'], + ['cyclic', 'https://api.bitbucket.org/2.0/repositories/acme?pagelen=50'], + ])('rejects a hostile %s next link %s before another fetch', async (_category, next) => { + const fetchMock = vi.fn().mockResolvedValue( + jsonResponse({ + pagelen: 50, + values: [], + next, + }) + ); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'invalid_pagination', message: 'invalid_pagination' }); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it('rejects a role-filtered next link before another fetch', async () => { + const next = 'https://api.bitbucket.org/2.0/repositories/acme?role=contributor&pagelen=50'; + const fetchMock = vi.fn().mockResolvedValue( + jsonResponse({ + pagelen: 50, + values: [], + next, + }) + ); + + await expect( + listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'invalid_pagination', message: 'invalid_pagination' }); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it('sanitizes transport, provider, and payload failures', async () => { + const sensitiveFailure = `Authorization: Bearer ${accessToken}`; + const fetchImplementations = [ + vi.fn().mockRejectedValue(new Error(sensitiveFailure)), + vi.fn().mockResolvedValue(new Response(sensitiveFailure, { status: 502 })), + vi + .fn() + .mockResolvedValue( + new Response(sensitiveFailure, { headers: { 'Content-Type': 'application/json' } }) + ), + vi.fn().mockResolvedValue( + jsonResponse({ + pagelen: 50, + values: [], + next: `https://${accessToken}@api.bitbucket.org/2.0/repositories/acme?pagelen=50`, + }) + ), + vi.fn().mockResolvedValue( + new Response( + new ReadableStream({ + start(controller) { + controller.error(new Error(sensitiveFailure)); + }, + }) + ) + ), + ]; + + for (const fetchImplementation of fetchImplementations) { + let thrown: unknown; + try { + await listBitbucketWorkspaceRepositories({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + fetch: fetchImplementation, + }); + } catch (error) { + thrown = error; + } + + expect(thrown).toBeInstanceOf(Error); + const errorText = + thrown instanceof Error + ? `${thrown.name} ${thrown.message} ${thrown.stack ?? ''}` + : String(thrown); + expect(errorText).not.toContain(accessToken); + expect(errorText).not.toContain('Authorization'); + expect(JSON.stringify(thrown)).not.toContain(accessToken); + } + }); +}); + +describe('findBitbucketWorkspaceRepositoryByUuid', () => { + it('finds a validated workspace repository by normalized UUID', async () => { + const fetchMock = vi.fn().mockResolvedValue( + jsonResponse({ + pagelen: 50, + values: [ + { + uuid: `{${repositoryUuid}}`, + name: 'Widgets', + slug: 'widgets', + full_name: 'acme/widgets', + is_private: true, + workspace: { uuid: `{${workspaceUuid}}`, slug: 'acme' }, + mainbranch: { name: 'main' }, + }, + ], + }) + ); + + await expect( + findBitbucketWorkspaceRepositoryByUuid({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + repositoryUuid: `{${repositoryUuid}}`, + fetch: fetchMock, + }) + ).resolves.toMatchObject({ id: repositoryUuid, fullName: 'acme/widgets' }); + }); + + it('returns null when the contributor listing does not contain the UUID', async () => { + const fetchMock = vi + .fn() + .mockResolvedValue(jsonResponse({ pagelen: 50, values: [repositoryPayload()] })); + + await expect( + findBitbucketWorkspaceRepositoryByUuid({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + repositoryUuid: anotherRepositoryUuid, + fetch: fetchMock, + }) + ).resolves.toBeNull(); + }); + + it('rejects a malformed target UUID before making a request', async () => { + const fetchMock = vi.fn(); + + await expect( + findBitbucketWorkspaceRepositoryByUuid({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + repositoryUuid: 'not-a-uuid', + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'invalid_request' }); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('validates all pagination before returning a matching repository', async () => { + const fetchMock = vi.fn().mockResolvedValue( + jsonResponse({ + pagelen: 50, + values: [repositoryPayload()], + next: 'https://evil.example/2.0/repositories/acme?pagelen=50', + }) + ); + + await expect( + findBitbucketWorkspaceRepositoryByUuid({ + accessToken, + workspace: { slug: 'acme', uuid: workspaceUuid }, + repositoryUuid, + fetch: fetchMock, + }) + ).rejects.toMatchObject({ code: 'invalid_pagination' }); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/git-token-service/src/bitbucket-api.ts b/services/git-token-service/src/bitbucket-api.ts new file mode 100644 index 0000000000..5637b9cde1 --- /dev/null +++ b/services/git-token-service/src/bitbucket-api.ts @@ -0,0 +1,343 @@ +import { z } from 'zod'; +import { normalizeBitbucketUuid } from './bitbucket-url.js'; + +const BITBUCKET_REPOSITORY_PAGE_LENGTH = 50; +const BITBUCKET_MAX_REPOSITORY_PAGES = 20; +const BITBUCKET_MAX_REPOSITORY_ITEMS = 500; +const BITBUCKET_MAX_RESPONSE_BYTES = 1_000_000; +const BITBUCKET_REQUEST_TIMEOUT_MS = 10_000; +const BITBUCKET_MAX_REQUEST_TIMEOUT_MS = 30_000; + +const BitbucketRepositoryPayloadSchema = z.object({ + uuid: z.string(), + name: z.string().min(1), + slug: z.string().min(1), + full_name: z.string().min(3), + is_private: z.boolean(), + workspace: z.object({ + uuid: z.string(), + slug: z.string().min(1), + }), + mainbranch: z + .object({ name: z.string().min(1) }) + .nullable() + .optional(), +}); + +const BitbucketRepositoryPageSchema = z + .object({ + pagelen: z.number().int().positive().max(BITBUCKET_REPOSITORY_PAGE_LENGTH), + values: z.array(BitbucketRepositoryPayloadSchema).max(BITBUCKET_REPOSITORY_PAGE_LENGTH), + next: z.string().min(1).optional(), + }) + .refine(page => page.values.length <= page.pagelen); + +type BitbucketRepositoryPayload = z.infer; + +export type BitbucketRepository = { + id: string; + workspaceUuid: string; + name: string; + fullName: string; + private: boolean; + defaultBranch?: string; +}; + +export type BitbucketRepositoryApiOptions = { + accessToken: string; + workspace: { + slug: string; + uuid: string; + }; + fetch?: typeof fetch; + requestTimeoutMs?: number; +}; + +export type BitbucketApiErrorCode = + | 'invalid_request' + | 'request_failed' + | 'request_timed_out' + | 'transport_failed' + | 'authentication_rejected' + | 'insufficient_permissions' + | 'not_found' + | 'rate_limited' + | 'provider_unavailable' + | 'redirect_rejected' + | 'invalid_response' + | 'workspace_mismatch' + | 'invalid_pagination' + | 'page_limit_exceeded' + | 'item_limit_exceeded' + | 'response_too_large'; + +export class BitbucketApiError extends Error { + constructor(readonly code: BitbucketApiErrorCode) { + super(code); + this.name = 'BitbucketApiError'; + } +} + +function isValidBitbucketPathSegment(value: string): boolean { + return value.length <= 255 && /^[A-Za-z0-9_.-]+$/.test(value) && value !== '.' && value !== '..'; +} + +function repositoryEndpoint(workspaceSlug: string): string { + return `https://api.bitbucket.org/2.0/repositories/${encodeURIComponent(workspaceSlug)}?pagelen=${BITBUCKET_REPOSITORY_PAGE_LENGTH}`; +} + +function hasNonVisibleAscii(value: string): boolean { + for (let index = 0; index < value.length; index += 1) { + const code = value.charCodeAt(index); + if (code < 0x21 || code > 0x7e) return true; + } + return false; +} + +function validateNextLink(value: string, workspaceSlug: string): string { + const expectedPath = `/2.0/repositories/${encodeURIComponent(workspaceSlug)}`; + const rawUrl = /^https:\/\/api\.bitbucket\.org([^?#]*)(\?[^#]*)?$/.exec(value); + if (!rawUrl || rawUrl[1] !== expectedPath || hasNonVisibleAscii(value)) { + throw new BitbucketApiError('invalid_pagination'); + } + + let parsed: URL; + try { + parsed = new URL(value); + decodeURIComponent(`${parsed.pathname}${parsed.search}`); + } catch { + throw new BitbucketApiError('invalid_pagination'); + } + + const rawQueryParameters = (rawUrl[2]?.slice(1) ?? '').split('&'); + const queryParameterNames = [...parsed.searchParams.keys()]; + const hasRole = queryParameterNames.some(name => name.toLowerCase() === 'role'); + const hasCaseVariantPageLength = queryParameterNames.some( + name => name !== 'pagelen' && name.toLowerCase() === 'pagelen' + ); + const pageLengths = parsed.searchParams.getAll('pagelen'); + if ( + parsed.protocol !== 'https:' || + parsed.origin !== 'https://api.bitbucket.org' || + parsed.username !== '' || + parsed.password !== '' || + parsed.port !== '' || + parsed.pathname !== expectedPath || + parsed.href !== value || + hasRole || + hasCaseVariantPageLength || + pageLengths.length > 1 || + (pageLengths.length === 1 && + (!/^[1-9][0-9]*$/.test(pageLengths[0]) || + Number(pageLengths[0]) > BITBUCKET_REPOSITORY_PAGE_LENGTH || + rawQueryParameters.filter(parameter => parameter === `pagelen=${pageLengths[0]}`).length !== + 1)) + ) { + throw new BitbucketApiError('invalid_pagination'); + } + + return value; +} + +function normalizeRepository( + repository: BitbucketRepositoryPayload, + workspace: { slug: string; uuid: string } +): BitbucketRepository { + const id = normalizeBitbucketUuid(repository.uuid); + const repositoryWorkspaceUuid = normalizeBitbucketUuid(repository.workspace.uuid); + if (!id || !repositoryWorkspaceUuid || !isValidBitbucketPathSegment(repository.slug)) { + throw new BitbucketApiError('invalid_response'); + } + if ( + repositoryWorkspaceUuid !== workspace.uuid || + repository.workspace.slug !== workspace.slug || + repository.full_name !== `${workspace.slug}/${repository.slug}` + ) { + throw new BitbucketApiError('workspace_mismatch'); + } + + return { + id, + workspaceUuid: workspace.uuid, + name: repository.name, + fullName: repository.full_name, + private: repository.is_private, + ...(repository.mainbranch ? { defaultBranch: repository.mainbranch.name } : {}), + }; +} + +async function readBoundedJson(response: Response, signal: AbortSignal): Promise { + if (!response.body) throw new BitbucketApiError('invalid_response'); + + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let totalBytes = 0; + try { + while (true) { + const chunk = await reader.read(); + if (chunk.done) break; + const chunkValue: unknown = chunk.value; + if (!(chunkValue instanceof Uint8Array)) { + throw new BitbucketApiError('invalid_response'); + } + totalBytes += chunkValue.byteLength; + if (totalBytes > BITBUCKET_MAX_RESPONSE_BYTES) { + try { + await reader.cancel(); + } catch { + // The bounded read still fails closed if cancellation itself fails. + } + throw new BitbucketApiError('response_too_large'); + } + chunks.push(chunkValue); + } + } catch (error) { + if (error instanceof BitbucketApiError) throw error; + throw new BitbucketApiError(signal.aborted ? 'request_timed_out' : 'invalid_response'); + } finally { + reader.releaseLock(); + } + + const body = new Uint8Array(totalBytes); + let offset = 0; + for (const chunk of chunks) { + body.set(chunk, offset); + offset += chunk.byteLength; + } + + try { + return JSON.parse(new TextDecoder('utf-8', { fatal: true, ignoreBOM: false }).decode(body)); + } catch { + throw new BitbucketApiError('invalid_response'); + } +} + +async function fetchRepositoryPage( + endpoint: string, + accessToken: string, + fetchImplementation: typeof fetch, + requestTimeoutMs: number +): Promise> { + const signal = AbortSignal.timeout(requestTimeoutMs); + let response: Response; + try { + response = await fetchImplementation(endpoint, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + redirect: 'manual', + signal, + }); + } catch { + throw new BitbucketApiError(signal.aborted ? 'request_timed_out' : 'transport_failed'); + } + + if ( + (response.status >= 300 && response.status < 400) || + response.redirected || + (response.url !== '' && response.url !== endpoint) + ) { + throw new BitbucketApiError('redirect_rejected'); + } + if (response.status === 401) throw new BitbucketApiError('authentication_rejected'); + if (response.status === 403) throw new BitbucketApiError('insufficient_permissions'); + if (response.status === 404) throw new BitbucketApiError('not_found'); + if (response.status === 429) throw new BitbucketApiError('rate_limited'); + if (response.status >= 500 && response.status <= 599) { + throw new BitbucketApiError('provider_unavailable'); + } + if (response.status !== 200) throw new BitbucketApiError('request_failed'); + + const contentType = response.headers.get('Content-Type')?.split(';', 1)[0].trim().toLowerCase(); + if (contentType !== 'application/json') throw new BitbucketApiError('invalid_response'); + + const contentLength = response.headers.get('Content-Length'); + if (contentLength) { + if (!/^[0-9]+$/.test(contentLength)) throw new BitbucketApiError('invalid_response'); + if (Number(contentLength) > BITBUCKET_MAX_RESPONSE_BYTES) { + throw new BitbucketApiError('response_too_large'); + } + } + + const payload = await readBoundedJson(response, signal); + const page = BitbucketRepositoryPageSchema.safeParse(payload); + if (!page.success) throw new BitbucketApiError('invalid_response'); + return page.data; +} + +export async function listBitbucketWorkspaceRepositories( + options: BitbucketRepositoryApiOptions +): Promise { + const workspaceUuid = normalizeBitbucketUuid(options.workspace.uuid); + const requestTimeoutMs = options.requestTimeoutMs ?? BITBUCKET_REQUEST_TIMEOUT_MS; + if ( + !workspaceUuid || + !isValidBitbucketPathSegment(options.workspace.slug) || + options.accessToken === '' || + hasNonVisibleAscii(options.accessToken) || + !Number.isInteger(requestTimeoutMs) || + requestTimeoutMs <= 0 || + requestTimeoutMs > BITBUCKET_MAX_REQUEST_TIMEOUT_MS + ) { + throw new BitbucketApiError('invalid_request'); + } + + const workspace = { slug: options.workspace.slug, uuid: workspaceUuid }; + const fetchImplementation = options.fetch ?? globalThis.fetch; + const repositories: BitbucketRepository[] = []; + const repositoryIds = new Set(); + const repositoryFullNames = new Set(); + const visited = new Set(); + let endpoint: string | undefined = repositoryEndpoint(workspace.slug); + + for (let pageNumber = 0; endpoint; pageNumber += 1) { + if (pageNumber >= BITBUCKET_MAX_REPOSITORY_PAGES) { + throw new BitbucketApiError('page_limit_exceeded'); + } + if (visited.has(endpoint)) throw new BitbucketApiError('invalid_pagination'); + visited.add(endpoint); + + const page = await fetchRepositoryPage( + endpoint, + options.accessToken, + fetchImplementation, + requestTimeoutMs + ); + for (const payload of page.values) { + const repository = normalizeRepository(payload, workspace); + if (repositoryIds.has(repository.id) || repositoryFullNames.has(repository.fullName)) { + throw new BitbucketApiError('invalid_response'); + } + repositoryIds.add(repository.id); + repositoryFullNames.add(repository.fullName); + repositories.push(repository); + if (repositories.length > BITBUCKET_MAX_REPOSITORY_ITEMS) { + throw new BitbucketApiError('item_limit_exceeded'); + } + } + + if (!page.next) return repositories; + if (repositories.length >= BITBUCKET_MAX_REPOSITORY_ITEMS) { + throw new BitbucketApiError('item_limit_exceeded'); + } + endpoint = validateNextLink(page.next, workspace.slug); + } + + return repositories; +} + +export async function findBitbucketWorkspaceRepositoryByUuid( + options: BitbucketRepositoryApiOptions & { repositoryUuid: string } +): Promise { + const repositoryUuid = normalizeBitbucketUuid(options.repositoryUuid); + if (!repositoryUuid) throw new BitbucketApiError('invalid_request'); + + const repositories = await listBitbucketWorkspaceRepositories({ + accessToken: options.accessToken, + workspace: options.workspace, + fetch: options.fetch, + requestTimeoutMs: options.requestTimeoutMs, + }); + return repositories.find(repository => repository.id === repositoryUuid) ?? null; +} diff --git a/services/git-token-service/src/bitbucket-authorization-service.test.ts b/services/git-token-service/src/bitbucket-authorization-service.test.ts new file mode 100644 index 0000000000..bcae4b605d --- /dev/null +++ b/services/git-token-service/src/bitbucket-authorization-service.test.ts @@ -0,0 +1,372 @@ +import { generateKeyPairSync } from 'node:crypto'; +import type * as DbClientModule from '@kilocode/db/client'; +import { encryptKeyedEnvelope } from '@kilocode/encryption'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const database = vi.hoisted(() => ({ + row: undefined as Record | undefined, + rows: undefined as Array | undefined> | undefined, + updates: [] as Array>, + returnedCredential: undefined as Record | undefined, + locks: 0, +})); + +vi.mock('@kilocode/db/client', async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + getWorkerDb: (connectionString: string) => { + if (connectionString === 'postgres://query-builder') { + return actual.getWorkerDb(connectionString); + } + const transactionDb = { + execute: async () => { + database.locks += 1; + }, + select: () => ({ + from: () => ({ + where: () => ({}), + leftJoin: () => ({ + innerJoin: () => ({ + where: () => ({ + limit: async () => { + const row = database.rows ? database.rows.shift() : database.row; + return row ? [row] : []; + }, + }), + }), + }), + }), + }), + update: () => ({ + set: (values: Record) => { + database.updates.push(values); + const result = { + returning: async () => { + if (!database.returnedCredential) return []; + const row = database.row as { credential?: Record } | undefined; + if (row) row.credential = database.returnedCredential; + return [database.returnedCredential]; + }, + then: (resolve: (value: unknown[]) => unknown) => Promise.resolve([]).then(resolve), + }; + return { where: () => result }; + }, + }), + }; + return { + ...transactionDb, + transaction: async (operation: (tx: typeof transactionDb) => Promise) => + operation(transactionDb), + }; + }, + }; +}); + +import { getWorkerDb } from '@kilocode/db/client'; +import { + BITBUCKET_CLOUD_AGENT_MINIMUM_VALIDITY_MS, + BitbucketAuthorizationService, + buildBitbucketAuthorizationQuery, +} from './bitbucket-authorization-service.js'; + +const scheme = 'bitbucket-oauth-credential-rsa-aes-256-gcm'; +const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); +const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString(); +const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(); + +type TestOwner = { type: 'user' | 'org'; id: string }; + +function aad(kind: 'access' | 'refresh', owner: TestOwner = { type: 'user', id: 'user-1' }) { + return JSON.stringify({ + scheme, + version: 1, + platform: 'bitbucket', + credentialId: 'credential-1', + integrationId: 'integration-1', + owner, + authorizedByUserId: 'user-1', + kind, + }); +} + +function credential( + expiresInMs = 60 * 60 * 1000, + owner: TestOwner = { type: 'user', id: 'user-1' } +) { + const now = new Date().toISOString(); + return { + id: 'credential-1', + platform_integration_id: 'integration-1', + platform: 'bitbucket', + authorized_by_user_id: 'user-1', + provider_subject_id: '123e4567-e89b-12d3-a456-426614174010', + provider_subject_login: 'bucket-user', + access_token_encrypted: encryptKeyedEnvelope( + 'access-token', + scheme, + { keyId: 'active', publicKeyPem }, + aad('access', owner) + ), + access_token_expires_at: new Date(Date.now() + expiresInMs).toISOString(), + refresh_token_encrypted: encryptKeyedEnvelope( + 'refresh-token', + scheme, + { keyId: 'active', publicKeyPem }, + aad('refresh', owner) + ), + refresh_token_expires_at: null, + credential_version: 1, + revoked_at: null, + revocation_reason: null, + last_used_at: null, + created_at: now, + updated_at: now, + }; +} + +function activeRow(expiresInMs?: number, owner: TestOwner = { type: 'user', id: 'user-1' }) { + return { + credential: credential(expiresInMs, owner), + integrationId: 'integration-1', + integrationStatus: 'active', + installationId: '123e4567-e89b-12d3-a456-426614174020', + accountId: '123e4567-e89b-12d3-a456-426614174020', + accountLogin: 'acme', + scopes: ['account', 'email', 'pullrequest', 'repository', 'repository:write', 'webhook'], + metadata: { + state: 'active', + workspace: { + uuid: '123e4567-e89b-12d3-a456-426614174020', + slug: 'acme', + name: 'Acme', + }, + }, + }; +} + +function service() { + return new BitbucketAuthorizationService({ + HYPERDRIVE: { connectionString: 'postgres://test' }, + BITBUCKET_CLIENT_ID: 'client-id', + BITBUCKET_CLIENT_SECRET: 'client-secret', + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID: 'active', + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY: Buffer.from(publicKeyPem).toString('base64'), + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY: Buffer.from(privateKeyPem).toString('base64'), + } as unknown as CloudflareEnv); +} + +describe('BitbucketAuthorizationService', () => { + it('requires current membership for organization-scoped credentials', () => { + const db = getWorkerDb('postgres://query-builder'); + const query = buildBitbucketAuthorizationQuery(db, { + userId: 'member-1', + orgId: '123e4567-e89b-12d3-a456-426614174030', + }).toSQL(); + + expect(query.sql).toContain('exists (select'); + expect(query.sql).toContain('"organization_memberships"'); + expect(query.sql).toContain('"kilocode_users"."is_admin"'); + expect(query.params).toContain('member-1'); + expect(query.params).toContain('123e4567-e89b-12d3-a456-426614174030'); + }); + + beforeEach(() => { + database.row = activeRow(); + database.rows = undefined; + database.updates = []; + database.returnedCredential = undefined; + database.locks = 0; + vi.restoreAllMocks(); + }); + + it('returns a decrypted token only for an active selected workspace', async () => { + await expect(service().getAuthorization({ userId: 'user-1' })).resolves.toMatchObject({ + status: 'available', + token: 'access-token', + integrationId: 'integration-1', + workspace: { slug: 'acme' }, + }); + }); + + it('requires stored OAuth credentials to include webhook scope', async () => { + database.row = { + ...activeRow(), + scopes: ['account', 'email', 'pullrequest', 'repository', 'repository:write'], + }; + + await expect(service().getAuthorization({ userId: 'user-1' })).resolves.toEqual({ + status: 'reconnect_required', + }); + }); + + it('requires stored OAuth credentials to include pull request read scope', async () => { + database.row = { + ...activeRow(), + scopes: ['account', 'email', 'repository', 'repository:write', 'webhook'], + }; + + await expect(service().getAuthorization({ userId: 'user-1' })).resolves.toEqual({ + status: 'reconnect_required', + }); + }); + + it('decrypts organization credentials only with organization-bound AAD', async () => { + const orgId = '123e4567-e89b-12d3-a456-426614174030'; + database.row = activeRow(undefined, { type: 'org', id: orgId }); + + await expect(service().getAuthorization({ userId: 'member-1', orgId })).resolves.toMatchObject({ + status: 'available', + token: 'access-token', + }); + await expect(service().getAuthorization({ userId: 'user-1' })).resolves.toEqual({ + status: 'reconnect_required', + }); + }); + + it('returns workspace selection state without decrypting credentials', async () => { + database.row = { + ...activeRow(), + integrationStatus: 'pending', + installationId: null, + accountId: null, + accountLogin: null, + metadata: { + state: 'workspace_selection_required', + availableWorkspaces: [activeRow().metadata.workspace], + }, + }; + + await expect(service().getAuthorization({ userId: 'user-1' })).resolves.toEqual({ + status: 'workspace_selection_required', + }); + }); + + it('fails closed when organization access disappears before a credential refresh', async () => { + const orgId = '123e4567-e89b-12d3-a456-426614174030'; + database.rows = [ + activeRow(BITBUCKET_CLOUD_AGENT_MINIMUM_VALIDITY_MS - 1, { type: 'org', id: orgId }), + undefined, + ]; + const fetchMock = vi.fn(); + vi.stubGlobal('fetch', fetchMock); + + await expect( + service().getAuthorization( + { userId: 'member-1', orgId }, + BITBUCKET_CLOUD_AGENT_MINIMUM_VALIDITY_MS + ) + ).resolves.toEqual({ status: 'not_connected' }); + expect(database.locks).toBe(1); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('refreshes under a lock and rotates both credential envelopes', async () => { + database.row = activeRow(BITBUCKET_CLOUD_AGENT_MINIMUM_VALIDITY_MS - 1); + const nextCredential = { + ...credential(2 * 60 * 60 * 1000), + credential_version: 2, + access_token_encrypted: encryptKeyedEnvelope( + 'next-access-token', + scheme, + { keyId: 'active', publicKeyPem }, + aad('access') + ), + refresh_token_encrypted: encryptKeyedEnvelope( + 'next-refresh-token', + scheme, + { keyId: 'active', publicKeyPem }, + aad('refresh') + ), + }; + database.returnedCredential = nextCredential; + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + Response.json({ + access_token: 'next-access-token', + refresh_token: 'next-refresh-token', + token_type: 'bearer', + expires_in: 7200, + scope: 'account repository repository:write pullrequest webhook', + }) + ) + ); + + const result = await service().getAuthorization( + { userId: 'user-1' }, + BITBUCKET_CLOUD_AGENT_MINIMUM_VALIDITY_MS + ); + + expect(result).toMatchObject({ status: 'available', token: 'next-access-token' }); + expect(database.locks).toBe(1); + const rotation = database.updates.find(update => 'access_token_encrypted' in update); + expect(JSON.parse(String(rotation?.access_token_encrypted))).toMatchObject({ + scheme, + keyId: 'active', + }); + expect(JSON.parse(String(rotation?.refresh_token_encrypted))).toMatchObject({ + scheme, + keyId: 'active', + }); + expect(fetch).toHaveBeenCalledWith( + 'https://bitbucket.org/site/oauth2/access_token', + expect.objectContaining({ redirect: 'manual' }) + ); + }); + + it('normalizes Atlassian legacy scope aliases during credential refresh', async () => { + database.row = activeRow(BITBUCKET_CLOUD_AGENT_MINIMUM_VALIDITY_MS - 1); + database.returnedCredential = { + ...credential(2 * 60 * 60 * 1000), + credential_version: 2, + }; + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + Response.json({ + access_token: 'next-access-token', + refresh_token: 'next-refresh-token', + token_type: 'bearer', + expires_in: 7200, + scope: [ + 'read:pullrequest:bitbucket-legacy', + 'pullrequest', + 'offline_access', + 'write:repository:bitbucket-legacy', + 'read:account:bitbucket-legacy', + 'admin:webhook:bitbucket-legacy', + 'read:email:bitbucket-legacy', + 'read:repository:bitbucket-legacy', + 'snippet', + ].join(' '), + }) + ) + ); + + await expect( + service().getAuthorization({ userId: 'user-1' }, BITBUCKET_CLOUD_AGENT_MINIMUM_VALIDITY_MS) + ).resolves.toMatchObject({ status: 'available' }); + + expect(database.updates).toContainEqual( + expect.objectContaining({ + scopes: ['account', 'email', 'pullrequest', 'repository', 'repository:write', 'webhook'], + }) + ); + }); + + it('marks terminal invalid_grant refresh failures as reconnect required', async () => { + database.row = activeRow(-1); + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(Response.json({ error: 'invalid_grant' }, { status: 400 })) + ); + + await expect(service().getAuthorization({ userId: 'user-1' })).resolves.toEqual({ + status: 'reconnect_required', + }); + expect(database.updates).toContainEqual( + expect.objectContaining({ revocation_reason: 'refresh_token_rejected' }) + ); + }); +}); diff --git a/services/git-token-service/src/bitbucket-authorization-service.ts b/services/git-token-service/src/bitbucket-authorization-service.ts new file mode 100644 index 0000000000..ec15b3b4e8 --- /dev/null +++ b/services/git-token-service/src/bitbucket-authorization-service.ts @@ -0,0 +1,492 @@ +import { createPrivateKey, createPublicKey } from 'node:crypto'; +import { + decryptKeyedEnvelope, + encryptKeyedEnvelope, + EncryptionConfigurationError, +} from '@kilocode/encryption'; +import { getWorkerDb } from '@kilocode/db/client'; +import { + kilocode_users, + organization_memberships, + platform_integrations, + platform_oauth_credentials, +} from '@kilocode/db/schema'; +import { and, eq, exists, isNull, or, sql } from 'drizzle-orm'; +import { z } from 'zod'; + +const BITBUCKET_PLATFORM = 'bitbucket'; +const TOKEN_SCHEME = 'bitbucket-oauth-credential-rsa-aes-256-gcm'; +export const BITBUCKET_API_MINIMUM_VALIDITY_MS = 5 * 60 * 1000; +export const BITBUCKET_CLOUD_AGENT_MINIMUM_VALIDITY_MS = 55 * 60 * 1000; + +const WorkspaceSchema = z + .object({ + uuid: z.string().min(1), + slug: z.string().min(1), + name: z.string().min(1), + }) + .strict(); +const MetadataSchema = z.discriminatedUnion('state', [ + z + .object({ + state: z.literal('workspace_selection_required'), + availableWorkspaces: z.array(WorkspaceSchema).min(1), + }) + .strict(), + z.object({ state: z.literal('active'), workspace: WorkspaceSchema }).strict(), +]); +const RefreshResponseSchema = z.object({ + access_token: z.string().min(1), + refresh_token: z.string().min(1), + token_type: z + .string() + .transform(value => value.toLowerCase()) + .pipe(z.literal('bearer')), + expires_in: z + .number() + .int() + .positive() + .max(24 * 60 * 60), + scope: z.string(), +}); +const RefreshErrorSchema = z.object({ error: z.string() }); + +export type BitbucketAuthorizationOwner = { + userId: string; + orgId?: string; +}; +export type BitbucketWorkspaceIdentity = z.infer; +export type BitbucketAuthorizationResult = + | { + status: 'available'; + token: string; + integrationId: string; + workspace: BitbucketWorkspaceIdentity; + } + | { status: 'not_connected' } + | { status: 'workspace_selection_required' } + | { status: 'reconnect_required' } + | { status: 'temporarily_unavailable' }; + +type CredentialRow = typeof platform_oauth_credentials.$inferSelect; +type WorkerDb = ReturnType; +type WorkerTransaction = Parameters[0]>[0]; +type Secret = SecretsStoreSecret | string | undefined; +type BitbucketAuthorizationEnv = Pick & { + BITBUCKET_CLIENT_ID?: Secret; + BITBUCKET_CLIENT_SECRET?: Secret; + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID?: Secret; + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY?: Secret; + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY?: Secret; +}; + +type ActiveAuthorization = { + credential: CredentialRow; + integrationId: string; + owner: BitbucketAuthorizationOwner; + scopes: string[] | null; + workspace: BitbucketWorkspaceIdentity; +}; + +const BITBUCKET_OAUTH_SCOPE_ALIASES: Record = { + 'read:account:bitbucket-legacy': ['account'], + 'read:email:bitbucket-legacy': ['email'], + 'read:repository:bitbucket-legacy': ['repository'], + 'write:repository:bitbucket-legacy': ['repository:write'], + 'read:webhook:bitbucket-legacy': ['webhook'], + 'write:webhook:bitbucket-legacy': ['webhook'], + 'admin:webhook:bitbucket-legacy': ['webhook'], + pullrequest: ['pullrequest'], + 'read:pullrequest:bitbucket-legacy': ['pullrequest'], + offline_access: [], +}; + +async function resolveSecret(secret: Secret): Promise { + if (!secret) return null; + const value = typeof secret === 'string' ? secret : await secret.get(); + return value || null; +} + +function normalizedScopes(scope: string): string[] | null { + const scopes = new Set(); + for (const rawScope of scope.split(/\s+/).filter(Boolean)) { + for (const normalizedScope of BITBUCKET_OAUTH_SCOPE_ALIASES[rawScope.toLowerCase()] ?? [ + rawScope.toLowerCase(), + ]) { + scopes.add(normalizedScope); + } + } + + if (scopes.has('repository:write')) scopes.add('repository'); + if (scopes.has('account')) scopes.add('email'); + const allowed = new Set([ + 'account', + 'email', + 'repository', + 'repository:write', + 'pullrequest', + 'webhook', + ]); + if ( + !scopes.has('account') || + !scopes.has('repository:write') || + !scopes.has('pullrequest') || + !scopes.has('webhook') + ) { + return null; + } + return [...scopes].filter(scope => allowed.has(scope)).sort(); +} + +function hasRequiredStoredScopes(scopes: string[] | null): boolean { + if (!scopes) return false; + return normalizedScopes(scopes.join(' ')) !== null; +} + +function typedOwner(owner: BitbucketAuthorizationOwner) { + return owner.orgId ? { type: 'org', id: owner.orgId } : { type: 'user', id: owner.userId }; +} + +function ownerCondition(owner: BitbucketAuthorizationOwner) { + return owner.orgId + ? eq(platform_integrations.owned_by_organization_id, owner.orgId) + : and( + eq(platform_integrations.owned_by_user_id, owner.userId), + isNull(platform_integrations.owned_by_organization_id) + ); +} + +export function buildBitbucketAuthorizationQuery( + db: WorkerDb | WorkerTransaction, + owner: BitbucketAuthorizationOwner +) { + const currentOrganizationMembership = owner.orgId + ? exists( + db + .select({ id: organization_memberships.id }) + .from(organization_memberships) + .where( + and( + eq(organization_memberships.organization_id, owner.orgId), + eq(organization_memberships.kilo_user_id, owner.userId) + ) + ) + ) + : undefined; + const currentOrganizationAccess = owner.orgId + ? or(currentOrganizationMembership, eq(kilocode_users.is_admin, true)) + : undefined; + + return db + .select({ + credential: platform_oauth_credentials, + integrationId: platform_integrations.id, + integrationStatus: platform_integrations.integration_status, + installationId: platform_integrations.platform_installation_id, + accountId: platform_integrations.platform_account_id, + accountLogin: platform_integrations.platform_account_login, + scopes: platform_integrations.scopes, + metadata: platform_integrations.metadata, + }) + .from(platform_integrations) + .leftJoin( + platform_oauth_credentials, + and( + eq(platform_oauth_credentials.platform_integration_id, platform_integrations.id), + eq(platform_oauth_credentials.platform, BITBUCKET_PLATFORM) + ) + ) + .innerJoin( + kilocode_users, + and(eq(kilocode_users.id, owner.userId), isNull(kilocode_users.blocked_reason)) + ) + .where( + and( + ownerCondition(owner), + eq(platform_integrations.platform, BITBUCKET_PLATFORM), + currentOrganizationAccess + ) + ) + .limit(1); +} + +function credentialAad( + credential: CredentialRow, + owner: BitbucketAuthorizationOwner, + kind: 'access' | 'refresh' +): string { + return JSON.stringify({ + scheme: TOKEN_SCHEME, + version: 1, + platform: BITBUCKET_PLATFORM, + credentialId: credential.id, + integrationId: credential.platform_integration_id, + owner: typedOwner(owner), + authorizedByUserId: credential.authorized_by_user_id, + kind, + }); +} + +export class BitbucketAuthorizationService { + constructor(private env: BitbucketAuthorizationEnv) {} + + async getAuthorization( + owner: BitbucketAuthorizationOwner, + minimumValidityMs = BITBUCKET_API_MINIMUM_VALIDITY_MS + ): Promise { + if (!this.env.HYPERDRIVE) return { status: 'temporarily_unavailable' }; + const db = getWorkerDb(this.env.HYPERDRIVE.connectionString, { statement_timeout: 10_000 }); + const loaded = await this.loadAuthorization(db, owner); + if (loaded.status !== 'available') return loaded; + + let authorization = loaded.authorization; + const accessTokenExpiresAt = authorization.credential.access_token_expires_at; + if (!accessTokenExpiresAt) return { status: 'reconnect_required' }; + if (new Date(accessTokenExpiresAt).getTime() - Date.now() < minimumValidityMs) { + const refreshed = await this.refreshWithLock(db, authorization, minimumValidityMs); + if (refreshed.status !== 'available') return refreshed; + authorization = refreshed.authorization; + } + + const token = await this.decryptCredential(authorization, 'access'); + if (!token) return { status: 'reconnect_required' }; + await db + .update(platform_oauth_credentials) + .set({ last_used_at: new Date().toISOString() }) + .where(eq(platform_oauth_credentials.id, authorization.credential.id)); + return { + status: 'available', + token, + integrationId: authorization.integrationId, + workspace: authorization.workspace, + }; + } + + private async loadAuthorization( + db: WorkerDb | WorkerTransaction, + owner: BitbucketAuthorizationOwner + ): Promise< + | { status: 'available'; authorization: ActiveAuthorization } + | Exclude + > { + const [row] = await buildBitbucketAuthorizationQuery(db, owner); + if (!row) return { status: 'not_connected' }; + if (!row.credential || row.credential.revoked_at) return { status: 'reconnect_required' }; + + const metadata = MetadataSchema.safeParse(row.metadata); + if (!metadata.success) return { status: 'reconnect_required' }; + if ( + row.integrationStatus === 'pending' && + metadata.data.state === 'workspace_selection_required' + ) { + return { status: 'workspace_selection_required' }; + } + if ( + row.integrationStatus !== 'active' || + metadata.data.state !== 'active' || + row.installationId !== metadata.data.workspace.uuid || + row.accountId !== metadata.data.workspace.uuid || + row.accountLogin !== metadata.data.workspace.slug || + !hasRequiredStoredScopes(row.scopes) + ) { + return { status: 'reconnect_required' }; + } + return { + status: 'available', + authorization: { + credential: row.credential, + integrationId: row.integrationId, + owner, + scopes: row.scopes, + workspace: metadata.data.workspace, + }, + }; + } + + private async refreshWithLock( + db: WorkerDb, + candidate: ActiveAuthorization, + minimumValidityMs: number + ): Promise< + | { status: 'available'; authorization: ActiveAuthorization } + | Exclude + > { + return db.transaction(async tx => { + await tx.execute( + sql`SELECT pg_advisory_xact_lock(hashtextextended(${`bitbucket-oauth-credential:${candidate.credential.id}`}, 0))` + ); + const current = await this.loadAuthorization(tx, candidate.owner); + if (current.status !== 'available') return current; + const currentExpiry = current.authorization.credential.access_token_expires_at; + if (!currentExpiry) return { status: 'reconnect_required' }; + if ( + current.authorization.credential.credential_version !== + candidate.credential.credential_version || + new Date(currentExpiry).getTime() - Date.now() >= minimumValidityMs + ) { + return current; + } + return this.refreshAuthorization(tx, current.authorization); + }); + } + + private async refreshAuthorization( + tx: WorkerTransaction, + authorization: ActiveAuthorization + ): Promise< + | { status: 'available'; authorization: ActiveAuthorization } + | Exclude + > { + const [clientId, clientSecret, refreshToken] = await Promise.all([ + resolveSecret(this.env.BITBUCKET_CLIENT_ID), + resolveSecret(this.env.BITBUCKET_CLIENT_SECRET), + this.decryptCredential(authorization, 'refresh'), + ]); + if (!clientId || !clientSecret) return { status: 'temporarily_unavailable' }; + if (!refreshToken) return { status: 'reconnect_required' }; + + let response: Response; + try { + response = await fetch('https://bitbucket.org/site/oauth2/access_token', { + method: 'POST', + redirect: 'manual', + headers: { + Accept: 'application/json', + Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + }).toString(), + }); + } catch { + return { status: 'temporarily_unavailable' }; + } + + let responseBody: unknown; + try { + responseBody = await response.json(); + } catch { + return { status: 'temporarily_unavailable' }; + } + if (!response.ok) { + const error = RefreshErrorSchema.safeParse(responseBody); + if (error.success && error.data.error === 'invalid_grant') { + await tx + .update(platform_oauth_credentials) + .set({ + revoked_at: new Date().toISOString(), + revocation_reason: 'refresh_token_rejected', + }) + .where( + and( + eq(platform_oauth_credentials.id, authorization.credential.id), + eq( + platform_oauth_credentials.credential_version, + authorization.credential.credential_version + ), + isNull(platform_oauth_credentials.revoked_at) + ) + ); + return { status: 'reconnect_required' }; + } + return { status: 'temporarily_unavailable' }; + } + + const parsed = RefreshResponseSchema.safeParse(responseBody); + if (!parsed.success) return { status: 'temporarily_unavailable' }; + const scopes = normalizedScopes(parsed.data.scope); + if (!scopes) return { status: 'temporarily_unavailable' }; + const [accessTokenEncrypted, refreshTokenEncrypted] = await Promise.all([ + this.encryptCredential(parsed.data.access_token, authorization, 'access'), + this.encryptCredential(parsed.data.refresh_token, authorization, 'refresh'), + ]); + if (!accessTokenEncrypted || !refreshTokenEncrypted) { + return { status: 'temporarily_unavailable' }; + } + + const [updated] = await tx + .update(platform_oauth_credentials) + .set({ + access_token_encrypted: accessTokenEncrypted, + access_token_expires_at: new Date(Date.now() + parsed.data.expires_in * 1000).toISOString(), + refresh_token_encrypted: refreshTokenEncrypted, + credential_version: sql`${platform_oauth_credentials.credential_version} + 1`, + last_used_at: new Date().toISOString(), + }) + .where( + and( + eq(platform_oauth_credentials.id, authorization.credential.id), + eq( + platform_oauth_credentials.credential_version, + authorization.credential.credential_version + ), + isNull(platform_oauth_credentials.revoked_at) + ) + ) + .returning(); + if (!updated) return this.loadAuthorization(tx, authorization.owner); + + await tx + .update(platform_integrations) + .set({ scopes, updated_at: new Date().toISOString() }) + .where(eq(platform_integrations.id, authorization.integrationId)); + return { + status: 'available', + authorization: { ...authorization, credential: updated, scopes }, + }; + } + + private async decryptCredential( + authorization: ActiveAuthorization, + kind: 'access' | 'refresh' + ): Promise { + try { + const keyId = await resolveSecret(this.env.BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID); + const encodedPrivateKey = await resolveSecret( + this.env.BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY + ); + if (!keyId || !encodedPrivateKey) return null; + const privateKeyPem = Buffer.from(encodedPrivateKey, 'base64').toString('utf8'); + const privateKey = createPrivateKey(privateKeyPem); + if (privateKey.asymmetricKeyType !== 'rsa') return null; + return decryptKeyedEnvelope( + kind === 'access' + ? authorization.credential.access_token_encrypted + : authorization.credential.refresh_token_encrypted, + TOKEN_SCHEME, + { active: { keyId, privateKeyPem } }, + credentialAad(authorization.credential, authorization.owner, kind) + ); + } catch { + return null; + } + } + + private async encryptCredential( + value: string, + authorization: ActiveAuthorization, + kind: 'access' | 'refresh' + ): Promise { + try { + const keyId = await resolveSecret(this.env.BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID); + const encodedPublicKey = await resolveSecret( + this.env.BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY + ); + if (!keyId || !encodedPublicKey) return null; + const publicKeyPem = Buffer.from(encodedPublicKey, 'base64').toString('utf8'); + const publicKey = createPublicKey(publicKeyPem); + if (publicKey.asymmetricKeyType !== 'rsa') return null; + return encryptKeyedEnvelope( + value, + TOKEN_SCHEME, + { keyId, publicKeyPem }, + credentialAad(authorization.credential, authorization.owner, kind) + ); + } catch (error) { + if (error instanceof EncryptionConfigurationError) return null; + return null; + } + } +} diff --git a/services/git-token-service/src/bitbucket-runtime-token-resolver.test.ts b/services/git-token-service/src/bitbucket-runtime-token-resolver.test.ts new file mode 100644 index 0000000000..8655824a08 --- /dev/null +++ b/services/git-token-service/src/bitbucket-runtime-token-resolver.test.ts @@ -0,0 +1,284 @@ +import { describe, expect, it, vi } from 'vitest'; +import { BitbucketApiError, type BitbucketRepository } from './bitbucket-api.js'; +import { + listBitbucketRepositories, + resolveBitbucketToken, +} from './bitbucket-runtime-token-resolver.js'; +import type { BitbucketAuthorizationResult } from './bitbucket-authorization-service.js'; +import type { BitbucketWorkspaceAccessTokenAuthorization } from './bitbucket-workspace-access-token-authorization-service.js'; + +const organizationId = '123e4567-e89b-12d3-a456-426614174030'; +const workspaceUuid = '123e4567-e89b-12d3-a456-426614174031'; +const repositoryUuid = '123e4567-e89b-12d3-a456-426614174032'; +const authorization: BitbucketWorkspaceAccessTokenAuthorization = { + status: 'available', + token: 'ATCT-runtime-token', + organizationId, + integrationId: '123e4567-e89b-12d3-a456-426614174033', + credentialId: '123e4567-e89b-12d3-a456-426614174034', + credentialVersion: 7, + workspace: { uuid: workspaceUuid, slug: 'acme' }, +}; +const repository: BitbucketRepository = { + id: repositoryUuid, + workspaceUuid, + name: 'Widgets', + fullName: 'acme/widgets', + private: true, + defaultBranch: 'main', +}; +const oauthAuthorization: Extract = { + status: 'available', + token: 'oauth-runtime-token', + integrationId: '123e4567-e89b-12d3-a456-426614174044', + workspace: { uuid: workspaceUuid, slug: 'acme', name: 'Acme' }, +}; + +function dependencies() { + return { + authorizationService: { + getAuthorization: vi.fn().mockResolvedValue(authorization), + invalidateAuthorization: vi.fn().mockResolvedValue(undefined), + }, + oauthAuthorizationService: { + getAuthorization: vi.fn().mockResolvedValue({ status: 'not_connected' }), + }, + listRepositories: vi.fn().mockResolvedValue([repository]), + findRepository: vi.fn().mockResolvedValue(repository), + }; +} + +function tokenParams(overrides: Record = {}) { + return { + userId: 'member-1', + orgId: organizationId, + workspaceUuid, + repositoryUuid, + repositoryUrl: 'https://bitbucket.org/acme/widgets.git', + ...overrides, + }; +} + +describe('Bitbucket runtime token resolver', () => { + it('requires an organization before static credential lookup', async () => { + const deps = dependencies(); + + await expect( + resolveBitbucketToken({} as CloudflareEnv, tokenParams({ orgId: undefined }), deps) + ).resolves.toEqual({ success: false, reason: 'invalid_request' }); + await expect( + listBitbucketRepositories({} as CloudflareEnv, { userId: 'member-1' }, deps) + ).resolves.toEqual({ status: 'invalid_request' }); + expect(deps.authorizationService.getAuthorization).not.toHaveBeenCalled(); + expect(deps.oauthAuthorizationService.getAuthorization).not.toHaveBeenCalled(); + }); + + it('releases only the opaque token after exact workspace and repository validation', async () => { + const deps = dependencies(); + + await expect(resolveBitbucketToken({} as CloudflareEnv, tokenParams(), deps)).resolves.toEqual({ + success: true, + token: 'ATCT-runtime-token', + }); + expect(deps.authorizationService.getAuthorization).toHaveBeenCalledWith({ + userId: 'member-1', + orgId: organizationId, + }); + expect(deps.findRepository).toHaveBeenCalledWith({ + accessToken: 'ATCT-runtime-token', + workspace: authorization.workspace, + repositoryUuid, + }); + expect(deps.oauthAuthorizationService.getAuthorization).not.toHaveBeenCalled(); + }); + + it('lists repositories through the same organization-only static authorization', async () => { + const deps = dependencies(); + + await expect( + listBitbucketRepositories( + {} as CloudflareEnv, + { userId: 'member-1', orgId: organizationId }, + deps + ) + ).resolves.toEqual({ status: 'available', repositories: [repository] }); + expect(deps.listRepositories).toHaveBeenCalledWith({ + accessToken: 'ATCT-runtime-token', + workspace: authorization.workspace, + }); + expect(deps.oauthAuthorizationService.getAuthorization).not.toHaveBeenCalled(); + }); + + it('falls back to OAuth authorization when no Workspace Access Token is connected', async () => { + const deps = dependencies(); + deps.authorizationService.getAuthorization.mockResolvedValue({ status: 'not_connected' }); + deps.oauthAuthorizationService.getAuthorization.mockResolvedValue(oauthAuthorization); + + await expect( + listBitbucketRepositories( + {} as CloudflareEnv, + { userId: 'member-1', orgId: organizationId }, + deps + ) + ).resolves.toEqual({ status: 'available', repositories: [repository] }); + expect(deps.oauthAuthorizationService.getAuthorization).toHaveBeenCalledWith({ + userId: 'member-1', + orgId: organizationId, + }); + expect(deps.listRepositories).toHaveBeenCalledWith({ + accessToken: 'oauth-runtime-token', + workspace: oauthAuthorization.workspace, + }); + }); + + it('does not fall back to OAuth while a Workspace Access Token needs attention', async () => { + const deps = dependencies(); + deps.authorizationService.getAuthorization.mockResolvedValue({ status: 'reconnect_required' }); + deps.oauthAuthorizationService.getAuthorization.mockResolvedValue(oauthAuthorization); + + await expect( + listBitbucketRepositories( + {} as CloudflareEnv, + { userId: 'member-1', orgId: organizationId }, + deps + ) + ).resolves.toEqual({ status: 'reconnect_required' }); + expect(deps.oauthAuthorizationService.getAuthorization).not.toHaveBeenCalled(); + }); + + it.each([ + ['authentication_rejected', 'reconnect_required', 'provider_rejected'], + ['insufficient_permissions', 'insufficient_permissions', null], + ['workspace_mismatch', 'reconnect_required', 'workspace_mismatch'], + ['rate_limited', 'temporarily_unavailable', null], + ] as const)( + 'maps repository-list provider failure %s to %s', + async (code, status, invalidationReason) => { + const deps = dependencies(); + deps.listRepositories.mockRejectedValue(new BitbucketApiError(code)); + + await expect( + listBitbucketRepositories( + {} as CloudflareEnv, + { userId: 'member-1', orgId: organizationId }, + deps + ) + ).resolves.toEqual({ status }); + if (invalidationReason) { + expect(deps.authorizationService.invalidateAuthorization).toHaveBeenCalledTimes(1); + expect(deps.authorizationService.invalidateAuthorization).toHaveBeenCalledWith( + authorization, + invalidationReason + ); + } else { + expect(deps.authorizationService.invalidateAuthorization).not.toHaveBeenCalled(); + } + } + ); + + it('generation-fences provider 401 invalidation', async () => { + const deps = dependencies(); + deps.findRepository.mockRejectedValue(new BitbucketApiError('authentication_rejected')); + + await expect(resolveBitbucketToken({} as CloudflareEnv, tokenParams(), deps)).resolves.toEqual({ + success: false, + reason: 'reconnect_required', + }); + expect(deps.authorizationService.invalidateAuthorization).toHaveBeenCalledTimes(1); + expect(deps.authorizationService.invalidateAuthorization).toHaveBeenCalledWith( + authorization, + 'provider_rejected' + ); + }); + + it('maps a collection-level provider 404 to reconnect without invalidation', async () => { + const deps = dependencies(); + deps.findRepository.mockRejectedValue(new BitbucketApiError('not_found')); + + await expect(resolveBitbucketToken({} as CloudflareEnv, tokenParams(), deps)).resolves.toEqual({ + success: false, + reason: 'reconnect_required', + }); + expect(deps.authorizationService.invalidateAuthorization).not.toHaveBeenCalled(); + }); + + it('reports 403 without invalidating the credential', async () => { + const deps = dependencies(); + deps.findRepository.mockRejectedValue(new BitbucketApiError('insufficient_permissions')); + + await expect(resolveBitbucketToken({} as CloudflareEnv, tokenParams(), deps)).resolves.toEqual({ + success: false, + reason: 'insufficient_permissions', + }); + expect(deps.authorizationService.invalidateAuthorization).not.toHaveBeenCalled(); + }); + + it.each([ + 'rate_limited', + 'provider_unavailable', + 'transport_failed', + 'request_timed_out', + 'invalid_response', + ] as const)('keeps temporary provider failure %s non-invalidating', async code => { + const deps = dependencies(); + deps.findRepository.mockRejectedValue(new BitbucketApiError(code)); + + await expect(resolveBitbucketToken({} as CloudflareEnv, tokenParams(), deps)).resolves.toEqual({ + success: false, + reason: 'temporarily_unavailable', + }); + expect(deps.authorizationService.invalidateAuthorization).not.toHaveBeenCalled(); + }); + + it('generation-fences a provider workspace mismatch before refusing release', async () => { + const deps = dependencies(); + deps.findRepository.mockRejectedValue(new BitbucketApiError('workspace_mismatch')); + + await expect(resolveBitbucketToken({} as CloudflareEnv, tokenParams(), deps)).resolves.toEqual({ + success: false, + reason: 'workspace_mismatch', + }); + expect(deps.authorizationService.invalidateAuthorization).toHaveBeenCalledTimes(1); + expect(deps.authorizationService.invalidateAuthorization).toHaveBeenCalledWith( + authorization, + 'workspace_mismatch' + ); + }); + + it.each([ + [{ workspaceUuid: '123e4567-e89b-12d3-a456-426614174099' }, 'workspace_mismatch'], + [{ repositoryUrl: 'https://bitbucket.org/other/widgets.git' }, 'workspace_mismatch'], + [{ repositoryUrl: 'https://user@bitbucket.org/acme/widgets.git' }, 'invalid_request'], + [{ integrationId: '123e4567-e89b-12d3-a456-426614174099' }, 'not_connected'], + ] as const)( + 'fails before provider access when request identity drifts %#', + async (overrides, reason) => { + const deps = dependencies(); + + await expect( + resolveBitbucketToken({} as CloudflareEnv, tokenParams(overrides), deps) + ).resolves.toEqual({ success: false, reason }); + expect(deps.findRepository).not.toHaveBeenCalled(); + expect(deps.authorizationService.invalidateAuthorization).not.toHaveBeenCalled(); + } + ); + + it.each([ + [{ ...repository, id: '123e4567-e89b-12d3-a456-426614174099' }, 'repository_mismatch'], + [ + { ...repository, workspaceUuid: '123e4567-e89b-12d3-a456-426614174099' }, + 'repository_mismatch', + ], + [{ ...repository, fullName: 'acme/other' }, 'repository_mismatch'], + [null, 'repository_not_found'], + ] as const)('does not release for provider repository mismatch %#', async (resolved, reason) => { + const deps = dependencies(); + deps.findRepository.mockResolvedValue(resolved); + + await expect(resolveBitbucketToken({} as CloudflareEnv, tokenParams(), deps)).resolves.toEqual({ + success: false, + reason, + }); + expect(deps.authorizationService.invalidateAuthorization).not.toHaveBeenCalled(); + }); +}); diff --git a/services/git-token-service/src/bitbucket-runtime-token-resolver.ts b/services/git-token-service/src/bitbucket-runtime-token-resolver.ts new file mode 100644 index 0000000000..71ef73bf2b --- /dev/null +++ b/services/git-token-service/src/bitbucket-runtime-token-resolver.ts @@ -0,0 +1,280 @@ +import { + BitbucketApiError, + findBitbucketWorkspaceRepositoryByUuid, + listBitbucketWorkspaceRepositories, + type BitbucketRepository, + type BitbucketRepositoryApiOptions, +} from './bitbucket-api.js'; +import { + BitbucketAuthorizationService, + type BitbucketAuthorizationResult, +} from './bitbucket-authorization-service.js'; +import { + BitbucketWorkspaceAccessTokenAuthorizationService, + type BitbucketWorkspaceAccessTokenAuthorization, + type BitbucketWorkspaceAccessTokenAuthorizationResult, +} from './bitbucket-workspace-access-token-authorization-service.js'; +import { normalizeBitbucketUuid, parseBitbucketCloneUrl } from './bitbucket-url.js'; + +export type BitbucketRepositoryListResult = + | { status: 'available'; repositories: BitbucketRepository[] } + | { status: 'invalid_request' } + | { status: 'not_connected' } + | { status: 'reconnect_required' } + | { status: 'insufficient_permissions' } + | { status: 'temporarily_unavailable' }; + +export type GetBitbucketTokenParams = { + userId: string; + orgId?: string; + integrationId?: string; + workspaceUuid: string; + repositoryUuid: string; + repositoryUrl: string; +}; + +export type GetBitbucketTokenResult = + | { success: true; token: string } + | { + success: false; + reason: + | 'invalid_request' + | 'not_connected' + | 'reconnect_required' + | 'temporarily_unavailable' + | 'insufficient_permissions' + | 'workspace_mismatch' + | 'repository_not_found' + | 'repository_mismatch'; + }; + +type AuthorizationService = { + getAuthorization(input: { + userId: string; + orgId?: string; + }): Promise; + invalidateAuthorization( + authorization: BitbucketWorkspaceAccessTokenAuthorization, + reason: 'provider_rejected' | 'workspace_mismatch' + ): Promise; +}; +type OAuthAuthorizationService = { + getAuthorization(input: { + userId: string; + orgId?: string; + }): Promise; +}; +type RuntimeAuthorization = + | (BitbucketWorkspaceAccessTokenAuthorization & { source: 'workspace_access_token' }) + | (Extract & { source: 'oauth' }); + +export type BitbucketRuntimeTokenResolverDependencies = { + authorizationService: AuthorizationService; + oauthAuthorizationService: OAuthAuthorizationService; + listRepositories(options: BitbucketRepositoryApiOptions): Promise; + findRepository( + options: BitbucketRepositoryApiOptions & { repositoryUuid: string } + ): Promise; +}; + +function dependencies( + env: CloudflareEnv, + overrides?: BitbucketRuntimeTokenResolverDependencies +): BitbucketRuntimeTokenResolverDependencies { + if (overrides) return overrides; + return { + authorizationService: new BitbucketWorkspaceAccessTokenAuthorizationService(env), + oauthAuthorizationService: new BitbucketAuthorizationService(env), + listRepositories: listBitbucketWorkspaceRepositories, + findRepository: findBitbucketWorkspaceRepositoryByUuid, + }; +} + +type CanonicalProviderFailure = + | 'invalid_request' + | 'reconnect_required' + | 'temporarily_unavailable' + | 'insufficient_permissions' + | 'workspace_mismatch'; + +async function classifyProviderError( + error: BitbucketApiError, + authorization: RuntimeAuthorization, + authorizationService: AuthorizationService +): Promise { + switch (error.code) { + case 'authentication_rejected': + if (authorization.source === 'workspace_access_token') { + const { source: _source, ...workspaceAuthorization } = authorization; + await authorizationService.invalidateAuthorization( + workspaceAuthorization, + 'provider_rejected' + ); + } + return 'reconnect_required'; + case 'workspace_mismatch': + if (authorization.source === 'workspace_access_token') { + const { source: _source, ...workspaceAuthorization } = authorization; + await authorizationService.invalidateAuthorization( + workspaceAuthorization, + 'workspace_mismatch' + ); + } + return 'workspace_mismatch'; + case 'insufficient_permissions': + return 'insufficient_permissions'; + case 'not_found': + return 'reconnect_required'; + case 'invalid_request': + return 'invalid_request'; + case 'request_failed': + case 'request_timed_out': + case 'transport_failed': + case 'rate_limited': + case 'provider_unavailable': + case 'redirect_rejected': + case 'invalid_response': + case 'invalid_pagination': + case 'page_limit_exceeded': + case 'item_limit_exceeded': + case 'response_too_large': + return 'temporarily_unavailable'; + } +} + +function toRepositoryListFailure( + failure: CanonicalProviderFailure +): Exclude { + return { status: failure === 'workspace_mismatch' ? 'reconnect_required' : failure }; +} + +function toTokenFailure( + failure: CanonicalProviderFailure +): Exclude { + return { success: false, reason: failure }; +} + +async function getRuntimeAuthorization( + owner: { userId: string; orgId?: string }, + runtimeDependencies: BitbucketRuntimeTokenResolverDependencies +): Promise< + | { status: 'available'; authorization: RuntimeAuthorization } + | Exclude +> { + const workspaceAccessTokenAuthorization = + await runtimeDependencies.authorizationService.getAuthorization(owner); + if (workspaceAccessTokenAuthorization.status === 'available') { + return { + status: 'available', + authorization: { ...workspaceAccessTokenAuthorization, source: 'workspace_access_token' }, + }; + } + if (workspaceAccessTokenAuthorization.status !== 'not_connected') { + return workspaceAccessTokenAuthorization; + } + + const oauthAuthorization = + await runtimeDependencies.oauthAuthorizationService.getAuthorization(owner); + if (oauthAuthorization.status === 'available') { + return { + status: 'available', + authorization: { ...oauthAuthorization, source: 'oauth' }, + }; + } + if (oauthAuthorization.status === 'workspace_selection_required') { + return { status: 'reconnect_required' }; + } + return oauthAuthorization; +} + +export async function listBitbucketRepositories( + env: CloudflareEnv, + owner: { userId: string; orgId?: string }, + dependencyOverrides?: BitbucketRuntimeTokenResolverDependencies +): Promise { + if (!owner.orgId) return { status: 'invalid_request' }; + const runtimeDependencies = dependencies(env, dependencyOverrides); + const authorizationResult = await getRuntimeAuthorization(owner, runtimeDependencies); + if (authorizationResult.status !== 'available') return authorizationResult; + const authorization = authorizationResult.authorization; + + try { + return { + status: 'available', + repositories: await runtimeDependencies.listRepositories({ + accessToken: authorization.token, + workspace: authorization.workspace, + }), + }; + } catch (error) { + if (error instanceof BitbucketApiError) { + const failure = await classifyProviderError( + error, + authorization, + runtimeDependencies.authorizationService + ); + return toRepositoryListFailure(failure); + } + throw error; + } +} + +export async function resolveBitbucketToken( + env: CloudflareEnv, + params: GetBitbucketTokenParams, + dependencyOverrides?: BitbucketRuntimeTokenResolverDependencies +): Promise { + if (!params.orgId) return { success: false, reason: 'invalid_request' }; + const workspaceUuid = normalizeBitbucketUuid(params.workspaceUuid); + const repositoryUuid = normalizeBitbucketUuid(params.repositoryUuid); + const parsedUrl = parseBitbucketCloneUrl(params.repositoryUrl); + if (!workspaceUuid || !repositoryUuid || !parsedUrl.success) { + return { success: false, reason: 'invalid_request' }; + } + + const runtimeDependencies = dependencies(env, dependencyOverrides); + const authorizationResult = await getRuntimeAuthorization( + { userId: params.userId, orgId: params.orgId }, + runtimeDependencies + ); + if (authorizationResult.status !== 'available') { + return { success: false, reason: authorizationResult.status }; + } + const authorization = authorizationResult.authorization; + if (params.integrationId && params.integrationId !== authorization.integrationId) { + return { success: false, reason: 'not_connected' }; + } + if ( + authorization.workspace.uuid !== workspaceUuid || + authorization.workspace.slug !== parsedUrl.workspace + ) { + return { success: false, reason: 'workspace_mismatch' }; + } + + try { + const repository = await runtimeDependencies.findRepository({ + accessToken: authorization.token, + workspace: authorization.workspace, + repositoryUuid, + }); + if (!repository) return { success: false, reason: 'repository_not_found' }; + if ( + repository.id !== repositoryUuid || + repository.workspaceUuid !== workspaceUuid || + repository.fullName !== parsedUrl.fullName + ) { + return { success: false, reason: 'repository_mismatch' }; + } + return { success: true, token: authorization.token }; + } catch (error) { + if (error instanceof BitbucketApiError) { + const failure = await classifyProviderError( + error, + authorization, + runtimeDependencies.authorizationService + ); + return toTokenFailure(failure); + } + throw error; + } +} diff --git a/services/git-token-service/src/bitbucket-url.test.ts b/services/git-token-service/src/bitbucket-url.test.ts new file mode 100644 index 0000000000..83e71faee5 --- /dev/null +++ b/services/git-token-service/src/bitbucket-url.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest'; +import { normalizeBitbucketUuid, parseBitbucketCloneUrl } from './bitbucket-url.js'; + +describe('parseBitbucketCloneUrl', () => { + it('parses a canonical credential-free Bitbucket clone URL', () => { + expect(parseBitbucketCloneUrl('https://bitbucket.org/acme/widgets.git')).toEqual({ + success: true, + workspace: 'acme', + repository: 'widgets', + fullName: 'acme/widgets', + }); + }); + + it.each([ + 'http://bitbucket.org/acme/widgets.git', + 'ssh://git@bitbucket.org/acme/widgets.git', + 'git@bitbucket.org:acme/widgets.git', + 'https://user@bitbucket.org/acme/widgets.git', + 'https://user:password@bitbucket.org/acme/widgets.git', + 'https://bitbucket.org:443/acme/widgets.git', + 'https://bitbucket.org:8443/acme/widgets.git', + 'https://bitbucket.org/acme/widgets.git?ref=main', + 'https://bitbucket.org/acme/widgets.git#readme', + 'https://bitbucket.org/acme%2Fother/widgets.git', + 'https://bitbucket.org/acme%252Fother/widgets.git', + 'https://bitbucket.org/acme%5Cother/widgets.git', + 'https://bitbucket.org/acme/widgets%2Fother.git', + 'https://bitbucket.org/acme/widgets%5Cother.git', + 'https://bitbucket.org/acme/widgets%255Cother.git', + 'https://bitbucket.org/acme\\other/widgets.git', + 'https://bitbucket.org/./widgets.git', + 'https://bitbucket.org/acme/../widgets.git', + 'https://bitbucket.org/%2e/widgets.git', + 'https://bitbucket.org/%252e%252e/widgets.git', + 'https://bitbucket.org/acme/%2e%2e.git', + 'https://bitbucket.org/acme/%2e..git', + 'https://bitbucket.org//acme/widgets.git', + 'https://bitbucket.org/acme//widgets.git', + 'https://bitbucket.org/acme/widgets/extra.git', + 'https://bitbucket.org/acme/widgets.git/extra', + 'https://bitbucket.org/ac%ZZme/widgets.git', + 'https://bitbucket.org/acme/%E0%A4%A.git', + 'https://bitbucket.org/acme/widgets', + 'https://bitbucket.org/acme/widgets.GIT', + 'https://bitbucket.org/acme/.git', + 'https://bitbucket.org/acme/widgets.git/', + 'https://bitbucket.org.evil.example/acme/widgets.git', + 'https://bitbucket.org./acme/widgets.git', + 'HTTPS://bitbucket.org/acme/widgets.git', + ])('rejects non-canonical or unsafe URL %s', repositoryUrl => { + expect(parseBitbucketCloneUrl(repositoryUrl)).toEqual({ + success: false, + reason: 'invalid_bitbucket_url', + }); + }); +}); + +describe('normalizeBitbucketUuid', () => { + it.each([ + ['A07D5C40-2D2D-4E79-A812-6A47824A77D6', 'a07d5c40-2d2d-4e79-a812-6a47824a77d6'], + ['{a07d5c40-2d2d-4e79-a812-6a47824a77d6}', 'a07d5c40-2d2d-4e79-a812-6a47824a77d6'], + ['{A07D5C40-2D2D-4E79-A812-6A47824A77D6}', 'a07d5c40-2d2d-4e79-a812-6a47824a77d6'], + ])('normalizes provider UUID %s', (providerUuid, expected) => { + expect(normalizeBitbucketUuid(providerUuid)).toBe(expected); + }); + + it.each([ + '', + '{}', + '{a07d5c40-2d2d-4e79-a812-6a47824a77d6', + 'a07d5c40-2d2d-4e79-a812-6a47824A77D6}', + '{a07d5c40-2d2d-4e79-a812-6a47824a77d6}}', + 'not-a-uuid', + 'a07d5c402d2d4e79a8126a47824a77d6', + ])('rejects malformed provider UUID %s', providerUuid => { + expect(normalizeBitbucketUuid(providerUuid)).toBeNull(); + }); +}); diff --git a/services/git-token-service/src/bitbucket-url.ts b/services/git-token-service/src/bitbucket-url.ts new file mode 100644 index 0000000000..3865a4f22c --- /dev/null +++ b/services/git-token-service/src/bitbucket-url.ts @@ -0,0 +1,50 @@ +export type BitbucketCloneUrlResult = + | { + success: true; + workspace: string; + repository: string; + fullName: string; + } + | { success: false; reason: 'invalid_bitbucket_url' }; + +const UUID_PATTERN = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'; +const PROVIDER_UUID_PATTERN = new RegExp(`^(?:\\{(${UUID_PATTERN})\\}|(${UUID_PATTERN}))$`, 'i'); + +export function normalizeBitbucketUuid(value: string): string | null { + const match = PROVIDER_UUID_PATTERN.exec(value); + if (!match) return null; + const uuid = match[1] ?? match[2]; + return uuid ? uuid.toLowerCase() : null; +} + +function normalizePathSegment(value: string): string | null { + let decoded: string; + try { + decoded = decodeURIComponent(value); + } catch { + return null; + } + + if (!/^[A-Za-z0-9_.-]+$/.test(decoded) || decoded === '.' || decoded === '..') { + return null; + } + return decoded; +} + +export function parseBitbucketCloneUrl(repositoryUrl: string): BitbucketCloneUrlResult { + const match = /^https:\/\/bitbucket\.org\/([^/]+)\/([^/]+)\.git$/.exec(repositoryUrl); + if (!match || match[0] !== repositoryUrl) { + return { success: false, reason: 'invalid_bitbucket_url' }; + } + + const workspace = normalizePathSegment(match[1]); + const repository = normalizePathSegment(match[2]); + if (!workspace || !repository) return { success: false, reason: 'invalid_bitbucket_url' }; + + return { + success: true, + workspace, + repository, + fullName: `${workspace}/${repository}`, + }; +} diff --git a/services/git-token-service/src/bitbucket-workspace-access-token-authorization-service.test.ts b/services/git-token-service/src/bitbucket-workspace-access-token-authorization-service.test.ts new file mode 100644 index 0000000000..9a0b15f052 --- /dev/null +++ b/services/git-token-service/src/bitbucket-workspace-access-token-authorization-service.test.ts @@ -0,0 +1,429 @@ +import { generateKeyPairSync } from 'node:crypto'; +import { getWorkerDb } from '@kilocode/db/client'; +import { encryptKeyedEnvelope } from '@kilocode/encryption'; +import { + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_PROVIDER_CREDENTIAL_TYPE, + buildBitbucketWorkspaceAccessTokenAad, + type BitbucketWorkspaceAccessTokenInvalidationReason, +} from '@kilocode/worker-utils/bitbucket-workspace-access-token'; +import { describe, expect, it } from 'vitest'; +import { + BitbucketWorkspaceAccessTokenAuthorizationService, + buildBitbucketWorkspaceAccessTokenAuthorizationQuery, + buildBitbucketWorkspaceAccessTokenCredentialGenerationQuery, + buildBitbucketWorkspaceAccessTokenInvalidationQuery, + buildBitbucketWorkspaceAccessTokenMarkUsedQuery, + withBitbucketWorkspaceAccessTokenOrganizationLock, + type BitbucketWorkspaceAccessTokenAuthorizationCandidate, + type BitbucketWorkspaceAccessTokenAuthorizationFence, + type BitbucketWorkspaceAccessTokenAuthorizationStore, +} from './bitbucket-workspace-access-token-authorization-service.js'; + +const organizationId = '123e4567-e89b-12d3-a456-426614174030'; +const integrationId = '123e4567-e89b-12d3-a456-426614174031'; +const credentialId = '123e4567-e89b-12d3-a456-426614174032'; +const workspaceUuid = '123e4567-e89b-12d3-a456-426614174033'; +const now = new Date('2026-06-24T10:00:00.000Z'); +const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); +const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString(); +const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(); +const mismatchedPublicKeyPem = generateKeyPairSync('rsa', { modulusLength: 2048 }) + .publicKey.export({ type: 'spki', format: 'pem' }) + .toString(); + +function fence(candidate: BitbucketWorkspaceAccessTokenAuthorizationCandidate) { + return { + organizationId: candidate.organizationId, + integrationId: candidate.integrationId, + credentialId: candidate.credentialId, + credentialVersion: candidate.credentialVersion, + }; +} + +function candidate( + overrides: Partial = {} +): BitbucketWorkspaceAccessTokenAuthorizationCandidate { + const credentialVersion = overrides.credentialVersion ?? 3; + const tokenEncrypted = encryptKeyedEnvelope( + 'ATCT-runtime-token', + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + { keyId: 'active', publicKeyPem }, + buildBitbucketWorkspaceAccessTokenAad({ + organizationId, + integrationId, + credentialId, + credentialVersion, + }) + ); + + return { + integrationId, + credentialId, + organizationId, + ownedByUserId: null, + platform: BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM, + integrationType: BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE, + integrationStatus: 'active', + installationId: null, + accountId: workspaceUuid, + accountLogin: 'acme', + authInvalidAt: null, + credentialPlatform: BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM, + credentialIntegrationType: BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE, + tokenEncrypted, + providerCredentialType: BITBUCKET_WORKSPACE_ACCESS_TOKEN_PROVIDER_CREDENTIAL_TYPE, + providerScopes: ['account', 'pullrequest', 'repository', 'repository:write', 'webhook'], + providerVerifiedAt: '2026-06-24T09:00:00.000Z', + lastValidatedAt: '2026-06-24T09:00:00.000Z', + credentialVersion, + ...overrides, + }; +} + +class StatefulAuthorizationStore implements BitbucketWorkspaceAccessTokenAuthorizationStore { + loadCount = 0; + used: Array<{ fence: BitbucketWorkspaceAccessTokenAuthorizationFence; at: string }> = []; + invalidations: Array<{ + fence: BitbucketWorkspaceAccessTokenAuthorizationFence; + reason: BitbucketWorkspaceAccessTokenInvalidationReason; + at: string; + }> = []; + + constructor(public current: BitbucketWorkspaceAccessTokenAuthorizationCandidate | null) {} + + async findAuthorization(): Promise { + this.loadCount += 1; + return this.current; + } + + async markUsed( + authorizationFence: BitbucketWorkspaceAccessTokenAuthorizationFence, + at: string + ): Promise { + if ( + !this.current || + JSON.stringify(fence(this.current)) !== JSON.stringify(authorizationFence) + ) { + return false; + } + this.used.push({ fence: authorizationFence, at }); + return true; + } + + async invalidate( + authorizationFence: BitbucketWorkspaceAccessTokenAuthorizationFence, + reason: BitbucketWorkspaceAccessTokenInvalidationReason, + at: string + ): Promise { + if ( + !this.current || + JSON.stringify(fence(this.current)) !== JSON.stringify(authorizationFence) + ) { + return false; + } + this.invalidations.push({ fence: authorizationFence, reason, at }); + return true; + } +} + +function service(store: StatefulAuthorizationStore, envOverrides: Record = {}) { + return new BitbucketWorkspaceAccessTokenAuthorizationService( + { + HYPERDRIVE: { connectionString: 'postgres://unused' }, + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID: 'active', + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY: Buffer.from(publicKeyPem).toString('base64'), + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY: Buffer.from(privateKeyPem).toString('base64'), + ...envOverrides, + } as CloudflareEnv, + { store, now: () => now } + ); +} + +describe('BitbucketWorkspaceAccessTokenAuthorizationService', () => { + it('queries an exact organization credential for a current member or platform admin', () => { + const query = buildBitbucketWorkspaceAccessTokenAuthorizationQuery( + getWorkerDb('postgres://query-builder'), + { userId: 'member-1', organizationId } + ).toSQL(); + + expect(query.sql).toContain('inner join "platform_access_token_credentials"'); + expect(query.sql).not.toContain('"platform_access_token_credentials"."expires_at"'); + expect(query.sql).toContain('inner join "kilocode_users"'); + expect(query.sql).toContain('exists (select'); + expect(query.sql).toContain('"organization_memberships"'); + expect(query.sql).toContain('"kilocode_users"."blocked_reason" is null'); + expect(query.sql).toContain('"kilocode_users"."is_admin" ='); + expect(query.sql).toContain('"platform_integrations"."owned_by_user_id" is null'); + expect(query.params).toContain('member-1'); + expect(query.params).toContain(organizationId); + expect(query.params).toContain(BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM); + expect(query.params).toContain(BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE); + }); + + it('uses exact credential generation fences', () => { + const db = getWorkerDb('postgres://query-builder'); + const authorizationFence = fence(candidate()); + + const generationQuery = buildBitbucketWorkspaceAccessTokenCredentialGenerationQuery( + db, + authorizationFence + ).toSQL(); + expect(generationQuery.sql).toContain('from "platform_access_token_credentials"'); + expect(generationQuery.sql).toContain('"owned_by_organization_id" ='); + expect(generationQuery.sql).toContain('"platform_integration_id" ='); + expect(generationQuery.sql).toContain('"credential_version" ='); + expect(generationQuery.params).toEqual( + expect.arrayContaining([organizationId, integrationId, credentialId, 3]) + ); + + const invalidationQuery = buildBitbucketWorkspaceAccessTokenInvalidationQuery( + db, + authorizationFence, + 'provider_rejected', + now.toISOString() + ).toSQL(); + expect(invalidationQuery.sql).not.toContain('exists (select'); + expect(invalidationQuery.sql).toContain('update "platform_integrations"'); + expect(invalidationQuery.sql).toContain('"owned_by_organization_id" ='); + expect(invalidationQuery.params).toEqual( + expect.arrayContaining([organizationId, integrationId, 'provider_rejected']) + ); + }); + + it('acquires the organization transaction lock before the fresh generation recheck', async () => { + const events: string[] = []; + const database = { + transaction: async ( + operation: (tx: { execute: () => Promise }) => Promise + ) => { + events.push('transaction'); + return operation({ + execute: async () => { + events.push('lock'); + }, + }); + }, + }; + + await expect( + withBitbucketWorkspaceAccessTokenOrganizationLock( + database as never, + organizationId, + async () => { + events.push('fresh-generation-recheck'); + return true; + } + ) + ).resolves.toBe(true); + expect(events).toEqual(['transaction', 'lock', 'fresh-generation-recheck']); + }); + + it('makes exact-generation last-used writes monotonic', () => { + const query = buildBitbucketWorkspaceAccessTokenMarkUsedQuery( + getWorkerDb('postgres://query-builder'), + fence(candidate()), + now.toISOString() + ).toSQL(); + + expect(query.sql).toContain('update "platform_access_token_credentials"'); + expect(query.sql).toContain('"owned_by_organization_id" ='); + expect(query.sql).toContain('"platform_integration_id" ='); + expect(query.sql).toContain('"credential_version" ='); + expect(query.sql).toContain('"last_used_at" is null'); + expect(query.sql).toContain('"last_used_at" <'); + expect(query.params).toEqual( + expect.arrayContaining([organizationId, integrationId, credentialId, 3, now.toISOString()]) + ); + }); + + it.each([undefined, '', 'not-an-organization-id'])( + 'requires a valid organization %j before credential lookup', + async orgId => { + const store = new StatefulAuthorizationStore(candidate()); + + await expect(service(store).getAuthorization({ userId: 'member-1', orgId })).resolves.toEqual( + { + status: 'invalid_request', + } + ); + expect(store.loadCount).toBe(0); + } + ); + + it('decrypts a verified organization Workspace Access Token and fences last use', async () => { + const storedCandidate = candidate(); + const store = new StatefulAuthorizationStore(storedCandidate); + + await expect( + service(store).getAuthorization({ userId: 'member-1', orgId: organizationId }) + ).resolves.toEqual({ + status: 'available', + token: 'ATCT-runtime-token', + organizationId, + integrationId, + credentialId, + credentialVersion: 3, + workspace: { uuid: workspaceUuid, slug: 'acme' }, + }); + expect(store.used).toEqual([{ fence: fence(storedCandidate), at: now.toISOString() }]); + expect(store.invalidations).toEqual([]); + }); + + it('treats missing and failed key resolution as temporary without invalidation', async () => { + const storedCandidate = candidate(); + const missingKeyStore = new StatefulAuthorizationStore(storedCandidate); + await expect( + service(missingKeyStore, { + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY: undefined, + }).getAuthorization({ userId: 'member-1', orgId: organizationId }) + ).resolves.toEqual({ status: 'temporarily_unavailable' }); + + const missingPublicKeyStore = new StatefulAuthorizationStore(storedCandidate); + await expect( + service(missingPublicKeyStore, { + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY: undefined, + }).getAuthorization({ userId: 'member-1', orgId: organizationId }) + ).resolves.toEqual({ status: 'temporarily_unavailable' }); + + const failedKeyStore = new StatefulAuthorizationStore(storedCandidate); + await expect( + service(failedKeyStore, { + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY: { + get: async () => { + throw new Error('secret store unavailable'); + }, + }, + }).getAuthorization({ userId: 'member-1', orgId: organizationId }) + ).resolves.toEqual({ status: 'temporarily_unavailable' }); + + expect(missingKeyStore.invalidations).toEqual([]); + expect(missingPublicKeyStore.invalidations).toEqual([]); + expect(failedKeyStore.invalidations).toEqual([]); + }); + + it.each([ + ['mismatched', Buffer.from(mismatchedPublicKeyPem).toString('base64')], + ['malformed', Buffer.from('not-a-public-key').toString('base64')], + ])('treats a %s configured RSA public key as temporary without invalidation', async (_, key) => { + const store = new StatefulAuthorizationStore(candidate()); + + await expect( + service(store, { + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY: key, + }).getAuthorization({ userId: 'member-1', orgId: organizationId }) + ).resolves.toEqual({ status: 'temporarily_unavailable' }); + expect(store.used).toEqual([]); + expect(store.invalidations).toEqual([]); + }); + + it('treats an unavailable envelope key generation as temporary', async () => { + const retiredEnvelope = encryptKeyedEnvelope( + 'ATCT-runtime-token', + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + { keyId: 'retired', publicKeyPem }, + buildBitbucketWorkspaceAccessTokenAad({ + organizationId, + integrationId, + credentialId, + credentialVersion: 3, + }) + ); + const store = new StatefulAuthorizationStore(candidate({ tokenEncrypted: retiredEnvelope })); + + await expect( + service(store).getAuthorization({ userId: 'member-1', orgId: organizationId }) + ).resolves.toEqual({ status: 'temporarily_unavailable' }); + expect(store.invalidations).toEqual([]); + }); + + it.each([ + ['malformed envelope', 'not-json'], + [ + 'authenticated decryption failure', + encryptKeyedEnvelope( + 'ATCT-runtime-token', + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + { keyId: 'active', publicKeyPem }, + buildBitbucketWorkspaceAccessTokenAad({ + organizationId, + integrationId, + credentialId, + credentialVersion: 2, + }) + ), + ], + ])('generation-fences encryption_unreadable for %s', async (_case, tokenEncrypted) => { + const unreadableCandidate = candidate({ tokenEncrypted }); + const store = new StatefulAuthorizationStore(unreadableCandidate); + + await expect( + service(store).getAuthorization({ userId: 'member-1', orgId: organizationId }) + ).resolves.toEqual({ status: 'reconnect_required' }); + expect(store.used).toEqual([]); + expect(store.invalidations).toEqual([ + { + fence: fence(unreadableCandidate), + reason: 'encryption_unreadable', + at: now.toISOString(), + }, + ]); + }); + + it.each([ + { providerCredentialType: 'oauth2' }, + { providerScopes: ['account', 'repository'] }, + { providerScopes: ['account', 'repository', 'repository:write'] }, + { providerScopes: ['repository:write', 'account'] }, + { providerVerifiedAt: 'not-a-timestamp' }, + { authInvalidAt: '2026-06-24T09:30:00.000Z' }, + { integrationStatus: 'suspended' }, + { ownedByUserId: 'user-1' }, + ])('fails closed for an unverified or inactive candidate %#', async overrides => { + const store = new StatefulAuthorizationStore(candidate(overrides)); + + await expect( + service(store).getAuthorization({ userId: 'member-1', orgId: organizationId }) + ).resolves.toEqual({ status: 'reconnect_required' }); + expect(store.used).toEqual([]); + expect(store.invalidations).toEqual([]); + }); + + it('allows an in-flight resolution to finish when a fenced last-use update loses rotation', async () => { + const original = candidate(); + const store = new StatefulAuthorizationStore(original); + store.markUsed = async authorizationFence => { + store.current = candidate({ credentialVersion: original.credentialVersion + 1 }); + return JSON.stringify(authorizationFence) === JSON.stringify(fence(store.current)); + }; + + await expect( + service(store).getAuthorization({ userId: 'member-1', orgId: organizationId }) + ).resolves.toEqual({ + status: 'available', + token: 'ATCT-runtime-token', + organizationId, + integrationId, + credentialId, + credentialVersion: original.credentialVersion, + workspace: { uuid: workspaceUuid, slug: 'acme' }, + }); + }); + + it('does not let a stale generation invalidate a rotated credential', async () => { + const original = candidate(); + const store = new StatefulAuthorizationStore(original); + const authorization = await service(store).getAuthorization({ + userId: 'member-1', + orgId: organizationId, + }); + if (authorization.status !== 'available') throw new Error('Expected authorization'); + + store.current = candidate({ credentialVersion: original.credentialVersion + 1 }); + await service(store).invalidateAuthorization(authorization, 'provider_rejected'); + + expect(store.invalidations).toEqual([]); + }); +}); diff --git a/services/git-token-service/src/bitbucket-workspace-access-token-authorization-service.ts b/services/git-token-service/src/bitbucket-workspace-access-token-authorization-service.ts new file mode 100644 index 0000000000..413c2d1b9a --- /dev/null +++ b/services/git-token-service/src/bitbucket-workspace-access-token-authorization-service.ts @@ -0,0 +1,561 @@ +import { createPrivateKey, createPublicKey } from 'node:crypto'; +import { getWorkerDb, type WorkerDb } from '@kilocode/db/client'; +import { + kilocode_users, + organization_memberships, + platform_access_token_credentials, + platform_integrations, +} from '@kilocode/db/schema'; +import { decryptKeyedEnvelope, parseKeyedEnvelope } from '@kilocode/encryption'; +import { + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_PROVIDER_CREDENTIAL_TYPE, + buildBitbucketOrganizationCredentialLockKey, + buildBitbucketWorkspaceAccessTokenAad, + hasBitbucketAccessTokenFamilyPrefix, + hasRequiredBitbucketWorkspaceAccessTokenScopes, + normalizeBitbucketWorkspaceAccessTokenScopes, + type BitbucketWorkspaceAccessTokenInvalidationReason, +} from '@kilocode/worker-utils/bitbucket-workspace-access-token'; +import { and, eq, exists, isNull, lt, or, sql } from 'drizzle-orm'; +import { normalizeBitbucketUuid } from './bitbucket-url.js'; + +export type BitbucketWorkspaceAccessTokenAuthorizationCandidate = { + integrationId: string; + credentialId: string; + organizationId: string; + ownedByUserId: string | null; + platform: string; + integrationType: string; + integrationStatus: string | null; + installationId: string | null; + accountId: string | null; + accountLogin: string | null; + authInvalidAt: string | null; + credentialPlatform: string; + credentialIntegrationType: string; + tokenEncrypted: string; + providerCredentialType: string; + providerScopes: string[]; + providerVerifiedAt: string; + credentialVersion: number; + lastValidatedAt: string; +}; + +export type BitbucketWorkspaceAccessTokenAuthorizationFence = { + organizationId: string; + integrationId: string; + credentialId: string; + credentialVersion: number; +}; + +export type BitbucketWorkspaceAccessTokenAuthorizationStore = { + findAuthorization(input: { + userId: string; + organizationId: string; + }): Promise; + markUsed(fence: BitbucketWorkspaceAccessTokenAuthorizationFence, at: string): Promise; + invalidate( + fence: BitbucketWorkspaceAccessTokenAuthorizationFence, + reason: BitbucketWorkspaceAccessTokenInvalidationReason, + at: string + ): Promise; +}; + +export type BitbucketWorkspaceAccessTokenAuthorization = { + status: 'available'; + token: string; + organizationId: string; + integrationId: string; + credentialId: string; + credentialVersion: number; + workspace: { uuid: string; slug: string }; +}; + +export type BitbucketWorkspaceAccessTokenAuthorizationResult = + | BitbucketWorkspaceAccessTokenAuthorization + | { status: 'invalid_request' } + | { status: 'not_connected' } + | { status: 'reconnect_required' } + | { status: 'temporarily_unavailable' }; + +type Secret = SecretsStoreSecret | string | undefined; +type AuthorizationEnv = Pick & { + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID?: Secret; + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY?: Secret; + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY?: Secret; +}; +type AuthorizationDependencies = { + store?: BitbucketWorkspaceAccessTokenAuthorizationStore; + now?: () => Date; +}; +type WorkerTransaction = Parameters[0]>[0]; +type AuthorizationDb = WorkerDb | WorkerTransaction; + +function normalizeOrganizationId(value: string): string | null { + if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) { + return null; + } + return value.toLowerCase(); +} + +function isCanonicalWorkspaceSlug(value: string): boolean { + return value.length <= 255 && /^[A-Za-z0-9_.-]+$/.test(value) && value !== '.' && value !== '..'; +} + +function isValidTimestamp(value: string): boolean { + return Number.isFinite(new Date(value).getTime()); +} + +function hasVisibleAsciiOnly(value: string): boolean { + for (let index = 0; index < value.length; index += 1) { + const code = value.charCodeAt(index); + if (code < 0x21 || code > 0x7e) return false; + } + return true; +} + +function authorizationFence( + candidate: BitbucketWorkspaceAccessTokenAuthorizationCandidate +): BitbucketWorkspaceAccessTokenAuthorizationFence { + return { + organizationId: candidate.organizationId, + integrationId: candidate.integrationId, + credentialId: candidate.credentialId, + credentialVersion: candidate.credentialVersion, + }; +} + +function hasVerifiedCredentialProfile( + candidate: BitbucketWorkspaceAccessTokenAuthorizationCandidate +): boolean { + const normalizedScopes = normalizeBitbucketWorkspaceAccessTokenScopes( + candidate.providerScopes.join(' ') + ); + return ( + candidate.credentialPlatform === BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM && + candidate.credentialIntegrationType === BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE && + candidate.providerCredentialType === + BITBUCKET_WORKSPACE_ACCESS_TOKEN_PROVIDER_CREDENTIAL_TYPE && + isValidTimestamp(candidate.providerVerifiedAt) && + isValidTimestamp(candidate.lastValidatedAt) && + normalizedScopes.length === candidate.providerScopes.length && + normalizedScopes.every((scope, index) => scope === candidate.providerScopes[index]) && + hasRequiredBitbucketWorkspaceAccessTokenScopes(normalizedScopes) + ); +} + +function hasActiveParent( + candidate: BitbucketWorkspaceAccessTokenAuthorizationCandidate, + organizationId: string +): boolean { + return ( + candidate.organizationId === organizationId && + candidate.ownedByUserId === null && + candidate.platform === BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM && + candidate.integrationType === BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE && + candidate.integrationStatus === 'active' && + candidate.installationId === null && + candidate.authInvalidAt === null + ); +} + +export function buildBitbucketWorkspaceAccessTokenAuthorizationQuery( + db: WorkerDb, + input: { userId: string; organizationId: string } +) { + const currentOrganizationMembership = exists( + db + .select({ id: organization_memberships.id }) + .from(organization_memberships) + .where( + and( + eq(organization_memberships.organization_id, input.organizationId), + eq(organization_memberships.kilo_user_id, input.userId) + ) + ) + ); + + return db + .select({ + integrationId: platform_integrations.id, + credentialId: platform_access_token_credentials.id, + organizationId: platform_integrations.owned_by_organization_id, + ownedByUserId: platform_integrations.owned_by_user_id, + platform: platform_integrations.platform, + integrationType: platform_integrations.integration_type, + integrationStatus: platform_integrations.integration_status, + installationId: platform_integrations.platform_installation_id, + accountId: platform_integrations.platform_account_id, + accountLogin: platform_integrations.platform_account_login, + authInvalidAt: platform_integrations.auth_invalid_at, + credentialPlatform: platform_access_token_credentials.platform, + credentialIntegrationType: platform_access_token_credentials.integration_type, + tokenEncrypted: platform_access_token_credentials.token_encrypted, + providerCredentialType: platform_access_token_credentials.provider_credential_type, + providerScopes: platform_access_token_credentials.provider_scopes, + providerVerifiedAt: platform_access_token_credentials.provider_verified_at, + credentialVersion: platform_access_token_credentials.credential_version, + lastValidatedAt: platform_access_token_credentials.last_validated_at, + }) + .from(platform_integrations) + .innerJoin( + platform_access_token_credentials, + and( + eq(platform_access_token_credentials.platform_integration_id, platform_integrations.id), + eq(platform_access_token_credentials.platform, platform_integrations.platform), + eq( + platform_access_token_credentials.integration_type, + platform_integrations.integration_type + ), + eq( + platform_access_token_credentials.owned_by_organization_id, + platform_integrations.owned_by_organization_id + ) + ) + ) + .innerJoin( + kilocode_users, + and(eq(kilocode_users.id, input.userId), isNull(kilocode_users.blocked_reason)) + ) + .where( + and( + eq(platform_integrations.owned_by_organization_id, input.organizationId), + isNull(platform_integrations.owned_by_user_id), + eq(platform_integrations.platform, BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM), + eq( + platform_integrations.integration_type, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE + ), + or(currentOrganizationMembership, eq(kilocode_users.is_admin, true)) + ) + ) + .limit(1); +} + +export function withBitbucketWorkspaceAccessTokenOrganizationLock( + db: Pick, + organizationId: string, + operation: (tx: WorkerTransaction) => Promise +): Promise { + return db.transaction(async tx => { + await tx.execute( + sql`SELECT pg_advisory_xact_lock(hashtextextended(${buildBitbucketOrganizationCredentialLockKey(organizationId)}, 0))` + ); + return operation(tx); + }); +} + +export function buildBitbucketWorkspaceAccessTokenCredentialGenerationQuery( + db: AuthorizationDb, + fence: BitbucketWorkspaceAccessTokenAuthorizationFence +) { + return db + .select({ id: platform_access_token_credentials.id }) + .from(platform_access_token_credentials) + .where( + and( + eq(platform_access_token_credentials.id, fence.credentialId), + eq(platform_access_token_credentials.owned_by_organization_id, fence.organizationId), + eq(platform_access_token_credentials.platform_integration_id, fence.integrationId), + eq(platform_access_token_credentials.platform, BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM), + eq( + platform_access_token_credentials.integration_type, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE + ), + eq(platform_access_token_credentials.credential_version, fence.credentialVersion) + ) + ) + .limit(1); +} + +export function buildBitbucketWorkspaceAccessTokenMarkUsedQuery( + db: AuthorizationDb, + fence: BitbucketWorkspaceAccessTokenAuthorizationFence, + at: string +) { + return db + .update(platform_access_token_credentials) + .set({ last_used_at: at }) + .where( + and( + eq(platform_access_token_credentials.id, fence.credentialId), + eq(platform_access_token_credentials.owned_by_organization_id, fence.organizationId), + eq(platform_access_token_credentials.platform_integration_id, fence.integrationId), + eq(platform_access_token_credentials.credential_version, fence.credentialVersion), + or( + isNull(platform_access_token_credentials.last_used_at), + lt(platform_access_token_credentials.last_used_at, at) + ) + ) + ) + .returning({ id: platform_access_token_credentials.id }); +} + +export function buildBitbucketWorkspaceAccessTokenInvalidationQuery( + db: AuthorizationDb, + fence: BitbucketWorkspaceAccessTokenAuthorizationFence, + reason: BitbucketWorkspaceAccessTokenInvalidationReason, + at: string +) { + return db + .update(platform_integrations) + .set({ auth_invalid_at: at, auth_invalid_reason: reason }) + .where( + and( + eq(platform_integrations.id, fence.integrationId), + eq(platform_integrations.owned_by_organization_id, fence.organizationId), + isNull(platform_integrations.owned_by_user_id), + eq(platform_integrations.platform, BITBUCKET_WORKSPACE_ACCESS_TOKEN_PLATFORM), + eq( + platform_integrations.integration_type, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_INTEGRATION_TYPE + ), + isNull(platform_integrations.auth_invalid_at) + ) + ) + .returning({ id: platform_integrations.id }); +} + +class DrizzleBitbucketWorkspaceAccessTokenAuthorizationStore implements BitbucketWorkspaceAccessTokenAuthorizationStore { + constructor(private db: WorkerDb) {} + + async findAuthorization(input: { + userId: string; + organizationId: string; + }): Promise { + const [candidate] = await buildBitbucketWorkspaceAccessTokenAuthorizationQuery(this.db, input); + if (!candidate?.organizationId) return null; + return { ...candidate, organizationId: candidate.organizationId }; + } + + async markUsed( + fence: BitbucketWorkspaceAccessTokenAuthorizationFence, + at: string + ): Promise { + const updated = await buildBitbucketWorkspaceAccessTokenMarkUsedQuery(this.db, fence, at); + return updated.length === 1; + } + + async invalidate( + fence: BitbucketWorkspaceAccessTokenAuthorizationFence, + reason: BitbucketWorkspaceAccessTokenInvalidationReason, + at: string + ): Promise { + return withBitbucketWorkspaceAccessTokenOrganizationLock( + this.db, + fence.organizationId, + async tx => { + const [currentGeneration] = + await buildBitbucketWorkspaceAccessTokenCredentialGenerationQuery(tx, fence); + if (!currentGeneration) return false; + + const updated = await buildBitbucketWorkspaceAccessTokenInvalidationQuery( + tx, + fence, + reason, + at + ); + return updated.length === 1; + } + ); + } +} + +async function resolveSecret(secret: Secret): Promise { + if (!secret) return null; + const value = typeof secret === 'string' ? secret : await secret.get(); + return value || null; +} + +export class BitbucketWorkspaceAccessTokenAuthorizationService { + constructor( + private env: AuthorizationEnv, + private dependencies: AuthorizationDependencies = {} + ) {} + + async getAuthorization(input: { + userId: string; + orgId?: string; + }): Promise { + const organizationId = input.orgId ? normalizeOrganizationId(input.orgId) : null; + if (!organizationId) return { status: 'invalid_request' }; + const store = this.getStore(); + if (!store) return { status: 'temporarily_unavailable' }; + + let candidate: BitbucketWorkspaceAccessTokenAuthorizationCandidate | null; + try { + candidate = await store.findAuthorization({ + userId: input.userId, + organizationId, + }); + } catch { + return { status: 'temporarily_unavailable' }; + } + if (!candidate) return { status: 'not_connected' }; + if (!hasActiveParent(candidate, organizationId) || !hasVerifiedCredentialProfile(candidate)) { + return { status: 'reconnect_required' }; + } + + const workspaceUuid = candidate.accountId ? normalizeBitbucketUuid(candidate.accountId) : null; + if ( + !workspaceUuid || + workspaceUuid !== candidate.accountId || + !candidate.accountLogin || + !isCanonicalWorkspaceSlug(candidate.accountLogin) || + !Number.isInteger(candidate.credentialVersion) || + candidate.credentialVersion <= 0 || + candidate.tokenEncrypted === '' + ) { + return { status: 'reconnect_required' }; + } + + const currentTime = (this.dependencies.now ?? (() => new Date()))(); + const decrypted = await this.decrypt(candidate); + if (decrypted.status === 'temporarily_unavailable') return decrypted; + if (decrypted.status === 'unreadable') { + await this.invalidateWithStore( + store, + candidate, + 'encryption_unreadable', + currentTime.toISOString() + ); + return { status: 'reconnect_required' }; + } + + try { + await store.markUsed(authorizationFence(candidate), currentTime.toISOString()); + } catch { + return { status: 'temporarily_unavailable' }; + } + return { + status: 'available', + token: decrypted.token, + organizationId: candidate.organizationId, + integrationId: candidate.integrationId, + credentialId: candidate.credentialId, + credentialVersion: candidate.credentialVersion, + workspace: { uuid: workspaceUuid, slug: candidate.accountLogin }, + }; + } + + async invalidateAuthorization( + authorization: BitbucketWorkspaceAccessTokenAuthorization, + reason: BitbucketWorkspaceAccessTokenInvalidationReason + ): Promise { + const store = this.getStore(); + if (!store) return; + const at = (this.dependencies.now ?? (() => new Date()))().toISOString(); + try { + await store.invalidate( + { + organizationId: authorization.organizationId, + integrationId: authorization.integrationId, + credentialId: authorization.credentialId, + credentialVersion: authorization.credentialVersion, + }, + reason, + at + ); + } catch { + return; + } + } + + private getStore(): BitbucketWorkspaceAccessTokenAuthorizationStore | null { + if (this.dependencies.store) return this.dependencies.store; + if (!this.env.HYPERDRIVE) return null; + const db = getWorkerDb(this.env.HYPERDRIVE.connectionString, { statement_timeout: 10_000 }); + return new DrizzleBitbucketWorkspaceAccessTokenAuthorizationStore(db); + } + + private async decrypt( + candidate: BitbucketWorkspaceAccessTokenAuthorizationCandidate + ): Promise< + | { status: 'available'; token: string } + | { status: 'temporarily_unavailable' } + | { status: 'unreadable' } + > { + let keyId: string | null; + let encodedPublicKey: string | null; + let encodedPrivateKey: string | null; + try { + [keyId, encodedPublicKey, encodedPrivateKey] = await Promise.all([ + resolveSecret(this.env.BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID), + resolveSecret(this.env.BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY), + resolveSecret(this.env.BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY), + ]); + } catch { + return { status: 'temporarily_unavailable' }; + } + if (!keyId || !encodedPublicKey || !encodedPrivateKey) { + return { status: 'temporarily_unavailable' }; + } + + let privateKeyPem: string; + try { + const publicKeyPem = Buffer.from(encodedPublicKey, 'base64').toString('utf8'); + privateKeyPem = Buffer.from(encodedPrivateKey, 'base64').toString('utf8'); + if (publicKeyPem.includes('PRIVATE KEY')) return { status: 'temporarily_unavailable' }; + const publicKey = createPublicKey(publicKeyPem); + const privateKey = createPrivateKey(privateKeyPem); + if (publicKey.asymmetricKeyType !== 'rsa' || privateKey.asymmetricKeyType !== 'rsa') { + return { status: 'temporarily_unavailable' }; + } + const configuredPublicKey = publicKey.export({ type: 'spki', format: 'der' }); + const derivedPublicKey = createPublicKey(privateKey).export({ type: 'spki', format: 'der' }); + if (!configuredPublicKey.equals(derivedPublicKey)) { + return { status: 'temporarily_unavailable' }; + } + } catch { + return { status: 'temporarily_unavailable' }; + } + + let envelope; + try { + envelope = parseKeyedEnvelope( + candidate.tokenEncrypted, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME + ); + } catch { + return { status: 'unreadable' }; + } + if (envelope.keyId !== keyId) return { status: 'temporarily_unavailable' }; + + let token: string; + try { + token = decryptKeyedEnvelope( + candidate.tokenEncrypted, + BITBUCKET_WORKSPACE_ACCESS_TOKEN_ENVELOPE_SCHEME, + { active: { keyId, privateKeyPem } }, + buildBitbucketWorkspaceAccessTokenAad({ + organizationId: candidate.organizationId, + integrationId: candidate.integrationId, + credentialId: candidate.credentialId, + credentialVersion: candidate.credentialVersion, + }) + ); + } catch { + return { status: 'unreadable' }; + } + if (!hasBitbucketAccessTokenFamilyPrefix(token) || !hasVisibleAsciiOnly(token)) { + return { status: 'unreadable' }; + } + return { status: 'available', token }; + } + + private async invalidateWithStore( + store: BitbucketWorkspaceAccessTokenAuthorizationStore, + candidate: BitbucketWorkspaceAccessTokenAuthorizationCandidate, + reason: BitbucketWorkspaceAccessTokenInvalidationReason, + at: string + ): Promise { + try { + await store.invalidate(authorizationFence(candidate), reason, at); + } catch { + return; + } + } +} diff --git a/services/git-token-service/src/github-user-authorization-entrypoint.test.ts b/services/git-token-service/src/github-user-authorization-entrypoint.test.ts index 703d89c4e0..16e446ee43 100644 --- a/services/git-token-service/src/github-user-authorization-entrypoint.test.ts +++ b/services/git-token-service/src/github-user-authorization-entrypoint.test.ts @@ -1,4 +1,4 @@ -import { signKiloToken } from '@kilocode/worker-utils'; +import { BITBUCKET_REPOSITORY_LIST_AUDIENCE, signKiloToken } from '@kilocode/worker-utils'; import { beforeEach, describe, expect, it, vi } from 'vitest'; const serviceMocks = vi.hoisted(() => ({ @@ -156,12 +156,13 @@ describe('fetch disconnect endpoint', () => { const env = { NEXTAUTH_SECRET: { get: async () => jwtSecret } as SecretsStoreSecret, } as CloudflareEnv; - const authorizationHeader = async (userId: string): Promise => { + const authorizationHeader = async (userId: string, audience?: string): Promise => { const { token } = await signKiloToken({ userId, pepper: null, secret: jwtSecret, expiresInSeconds: 60 * 60, + ...(audience ? { audience } : {}), }); return `Bearer ${token}`; }; @@ -187,6 +188,24 @@ describe('fetch disconnect endpoint', () => { } ); + it('rejects a token issued for the Bitbucket repository-list endpoint', async () => { + const response = await handler.fetch( + new Request( + 'https://git-token-service.kilosessions.ai/internal/github-user-authorizations/disconnect', + { + method: 'POST', + headers: { + Authorization: await authorizationHeader('user_1', BITBUCKET_REPOSITORY_LIST_AUDIENCE), + }, + } + ), + env + ); + + expect(response.status).toBe(401); + expect(serviceMocks.disconnectUserAuthorization).not.toHaveBeenCalled(); + }); + it('returns a sanitized availability error when JWT secret resolution fails', async () => { const unavailableEnv = { NEXTAUTH_SECRET: { get: async () => Promise.reject(new Error('secret store unavailable')) }, diff --git a/services/git-token-service/src/index.test.ts b/services/git-token-service/src/index.test.ts index 172ded024d..140ff9ebb9 100644 --- a/services/git-token-service/src/index.test.ts +++ b/services/git-token-service/src/index.test.ts @@ -1,3 +1,4 @@ +import { signKiloToken } from '@kilocode/worker-utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type * as GitLabLookupServiceModule from './gitlab-lookup-service.js'; @@ -13,6 +14,8 @@ const serviceMocks = vi.hoisted(() => ({ findGitLabIntegration: vi.fn(), findAuthorizedGitLabIntegrations: vi.fn(), getGitLabToken: vi.fn(), + listBitbucketRepositories: vi.fn(), + resolveBitbucketToken: vi.fn(), })); vi.mock('cloudflare:workers', () => ({ @@ -65,9 +68,95 @@ vi.mock('./gitlab-token-service.js', () => ({ }, })); +vi.mock('./bitbucket-runtime-token-resolver.js', () => ({ + listBitbucketRepositories: serviceMocks.listBitbucketRepositories, + resolveBitbucketToken: serviceMocks.resolveBitbucketToken, +})); + import type { AuthorizedGitLabIntegration } from './gitlab-lookup-service.js'; import { resolveGitLabRuntimeToken } from './gitlab-runtime-token-resolver.js'; -import { GitTokenRPCEntrypoint } from './index.js'; +import gitTokenServiceWorker, { GitTokenRPCEntrypoint } from './index.js'; + +describe('Bitbucket repository-list HTTP authorization', () => { + const jwtSecret = 'test-secret-that-is-at-least-32-characters'; + const env = { NEXTAUTH_SECRET: jwtSecret } as CloudflareEnv; + + beforeEach(() => { + serviceMocks.listBitbucketRepositories.mockReset().mockResolvedValue({ + status: 'available', + repositories: [], + }); + }); + + it('derives the owner from a purpose-bound token instead of request input', async () => { + const { token } = await signKiloToken({ + userId: 'member-1', + pepper: null, + secret: jwtSecret, + expiresInSeconds: 5 * 60, + audience: 'git-token-service:bitbucket-repositories', + extra: { organizationId: '123e4567-e89b-12d3-a456-426614174030' }, + }); + const response = await gitTokenServiceWorker.fetch( + new Request('https://git-token-service.test/internal/bitbucket/repositories', { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ orgId: '123e4567-e89b-12d3-a456-426614174099' }), + }), + env + ); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual({ status: 'available', repositories: [] }); + expect(serviceMocks.listBitbucketRepositories).toHaveBeenCalledWith(expect.anything(), { + userId: 'member-1', + orgId: '123e4567-e89b-12d3-a456-426614174030', + }); + }); + + it('requires an organization claim before repository lookup', async () => { + const { token } = await signKiloToken({ + userId: 'member-1', + pepper: null, + secret: jwtSecret, + expiresInSeconds: 5 * 60, + audience: 'git-token-service:bitbucket-repositories', + }); + const response = await gitTokenServiceWorker.fetch( + new Request('https://git-token-service.test/internal/bitbucket/repositories', { + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + }), + env + ); + + expect(response.status).toBe(403); + await expect(response.json()).resolves.toEqual({ error: 'organization_required' }); + expect(serviceMocks.listBitbucketRepositories).not.toHaveBeenCalled(); + }); + + it('rejects a generic Kilo token without the repository-list audience', async () => { + const { token } = await signKiloToken({ + userId: 'member-1', + pepper: null, + secret: jwtSecret, + expiresInSeconds: 5 * 60, + }); + const response = await gitTokenServiceWorker.fetch( + new Request('https://git-token-service.test/internal/bitbucket/repositories', { + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + }), + env + ); + + expect(response.status).toBe(401); + expect(serviceMocks.listBitbucketRepositories).not.toHaveBeenCalled(); + }); +}); const integration: AuthorizedGitLabIntegration = { integrationId: '123e4567-e89b-12d3-a456-426614174011', @@ -110,6 +199,45 @@ function createService(): GitTokenRPCEntrypoint { ); } +describe('GitTokenRPCEntrypoint Bitbucket runtime authorization', () => { + it('requires an organization before invoking the reachable V1 resolver', async () => { + serviceMocks.resolveBitbucketToken.mockReset(); + + await expect( + createService().getBitbucketToken({ + userId: 'user-1', + workspaceUuid: '123e4567-e89b-12d3-a456-426614174020', + repositoryUuid: '123e4567-e89b-12d3-a456-426614174021', + repositoryUrl: 'https://bitbucket.org/acme/widgets.git', + }) + ).resolves.toEqual({ success: false, reason: 'invalid_request' }); + expect(serviceMocks.resolveBitbucketToken).not.toHaveBeenCalled(); + }); + + it('returns only the opaque token from a successful V1 resolution', async () => { + serviceMocks.resolveBitbucketToken.mockReset().mockResolvedValue({ + success: true, + token: 'ATCT-runtime-token', + integrationId: 'must-not-cross-rpc', + credentialId: 'must-not-cross-rpc', + credentialVersion: 7, + }); + const params = { + userId: 'user-1', + orgId: '123e4567-e89b-12d3-a456-426614174030', + workspaceUuid: '123e4567-e89b-12d3-a456-426614174020', + repositoryUuid: '123e4567-e89b-12d3-a456-426614174021', + repositoryUrl: 'https://bitbucket.org/acme/widgets.git', + }; + + await expect(createService().getBitbucketToken(params)).resolves.toEqual({ + success: true, + token: 'ATCT-runtime-token', + }); + expect(serviceMocks.resolveBitbucketToken).toHaveBeenCalledWith(expect.anything(), params); + }); +}); + afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); diff --git a/services/git-token-service/src/index.ts b/services/git-token-service/src/index.ts index 84f0f00ab8..9314eb04f9 100644 --- a/services/git-token-service/src/index.ts +++ b/services/git-token-service/src/index.ts @@ -1,5 +1,9 @@ import { timingSafeEqual } from '@kilocode/encryption'; -import { extractBearerToken, verifyKiloToken } from '@kilocode/worker-utils'; +import { + BITBUCKET_REPOSITORY_LIST_AUDIENCE, + extractBearerToken, + verifyKiloToken, +} from '@kilocode/worker-utils'; import { WorkerEntrypoint } from 'cloudflare:workers'; import { GitHubTokenService, type GitHubAppType } from './github-token-service.js'; import { GitLabLookupService, type GitLabLookupSuccess } from './gitlab-lookup-service.js'; @@ -39,6 +43,13 @@ import { type GitAuthorConfig, type ManagedGitHubFallbackReason as UserAuthorizationFallbackReason, } from './github-user-authorization-service.js'; +import { + listBitbucketRepositories, + resolveBitbucketToken, + type BitbucketRepositoryListResult, + type GetBitbucketTokenParams, + type GetBitbucketTokenResult, +} from './bitbucket-runtime-token-resolver.js'; export type GetTokenForRepoParams = { githubRepo: string; @@ -71,6 +82,11 @@ export type { GetGitLabTokenFailure, GetGitLabTokenResult, } from './gitlab-runtime-token-resolver.js'; +export type { + BitbucketRepositoryListResult, + GetBitbucketTokenParams, + GetBitbucketTokenResult, +} from './bitbucket-runtime-token-resolver.js'; export type ManagedGitHubFallbackReason = UserAuthorizationFallbackReason | 'lite_installation'; @@ -176,8 +192,9 @@ export type RedeemGitLabSessionCapabilityResult = | { success: false; reason: RedeemGitLabSessionCapabilityFailureReason }; const DISCONNECT_PATH = '/internal/github-user-authorizations/disconnect'; +const BITBUCKET_REPOSITORIES_PATH = '/internal/bitbucket/repositories'; -type DisconnectEnv = CloudflareEnv & { +type ServiceHttpEnv = CloudflareEnv & { NEXTAUTH_SECRET: SecretsStoreSecret | string; }; @@ -714,6 +731,12 @@ export class GitTokenRPCEntrypoint extends WorkerEntrypoint { }); } + async getBitbucketToken(params: GetBitbucketTokenParams): Promise { + if (!params.orgId) return { success: false, reason: 'invalid_request' }; + const result = await resolveBitbucketToken(this.env, params); + return result.success ? { success: true, token: result.token } : result; + } + async issueGitLabSessionCapability( params: IssueGitLabSessionCapabilityParams ): Promise { @@ -867,9 +890,11 @@ export class GitTokenRPCEntrypoint extends WorkerEntrypoint { } export default { - async fetch(request: Request, env: DisconnectEnv): Promise { + async fetch(request: Request, env: ServiceHttpEnv): Promise { const url = new URL(request.url); - if (url.pathname !== DISCONNECT_PATH) return new Response(null, { status: 404 }); + if (url.pathname !== DISCONNECT_PATH && url.pathname !== BITBUCKET_REPOSITORIES_PATH) { + return new Response(null, { status: 404 }); + } if (request.method !== 'POST') return new Response(null, { status: 405 }); const token = extractBearerToken(request.headers.get('Authorization')); @@ -883,20 +908,40 @@ export default { } if (!secret) return Response.json({ error: 'authentication_unavailable' }, { status: 503 }); - let kiloUserId: string; + let authorization: Awaited>; try { - const authorization = await verifyKiloToken(token, secret); - kiloUserId = authorization.kiloUserId; + authorization = await verifyKiloToken( + token, + secret, + url.pathname === BITBUCKET_REPOSITORIES_PATH + ? { audience: BITBUCKET_REPOSITORY_LIST_AUDIENCE } + : undefined + ); } catch { return Response.json({ error: 'unauthorized' }, { status: 401 }); } + if (url.pathname === BITBUCKET_REPOSITORIES_PATH) { + if (!authorization.organizationId) { + return Response.json({ error: 'organization_required' }, { status: 403 }); + } + try { + const result: BitbucketRepositoryListResult = await listBitbucketRepositories(env, { + userId: authorization.kiloUserId, + orgId: authorization.organizationId, + }); + return Response.json(result); + } catch { + return Response.json({ status: 'temporarily_unavailable' }); + } + } + try { const service = new GitHubUserAuthorizationService(env); - await service.disconnectUserAuthorization(kiloUserId); + await service.disconnectUserAuthorization(authorization.kiloUserId); return Response.json({ disconnected: true }); } catch { return Response.json({ error: 'disconnect_failed' }, { status: 502 }); } }, -} satisfies ExportedHandler; +} satisfies ExportedHandler; diff --git a/services/git-token-service/worker-configuration.d.ts b/services/git-token-service/worker-configuration.d.ts index a7e4a86425..dd1ea85552 100644 --- a/services/git-token-service/worker-configuration.d.ts +++ b/services/git-token-service/worker-configuration.d.ts @@ -1,6 +1,29 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types --env-interface CloudflareEnv worker-configuration.d.ts` (hash: 845f65551893477d27bd8781d3b8d187) -// Runtime types generated with workerd@1.20260508.1 2025-09-15 nodejs_compat +// Generated by Wrangler by running `wrangler types --env-interface CloudflareEnv worker-configuration.d.ts` (hash: 396921e606d23839786ffebbb17645fd) +// Runtime types generated with workerd@1.20260603.1 2025-09-15 nodejs_compat +interface __BaseEnv_CloudflareEnv { + TOKEN_CACHE: KVNamespace; + HYPERDRIVE: Hyperdrive; + NEXTAUTH_SECRET: SecretsStoreSecret | string; + SCM_SESSION_CAPABILITY_ENCRYPTION_KEY: SecretsStoreSecret; + BITBUCKET_CLIENT_ID: SecretsStoreSecret | string; + BITBUCKET_CLIENT_SECRET: SecretsStoreSecret | string; + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID: SecretsStoreSecret | string; + GITHUB_APP_SLUG: "kiloconnect-development" | "kiloconnect"; + GITHUB_APP_BOT_USER_ID: "242397087" | "240665456"; + GITHUB_LITE_APP_SLUG: "" | "kiloconnect-lite"; + GITHUB_LITE_APP_BOT_USER_ID: "" | "257753004"; + GITHUB_APP_ID: string; + GITHUB_APP_PRIVATE_KEY: string; + GITHUB_LITE_APP_ID: string; + GITHUB_LITE_APP_PRIVATE_KEY: string; + GITLAB_CLIENT_ID: string; + GITLAB_CLIENT_SECRET: string; + GITHUB_APP_CLIENT_ID: string; + GITHUB_APP_CLIENT_SECRET: string; + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY: string; + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY: string; +} declare namespace Cloudflare { interface GlobalProps { mainModule: typeof import("./src/index"); @@ -10,6 +33,9 @@ declare namespace Cloudflare { HYPERDRIVE: Hyperdrive; NEXTAUTH_SECRET: SecretsStoreSecret; SCM_SESSION_CAPABILITY_ENCRYPTION_KEY: SecretsStoreSecret; + BITBUCKET_CLIENT_ID: SecretsStoreSecret; + BITBUCKET_CLIENT_SECRET: SecretsStoreSecret; + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID: SecretsStoreSecret; GITHUB_APP_SLUG: "kiloconnect-development"; GITHUB_APP_BOT_USER_ID: "242397087"; GITHUB_LITE_APP_SLUG: ""; @@ -22,40 +48,21 @@ declare namespace Cloudflare { GITLAB_CLIENT_SECRET: string; GITHUB_APP_CLIENT_ID: string; GITHUB_APP_CLIENT_SECRET: string; - USER_GITHUB_APP_TOKEN_ACTIVE_KEY_ID: string; - USER_GITHUB_APP_TOKEN_ACTIVE_PUBLIC_KEY: string; - USER_GITHUB_APP_TOKEN_ACTIVE_PRIVATE_KEY: string; NEXTAUTH_SECRET: string; - SCM_SESSION_CAPABILITY_ENCRYPTION_KEY: string; - } - interface Env { - TOKEN_CACHE: KVNamespace; - HYPERDRIVE: Hyperdrive; - NEXTAUTH_SECRET: SecretsStoreSecret | string; - SCM_SESSION_CAPABILITY_ENCRYPTION_KEY: SecretsStoreSecret | string; - GITHUB_APP_SLUG: "kiloconnect-development" | "kiloconnect"; - GITHUB_APP_BOT_USER_ID: "242397087" | "240665456"; - GITHUB_LITE_APP_SLUG: "" | "kiloconnect-lite"; - GITHUB_LITE_APP_BOT_USER_ID: "" | "257753004"; - GITHUB_APP_ID: string; - GITHUB_APP_PRIVATE_KEY: string; - GITHUB_LITE_APP_ID: string; - GITHUB_LITE_APP_PRIVATE_KEY: string; - GITLAB_CLIENT_ID: string; - GITLAB_CLIENT_SECRET: string; - GITHUB_APP_CLIENT_ID: string; - GITHUB_APP_CLIENT_SECRET: string; - USER_GITHUB_APP_TOKEN_ACTIVE_KEY_ID: string; - USER_GITHUB_APP_TOKEN_ACTIVE_PUBLIC_KEY: string; - USER_GITHUB_APP_TOKEN_ACTIVE_PRIVATE_KEY: string; + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_KEY_ID: string; + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PUBLIC_KEY: string; + BITBUCKET_OAUTH_CREDENTIAL_ACTIVE_PRIVATE_KEY: string; + BITBUCKET_CLIENT_ID: string; + BITBUCKET_CLIENT_SECRET: string; } + interface Env extends __BaseEnv_CloudflareEnv {} } -interface CloudflareEnv extends Cloudflare.Env {} +interface CloudflareEnv extends __BaseEnv_CloudflareEnv {} type StringifyValues> = { [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; }; declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface ProcessEnv extends StringifyValues> {} } // Begin runtime types @@ -3528,6 +3535,229 @@ declare abstract class Span { get isTraced(): boolean; setAttribute(key: string, value?: (boolean | number | string)): void; } +// ============================================================================ +// Agent Memory +// +// Public type surface for user Workers binding to an Agent Memory namespace. +// ============================================================================ +/** Memory type — every memory is classified into exactly one. */ +type AgentMemoryMemoryType = "fact" | "event" | "instruction" | "task"; +/** Search intensity for recall. */ +type AgentMemoryThinkingLevel = "low" | "medium" | "high"; +/** Response verbosity for recall. */ +type AgentMemoryResponseLength = "short" | "medium" | "long"; +/** A conversation message passed to ingest(). */ +interface AgentMemoryMessage { + role: "system" | "user" | "assistant"; + content: string; + /** Optional message timestamp. */ + timestamp?: Date; +} +/** Raw memory content passed to remember(). */ +interface AgentMemoryIncomingMemory { + /** Raw memory content. The service classifies and summarizes automatically. */ + content: string; + /** Optional session identifier to associate with this memory. */ + sessionId?: string | null | undefined; +} +/** A stored memory returned from remember(), get(), and delete(). */ +interface AgentMemoryMemory { + /** Memory ID. */ + id: string; + /** Memory type. */ + type: AgentMemoryMemoryType; + /** Text summary. */ + summary: string; + /** Memory text. */ + content: string; + /** Session that created this memory. */ + sessionId: string | null; + /** Memory creation time. */ + createdAt: Date; + /** Memory last-update time. */ + updatedAt: Date; +} +/** Single entry in a list() response. Same shape as Memory minus full content. */ +type AgentMemoryMemoryListEntry = Omit; +/** A scored memory candidate in a recall result. */ +interface AgentMemoryScoredCandidate { + /** Candidate ID. */ + id: string; + /** Text summary. */ + summary: string; + /** Session that created this candidate, when known. */ + sessionId: string | null; + /** Relevance score (higher is better). Comparable only within a single query. */ + score: number; +} +/** Options for the ingest() method. */ +interface AgentMemoryIngestOptions { + /** Session identifier to associate with memories created during ingestion. */ + sessionId?: string | null | undefined; +} +/** Options for the getSummary() method. */ +interface AgentMemoryGetSummaryOptions { + /** Session identifier to retrieve session summary for. */ + sessionId?: string | null | undefined; +} +/** Response from the getSummary() method. */ +interface AgentMemoryGetSummaryResponse { + /** Markdown summary. */ + summary: string; +} +/** + * Options for the recall() method. + * + * `referenceDate` accepts a Date object, an ISO-8601 date string + * (YYYY-MM-DD), or a full ISO-8601 datetime string. When provided, this + * date is used as "today" for resolving relative time references + * ("how many days ago", "last week") instead of the server's wall-clock time. + */ +interface AgentMemoryRecallOptions { + /** Recall intensity: "low" (default), "medium", or "high". */ + thinkingLevel?: AgentMemoryThinkingLevel; + /** Response verbosity: "short", "medium" (default), or "long". */ + responseLength?: AgentMemoryResponseLength; + /** Temporal anchor for date arithmetic. */ + referenceDate?: Date | string; +} +/** Response from the recall() method. */ +interface AgentMemoryRecallResult { + /** Number of memories retrieved. */ + count: number; + /** LLM-generated answer synthesizing the matching memories. */ + answer: string; + /** Matching memories ranked by relevance. */ + candidates: AgentMemoryScoredCandidate[]; +} +/** + * Options for the list() method. + * + * `cursor` is the opaque continuation token returned by the previous page; + * pass it back unchanged to fetch the next page. `sessionId` and `type` + * are exact-match filters; combining them is allowed. + */ +interface AgentMemoryListMemoriesOptions { + /** Maximum number of memories to return. Default 20, max 500. */ + limit?: number; + /** Opaque cursor from a previous page. */ + cursor?: string; + /** Exact-match session filter. */ + sessionId?: string; + /** Exact-match memory-type filter. */ + type?: AgentMemoryMemoryType; +} +/** Response from the list() method. */ +interface AgentMemoryListMemoriesResult { + memories: AgentMemoryMemoryListEntry[]; + /** Continuation cursor; absent when this page exhausted the result set. */ + cursor?: string; +} +/** + * A single Agent Memory profile, scoped to a profile name. + * + * Returned by {@link AgentMemoryNamespace.getProfile}. + */ +declare abstract class AgentMemoryProfile { + /** + * Retrieve a memory by ID. + * + * @param memoryId - ULID of the memory to retrieve. + * @throws if the memory does not exist. + */ + get(memoryId: string): Promise; + /** + * Delete a memory by ID. + * + * Removes the memory and any source messages linked by the memory's + * source message IDs. + * + * @param memoryId - ULID of the memory to delete. + * @throws if the memory does not exist. + */ + delete(memoryId: string): Promise; + /** + * Store a memory in this profile. The content is automatically classified, + * summarized, and indexed. + * + * @param memory - Raw memory content to persist. + */ + remember(memory: AgentMemoryIncomingMemory): Promise; + /** + * Extract memories from a conversation. + * + * @param messages - Conversation messages to extract memories from. + * @param options - Optional ingest options. + */ + ingest(messages: Iterable, options?: AgentMemoryIngestOptions): Promise; + /** + * Get a profile summary. + * + * @param options - Optional getSummary options. + */ + getSummary(options?: AgentMemoryGetSummaryOptions): Promise; + /** + * Recall memories in this profile. + * + * @param query - Recall query matched against memory content and keywords. + * @param options - Optional recall parameters. + * @returns Matching memories with relevance scores and a synthesized answer. + */ + recall(query: string, options?: AgentMemoryRecallOptions): Promise; + /** + * List active memories in this profile. + * + * Returns a paginated, filterable view of stored memories. Superseded + * versions are excluded. Use the returned `cursor` (when present) to + * fetch the next page. + * + * @param options - Optional pagination and filter options. + */ + list(options?: AgentMemoryListMemoriesOptions): Promise; + /** + * Soft-delete every memory and message in this profile that is tagged + * with `sessionId`. + * + * Idempotent: deleting a sessionId that has no rows is a no-op. + * + * @param sessionId - Session to delete. + */ + deleteSession(sessionId: string): Promise; +} +/** + * Namespace-level Agent Memory binding. + * + * Used as the type of an `env.MEMORY`-style binding backed by the Agent + * Memory product. + * + * @example + * ```ts + * export default { + * async fetch(_request: Request, env: Env): Promise { + * const profile = await env.MEMORY.getProfile("wrangler-e2e"); + * const summary = await profile.getSummary(); + * return Response.json(summary); + * }, + * }; + * ``` + */ +declare abstract class AgentMemoryNamespace { + /** + * Get a memory profile by name. Profiles are isolated by namespace and + * addressed by a compound key (namespaceId:profileName). + * + * @param profileName - Profile name (validated against naming rules). + * @returns RPC target for interacting with the profile. + */ + getProfile(profileName: string): Promise; + /** + * Soft-delete a profile and schedule deferred purge. Marks all + * memories and messages as deleted. + * + * @param profileName - Name of the profile to delete. + */ + deleteProfile(profileName: string): Promise; +} // ============ AI Search Error Interfaces ============ interface AiSearchInternalError extends Error { } @@ -4873,9 +5103,6 @@ type ChatCompletionChoice = { finish_reason: "stop" | "length" | "tool_calls" | "content_filter" | "function_call"; logprobs: ChatCompletionLogprobs | null; }; -type ChatCompletionsPromptInput = { - prompt: string; -} & ChatCompletionsCommonOptions; type ChatCompletionsMessagesInput = { messages: Array; } & ChatCompletionsCommonOptions; @@ -8805,11 +9032,11 @@ declare abstract class Base_Ai_Cf_Pipecat_Ai_Smart_Turn_V2 { postProcessedOutputs: Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Output; } declare abstract class Base_Ai_Cf_Openai_Gpt_Oss_120B { - inputs: XOR; + inputs: XOR; postProcessedOutputs: XOR; } declare abstract class Base_Ai_Cf_Openai_Gpt_Oss_20B { - inputs: XOR; + inputs: XOR; postProcessedOutputs: XOR; } interface Ai_Cf_Leonardo_Phoenix_1_0_Input { @@ -9785,6 +10012,10 @@ declare abstract class Base_Ai_Cf_Moonshotai_Kimi_K2_5 { inputs: ChatCompletionsInput; postProcessedOutputs: ChatCompletionsOutput; } +declare abstract class Base_Ai_Cf_Moonshotai_Kimi_K2_6 { + inputs: ChatCompletionsInput; + postProcessedOutputs: ChatCompletionsOutput; +} declare abstract class Base_Ai_Cf_Nvidia_Nemotron_3_120B_A12B { inputs: ChatCompletionsInput; postProcessedOutputs: ChatCompletionsOutput; @@ -9882,7 +10113,9 @@ interface AiModels { "@cf/black-forest-labs/flux-2-klein-9b": Base_Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B; "@cf/zai-org/glm-4.7-flash": Base_Ai_Cf_Zai_Org_Glm_4_7_Flash; "@cf/moonshotai/kimi-k2.5": Base_Ai_Cf_Moonshotai_Kimi_K2_5; + "@cf/moonshotai/kimi-k2.6": Base_Ai_Cf_Moonshotai_Kimi_K2_6; "@cf/nvidia/nemotron-3-120b-a12b": Base_Ai_Cf_Nvidia_Nemotron_3_120B_A12B; + "@cf/google/gemma-4-26b-a4b-it": Base_Ai_Cf_Google_Gemma_4_26B_A4B_IT; } type AiOptions = { /** @@ -9935,10 +10168,8 @@ type AiModelsSearchObject = { value: string; }[]; }; -type ChatCompletionsBase = XOR; -type ChatCompletionsInput = XOR; +type ChatCompletionsBase = ChatCompletionsMessagesInput; +type ChatCompletionsInput = ChatCompletionsMessagesInput; interface InferenceUpstreamError extends Error { } interface AiInternalError extends Error { @@ -9983,8 +10214,15 @@ declare abstract class Ai { }, options?: AiOptions): Promise; // Normal (default) - known model run(model: Name, inputs: AiModelList[Name]['inputs'], options?: AiOptions): Promise; - // Unknown model (gateway fallback) - run(model: string & {}, inputs: Record, options?: AiOptions): Promise>; + // Unknown model (fallback). + // + // The `Exclude<..., keyof AiModelList>` constraint forces TypeScript to + // route any model name that is a literal key of `AiModelList` to one of + // the known-model overloads above (so input/output mismatches surface as + // type errors rather than silently falling back to `Record`). + // Names that aren't in `AiModelList` — e.g. third-party gateway models + // like `"google/nano-banana"` — still hit this overload. + run(model: Model extends keyof AiModelList ? never : Model, inputs: Record, options?: AiOptions): Promise>; models(params?: AiModelsSearchParams): Promise; toMarkdown(): ToMarkdownService; toMarkdown(files: MarkdownDocument[], options?: ConversionRequestOptions): Promise; @@ -10175,12 +10413,17 @@ interface ArtifactsTokenListResult { /** Total number of tokens for the repository. */ total: number; } -/** Handle for a single repository. Returned by Artifacts.get(). */ +/** + * Handle for a single repository. Returned by Artifacts.get(). + * + * Methods may throw `ArtifactsError` with code `INTERNAL_ERROR` if an unexpected service error occurs. + */ interface ArtifactsRepo extends ArtifactsRepoInfo { /** * Create an access token for this repo. * @param scope Token scope: "write" (default) or "read". * @param ttl Time-to-live in seconds (default 86400, min 60, max 31536000). + * @throws {ArtifactsError} with code `INVALID_TTL` if ttl is out of range. */ createToken(scope?: 'write' | 'read', ttl?: number): Promise; /** List tokens for this repo (metadata only, no plaintext). */ @@ -10189,6 +10432,7 @@ interface ArtifactsRepo extends ArtifactsRepoInfo { * Revoke a token by plaintext or ID. * @param tokenOrId Plaintext token or token ID. * @returns true if revoked, false if not found. + * @throws {ArtifactsError} with code `INVALID_INPUT` if tokenOrId is empty. */ revokeToken(tokenOrId: string): Promise; // ── Fork ── @@ -10196,6 +10440,9 @@ interface ArtifactsRepo extends ArtifactsRepoInfo { * Fork this repo to a new repo. * @param name Target repository name. * @param opts Optional: description, readOnly flag, defaultBranchOnly (default true). + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid. + * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the target repo already exists. + * @throws {ArtifactsError} with code `FORK_IN_PROGRESS` if a fork is already running. */ fork(name: string, opts?: { description?: string; @@ -10203,13 +10450,41 @@ interface ArtifactsRepo extends ArtifactsRepoInfo { defaultBranchOnly?: boolean; }): Promise; } -/** Artifacts binding — namespace-level operations. */ +// ── Error types ────────────────────────────────────────────────────────────── +/** + * Error codes returned by Artifacts binding operations. + * + * Each code maps to a numeric code available on `ArtifactsError.numericCode`. + */ +type ArtifactsErrorCode = 'ALREADY_EXISTS' | 'NOT_FOUND' | 'IMPORT_IN_PROGRESS' | 'FORK_IN_PROGRESS' | 'INVALID_INPUT' | 'INVALID_REPO_NAME' | 'INVALID_TTL' | 'INVALID_URL' | 'REMOTE_AUTH_REQUIRED' | 'UPSTREAM_UNAVAILABLE' | 'MEMORY_LIMIT' | 'INTERNAL_ERROR'; +/** + * Error thrown by Artifacts binding operations. + * + * Uses a string `.code` discriminator following the Cloudflare platform + * convention (StreamError, ImagesError, etc.). The `.numericCode` matches + * the REST API `errors[].code` values. + */ +interface ArtifactsError extends Error { + readonly name: 'ArtifactsError'; + /** String error code for programmatic matching. */ + readonly code: ArtifactsErrorCode; + /** Numeric error code matching the REST API. */ + readonly numericCode: number; +} +// ── Binding ────────────────────────────────────────────────────────────────── +/** + * Artifacts binding — namespace-level operations. + * + * Methods may throw `ArtifactsError` with code `INTERNAL_ERROR` if an unexpected service error occurs. + */ interface Artifacts { /** * Create a new repository with an initial access token. * @param name Repository name (alphanumeric, dots, hyphens, underscores). * @param opts Optional: readOnly flag, description, default branch name. * @returns Repo metadata with initial token. + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid. + * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the repo already exists. */ create(name: string, opts?: { readOnly?: boolean; @@ -10220,12 +10495,23 @@ interface Artifacts { * Get a handle to an existing repository. * @param name Repository name. * @returns Repo handle. + * @throws {ArtifactsError} with code `NOT_FOUND` if the repo does not exist. + * @throws {ArtifactsError} with code `IMPORT_IN_PROGRESS` if the repo is still importing. + * @throws {ArtifactsError} with code `FORK_IN_PROGRESS` if the repo is still forking. */ get(name: string): Promise; /** * Import a repository from an external git remote. * @param params Source URL and optional branch/depth, plus target name and options. * @returns Repo metadata with initial token. + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if the target name is invalid. + * @throws {ArtifactsError} with code `INVALID_INPUT` if the source URL is not valid HTTPS. + * @throws {ArtifactsError} with code `INVALID_URL` if the source URL does not point to a git repository. + * @throws {ArtifactsError} with code `REMOTE_AUTH_REQUIRED` if the remote requires authentication. + * @throws {ArtifactsError} with code `NOT_FOUND` if the remote repository does not exist. + * @throws {ArtifactsError} with code `UPSTREAM_UNAVAILABLE` if the remote cannot be reached. + * @throws {ArtifactsError} with code `MEMORY_LIMIT` if the import exceeds service memory limits. + * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the target repo already exists. */ import(params: { source: { @@ -10253,6 +10539,7 @@ interface Artifacts { * Delete a repository and all associated tokens. * @param name Repository name. * @returns true if deleted, false if not found. + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid. */ delete(name: string): Promise; } @@ -10393,6 +10680,452 @@ declare abstract class AutoRAG { */ aiSearch(params: AutoRagAiSearchRequest): Promise; } +type BrowserRunLifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'; +type BrowserRunResourceType = 'document' | 'stylesheet' | 'image' | 'media' | 'font' | 'script' | 'texttrack' | 'xhr' | 'fetch' | 'prefetch' | 'eventsource' | 'websocket' | 'manifest' | 'signedexchange' | 'ping' | 'cspviolationreport' | 'preflight' | 'other'; +/** Options fields shared by all quick actions. */ +interface BrowserRunBaseOptions { + /** Adds `